From 7c803006afaf67a3314cdc72e0878b11956c6640 Mon Sep 17 00:00:00 2001 From: dngr2 Date: Tue, 18 Aug 2026 02:45:25 +0300 Subject: [PATCH] Do not glue a property-access dot onto an integer literal Formatting a numeric property access glued the object and the "." with no space (1 . x -> 1.x). In dialects where 1. is a valid number literal, 1. then re-lexes as a number and swallows the operator, so the output re-parses to a different tree and re-formatting is not idempotent. Insert a space in that case (as the layout already does to avoid gluing - onto - into a comment). Identifiers ending in a digit (t1.x) and decimals (1.5) are unaffected. --- src/formatter/Layout.ts | 12 ++++++++++++ test/mysql.test.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/formatter/Layout.ts b/src/formatter/Layout.ts index 39fd4071b7..e6b0a37237 100644 --- a/src/formatter/Layout.ts +++ b/src/formatter/Layout.ts @@ -63,6 +63,13 @@ export default class Layout { if (item.startsWith('-') && this.lastItemEndsWith('-')) { this.items.push(WS.SPACE); } + // Don't glue a "." onto a bare integer literal: "1." re-lexes as a number + // and absorbs the property-access operator, so "1 . x" would collapse to + // "1.x" and re-parse as a different expression. Identifiers that merely end + // in a digit ("t1.x") are unaffected. + if (item.startsWith('.') && this.lastItemIsIntegerLiteral()) { + this.items.push(WS.SPACE); + } this.items.push(item); } } @@ -73,6 +80,11 @@ export default class Layout { return typeof lastItem === 'string' && lastItem.endsWith(suffix); } + private lastItemIsIntegerLiteral(): boolean { + const lastItem = last(this.items); + return typeof lastItem === 'string' && /^[0-9]+$/.test(lastItem); + } + private trimHorizontalWhitespace() { while (isHorizontalWhitespace(last(this.items))) { this.items.pop(); diff --git a/test/mysql.test.ts b/test/mysql.test.ts index e6bcd37af9..0d99ab5816 100644 --- a/test/mysql.test.ts +++ b/test/mysql.test.ts @@ -98,6 +98,18 @@ describe('MySqlFormatter', () => { `); }); + it('keeps a numeric property access idempotent', () => { + // "1 ." must not glue into "1." which re-lexes as a number literal and + // swallows the property-access operator. + const sql = 'SELECT 1 . /*x*/ 5e'; + const result = dedent` + SELECT + 1 ./*x*/ 5e + `; + expect(format(sql)).toBe(result); + expect(format(result)).toBe(result); + }); + it('formats ALTER TABLE ... ALTER COLUMN', () => { expect( format(