Skip to content

Commit b46414f

Browse files
committed
test: failing regression — program.comments is not sorted by range
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 = <template>{{! glimmer at 22 }}</template>; /* 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 </template>) 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.
1 parent 2b4d6d1 commit b46414f

1 file changed

Lines changed: 87 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Regression test for the sorted-by-range invariant on Program.comments.
3+
*
4+
* ESLint's SourceCode builds `tokensAndComments = sortedMerge(tokens, comments)`
5+
* and `createIndexMap(tokens, comments)` — both iterate comments and tokens
6+
* with a merge that assumes each array is already sorted by `range[0]`. Every
7+
* standard JS parser (espree, @babel/eslint-parser, @typescript-eslint/parser)
8+
* honors that invariant.
9+
*
10+
* When a .gts file has a JS block comment (slash-star style) interleaved between templates,
11+
* TS-parser comments are spread into `program.comments` first and Glimmer
12+
* template comments get appended — producing an array like
13+
* `[jsAt56, glimmerAt23]` whose order doesn't match range order. The
14+
* downstream effect is `sourceCode.getTokenBefore(glimmerComment)` /
15+
* `getTokenAfter(glimmerComment)` returning wrong tokens (or tokens from
16+
* entirely different template regions), because the `indexMap` keyed on
17+
* unsorted input points at the wrong token index.
18+
*/
19+
20+
import { describe, expect, it } from 'vitest';
21+
import { Linter } from 'eslint';
22+
import { parseForESLint } from '../src/parser/gjs-gts-parser.js';
23+
24+
describe('program.comments sort order (ESLint tokensAndComments invariant)', () => {
25+
const mixedSource = [
26+
'const X = <template>',
27+
' {{! glimmer comment at 22 }}',
28+
'</template>;',
29+
'/* js comment at 56 */',
30+
'const Y = 1;',
31+
].join('\n');
32+
33+
it('ast.comments is sorted by range[0]', () => {
34+
const { ast } = parseForESLint(mixedSource, {
35+
filePath: 't.gts',
36+
range: true,
37+
loc: true,
38+
comment: true,
39+
tokens: true,
40+
});
41+
const starts = (ast.comments || []).map((c) => c.range[0]);
42+
const sorted = [...starts].sort((a, b) => a - b);
43+
expect(starts).toEqual(sorted);
44+
});
45+
46+
it('getTokenBefore / getTokenAfter on a Glimmer comment return source-adjacent tokens', () => {
47+
const linter = new Linter();
48+
linter.defineParser('p', { parseForESLint });
49+
const probes = [];
50+
linter.defineRule('probe', {
51+
create(context) {
52+
return {
53+
'Program:exit'() {
54+
const sc = context.sourceCode;
55+
for (const c of sc.getAllComments()) {
56+
if (c.value.includes('glimmer')) {
57+
probes.push({
58+
commentRange: c.range,
59+
before: sc.getTokenBefore(c)?.range ?? null,
60+
after: sc.getTokenAfter(c)?.range ?? null,
61+
});
62+
}
63+
}
64+
},
65+
};
66+
},
67+
});
68+
linter.verify(
69+
mixedSource,
70+
{
71+
parser: 'p',
72+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
73+
rules: { probe: 'error' },
74+
},
75+
{ filename: 't.gts' }
76+
);
77+
expect(probes).toHaveLength(1);
78+
const { commentRange, before, after } = probes[0];
79+
// Whatever the exact adjacent token ranges are, they must bracket the
80+
// comment — the token before must end at or before the comment's start,
81+
// and the token after must start at or after the comment's end.
82+
expect(before).not.toBeNull();
83+
expect(after).not.toBeNull();
84+
expect(before[1]).toBeLessThanOrEqual(commentRange[0]);
85+
expect(after[0]).toBeGreaterThanOrEqual(commentRange[1]);
86+
});
87+
});

0 commit comments

Comments
 (0)