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(