From ef48542a6b3c63e29bfe9b2805fc64a8a18a5702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Fri, 24 Apr 2026 09:08:10 +0200 Subject: [PATCH 1/2] =?UTF-8?q?test:=20failing=20regression=20=E2=80=94=20?= =?UTF-8?q?program.comments=20is=20not=20sorted=20by=20range?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESLint's SourceCode builds both `tokensAndComments = sortedMerge(tokens, comments)` and the token-store's `createIndexMap(tokens, comments)` assuming each input array is sorted by `range[0]`. Every standard JS parser (espree, @babel/eslint-parser, @typescript-eslint/parser) honors that invariant. When a .gts file has a JS /* */ comment interleaved between templates, TS-parser comments are spread into program.comments first and Glimmer template comments get appended — producing an array whose order doesn't match range order: const X = ; /* js at 56 */ const Y = 1; ast.comments: [[65,87] js, [23,51] glimmer] ← unsorted The downstream effect is that `sourceCode.getTokenBefore(glimmer)` / `getTokenAfter(glimmer)` return wrong tokens: before: ";" @ [63,64] (wrong — that's *after* the comment) after: "const" @ [88,93] (skips ) Adds two tests: 1. ast.comments is sorted by range[0] — direct structural assertion. 2. getTokenBefore / getTokenAfter on a Glimmer comment return source-adjacent tokens — end-to-end through Linter. Both currently fail; they'll pass once program.comments is sorted. --- tests/program-comments-sort-order.test.js | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/program-comments-sort-order.test.js diff --git a/tests/program-comments-sort-order.test.js b/tests/program-comments-sort-order.test.js new file mode 100644 index 0000000..1a5e63d --- /dev/null +++ b/tests/program-comments-sort-order.test.js @@ -0,0 +1,87 @@ +/** + * Regression test for the sorted-by-range invariant on Program.comments. + * + * ESLint's SourceCode builds `tokensAndComments = sortedMerge(tokens, comments)` + * and `createIndexMap(tokens, comments)` — both iterate comments and tokens + * with a merge that assumes each array is already sorted by `range[0]`. Every + * standard JS parser (espree, @babel/eslint-parser, @typescript-eslint/parser) + * honors that invariant. + * + * When a .gts file has a JS block comment (slash-star style) interleaved between templates, + * TS-parser comments are spread into `program.comments` first and Glimmer + * template comments get appended — producing an array like + * `[jsAt56, glimmerAt23]` whose order doesn't match range order. The + * downstream effect is `sourceCode.getTokenBefore(glimmerComment)` / + * `getTokenAfter(glimmerComment)` returning wrong tokens (or tokens from + * entirely different template regions), because the `indexMap` keyed on + * unsorted input points at the wrong token index. + */ + +import { describe, expect, it } from 'vitest'; +import { Linter } from 'eslint'; +import { parseForESLint } from '../src/parser/gjs-gts-parser.js'; + +describe('program.comments sort order (ESLint tokensAndComments invariant)', () => { + const mixedSource = [ + 'const X = ;', + '/* js comment at 56 */', + 'const Y = 1;', + ].join('\n'); + + it('ast.comments is sorted by range[0]', () => { + const { ast } = parseForESLint(mixedSource, { + filePath: 't.gts', + range: true, + loc: true, + comment: true, + tokens: true, + }); + const starts = (ast.comments || []).map((c) => c.range[0]); + const sorted = [...starts].sort((a, b) => a - b); + expect(starts).toEqual(sorted); + }); + + it('getTokenBefore / getTokenAfter on a Glimmer comment return source-adjacent tokens', () => { + const linter = new Linter(); + linter.defineParser('p', { parseForESLint }); + const probes = []; + linter.defineRule('probe', { + create(context) { + return { + 'Program:exit'() { + const sc = context.sourceCode; + for (const c of sc.getAllComments()) { + if (c.value.includes('glimmer')) { + probes.push({ + commentRange: c.range, + before: sc.getTokenBefore(c)?.range ?? null, + after: sc.getTokenAfter(c)?.range ?? null, + }); + } + } + }, + }; + }, + }); + linter.verify( + mixedSource, + { + parser: 'p', + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, + rules: { probe: 'error' }, + }, + { filename: 't.gts' } + ); + expect(probes).toHaveLength(1); + const { commentRange, before, after } = probes[0]; + // Whatever the exact adjacent token ranges are, they must bracket the + // comment — the token before must end at or before the comment's start, + // and the token after must start at or after the comment's end. + expect(before).not.toBeNull(); + expect(after).not.toBeNull(); + expect(before[1]).toBeLessThanOrEqual(commentRange[0]); + expect(after[0]).toBeGreaterThanOrEqual(commentRange[1]); + }); +}); From 20410ac12e2e5d19ee31f08720e80bebfaeb35fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Fri, 24 Apr 2026 15:19:17 +0200 Subject: [PATCH 2/2] fix: sort program.comments by range[0] after appending Glimmer comments ESLint's SourceCode.tokensAndComments merge and createIndexMap both assume comments are sorted by range[0]; appending Glimmer comments after the TS-parser comments breaks that invariant when a JS block comment sits between templates, producing wrong results from getTokenBefore/getTokenAfter on template comments. --- src/parser/gjs-gts-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/gjs-gts-parser.js b/src/parser/gjs-gts-parser.js index d8213a1..ab70f90 100644 --- a/src/parser/gjs-gts-parser.js +++ b/src/parser/gjs-gts-parser.js @@ -232,7 +232,7 @@ export function parseForESLint(code, options) { range: c.range, loc: c.loc, })), - ]; + ].sort((a, b) => a.range[0] - b.range[0]); } }