ember-eslint-parser 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/parser/gjs-gts-parser.js +45 -3
- package/src/parser/hbs-parser.js +26 -17
- package/src/parser/transforms.js +37 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-eslint-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@glimmer/syntax": "^0.95.0",
|
|
24
24
|
"@typescript-eslint/tsconfig-utils": "^8.57.1",
|
|
25
25
|
"content-tag": "^4.1.1",
|
|
26
|
-
"ember-estree": "^0.
|
|
26
|
+
"ember-estree": "^0.6.2",
|
|
27
27
|
"eslint-scope": "^9.1.2",
|
|
28
28
|
"html-tags": "^5.1.0",
|
|
29
29
|
"mathml-tag-names": "^4.0.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"mitata": "1.0.34",
|
|
48
48
|
"prettier": "^3.2.5",
|
|
49
49
|
"publint": "^0.2.6",
|
|
50
|
-
"release-plan": "^0.
|
|
50
|
+
"release-plan": "^0.18.0",
|
|
51
51
|
"typescript": "^5.3.3",
|
|
52
52
|
"vite": "^5.0.12",
|
|
53
53
|
"vitest": "^1.2.2"
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
"node": ">=16.0.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
+
"bench": "./scripts/run-bench.sh tests/parser.bench.mjs",
|
|
68
|
+
"bench:compare": "node scripts/bench-compare.mjs",
|
|
69
|
+
"bench:summary": "./scripts/local-bench-summary.sh",
|
|
67
70
|
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
|
|
68
71
|
"lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
|
|
69
72
|
"lint:js": "eslint . --max-warnings=0",
|
|
70
73
|
"lint:js:fix": "eslint . --fix --max-warnings=0",
|
|
71
74
|
"lint:package": "publint",
|
|
72
|
-
"bench": "node --expose-gc tests/parser.bench.mjs",
|
|
73
|
-
"bench:compare": "node scripts/bench-compare.mjs",
|
|
74
|
-
"bench:summary": "./scripts/local-bench-summary.sh",
|
|
75
75
|
"test": "vitest run"
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -161,12 +161,14 @@ export function parseForESLint(code, options) {
|
|
|
161
161
|
const useTS = isTypescript || useTypescript;
|
|
162
162
|
|
|
163
163
|
// Both paths create scopeManager inside the parser callback so it's
|
|
164
|
-
// available when toTree invokes
|
|
164
|
+
// available when toTree invokes the visitor factory — no second pass.
|
|
165
165
|
let scopeManager = null;
|
|
166
|
+
const glimmerComments = [];
|
|
166
167
|
|
|
167
168
|
try {
|
|
168
169
|
const result = toTree(code, {
|
|
169
170
|
filePath,
|
|
171
|
+
tokens: true,
|
|
170
172
|
parser: useTS
|
|
171
173
|
? (placeholderJS) => {
|
|
172
174
|
let parseCode = placeholderJS;
|
|
@@ -196,11 +198,44 @@ export function parseForESLint(code, options) {
|
|
|
196
198
|
});
|
|
197
199
|
return { ast: program, scopeManager };
|
|
198
200
|
},
|
|
199
|
-
|
|
201
|
+
// Factory form: scopeManager is set by the parser callback before this
|
|
202
|
+
// runs. Returns null when unavailable so ember-estree skips the walk.
|
|
203
|
+
// The comments array is passed through so comment handlers share a
|
|
204
|
+
// single closure over it.
|
|
205
|
+
visitors: () => buildGlimmerVisitors(scopeManager, useTS, glimmerComments),
|
|
200
206
|
});
|
|
201
207
|
|
|
202
208
|
if (!result.scopeManager) result.scopeManager = scopeManager;
|
|
203
209
|
|
|
210
|
+
// Forward each placeholder's tsNode onto its GlimmerTemplate replacement,
|
|
211
|
+
// so typed rules calling `getTypeAtLocation(glimmerTemplate)` see the
|
|
212
|
+
// placeholder's type (string for expression templates, void for class-
|
|
213
|
+
// member static blocks) instead of the TS error intrinsic.
|
|
214
|
+
const esMap = result.services?.esTreeNodeToTSNodeMap;
|
|
215
|
+
if (esMap && result.templateInfos) {
|
|
216
|
+
for (const ti of result.templateInfos) {
|
|
217
|
+
const ts = esMap.get(ti.placeholder);
|
|
218
|
+
if (ts && !esMap.has(ti.ast)) esMap.set(ti.ast, ts);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Surface Glimmer template comments in program.comments as type:'Block'
|
|
223
|
+
// so ESLint's inline-config scanner and plugin rules recognise them.
|
|
224
|
+
if (glimmerComments.length > 0) {
|
|
225
|
+
const programNode = result.ast?.program ?? result.ast;
|
|
226
|
+
if (programNode) {
|
|
227
|
+
programNode.comments = [
|
|
228
|
+
...(programNode.comments || []),
|
|
229
|
+
...glimmerComments.map((c) => ({
|
|
230
|
+
type: 'Block',
|
|
231
|
+
value: c.value,
|
|
232
|
+
range: c.range,
|
|
233
|
+
loc: c.loc,
|
|
234
|
+
})),
|
|
235
|
+
].sort((a, b) => a.range[0] - b.range[0]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
204
239
|
if (result.services?.program) {
|
|
205
240
|
const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs;
|
|
206
241
|
if (
|
|
@@ -231,7 +266,14 @@ export function parseForESLint(code, options) {
|
|
|
231
266
|
.split(':')
|
|
232
267
|
.slice(-2)
|
|
233
268
|
.map((x) => parseInt(x));
|
|
234
|
-
|
|
269
|
+
// e.source_code is the full diagnostic block. Strip ANSI escape codes
|
|
270
|
+
// (content-tag colours output in TTY environments, which breaks
|
|
271
|
+
// grep-based test:check assertions) and trim leading/trailing blank
|
|
272
|
+
// lines so consumers see a clean message.
|
|
273
|
+
// eslint-disable-next-line no-control-regex
|
|
274
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
275
|
+
const errorText = e.source_code ? stripAnsi(e.source_code).trim() : e.message;
|
|
276
|
+
const err = new Error(errorText);
|
|
235
277
|
err.lineNumber = line;
|
|
236
278
|
err.column = column;
|
|
237
279
|
err.fileName = filePath;
|
package/src/parser/hbs-parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as eslintScope from 'eslint-scope';
|
|
2
2
|
import { toTree, glimmerVisitorKeys, DocumentLines } from 'ember-estree';
|
|
3
|
+
import { registerHBSScopes } from './transforms.js';
|
|
3
4
|
|
|
4
5
|
// Constant: Program + all Glimmer node types. Computed once at module load.
|
|
5
6
|
const hbsVisitorKeys = { Program: ['body'], ...glimmerVisitorKeys };
|
|
@@ -26,7 +27,7 @@ export function parseForESLint(code, options) {
|
|
|
26
27
|
|
|
27
28
|
let result;
|
|
28
29
|
try {
|
|
29
|
-
result = toTree(code, { templateOnly: true });
|
|
30
|
+
result = toTree(code, { templateOnly: true, tokens: true });
|
|
30
31
|
} catch (e) {
|
|
31
32
|
// Transform glimmer parse error to ESLint-compatible error
|
|
32
33
|
const loc = e.location || (e.hash && e.hash.loc);
|
|
@@ -54,7 +55,14 @@ export function parseForESLint(code, options) {
|
|
|
54
55
|
type: 'Program',
|
|
55
56
|
body: [templateNode],
|
|
56
57
|
tokens: templateNode.tokens,
|
|
57
|
-
|
|
58
|
+
// Normalize Glimmer comment nodes to type:'Block' so ESLint's inline-config
|
|
59
|
+
// scanner and plugin rules that filter on type recognise them as comments.
|
|
60
|
+
comments: (comments || []).map((c) => ({
|
|
61
|
+
type: 'Block',
|
|
62
|
+
value: c.value,
|
|
63
|
+
range: c.range,
|
|
64
|
+
loc: c.loc,
|
|
65
|
+
})),
|
|
58
66
|
range: [0, code.length],
|
|
59
67
|
start: 0,
|
|
60
68
|
end: code.length,
|
|
@@ -64,26 +72,27 @@ export function parseForESLint(code, options) {
|
|
|
64
72
|
},
|
|
65
73
|
};
|
|
66
74
|
|
|
67
|
-
//
|
|
68
|
-
|
|
75
|
+
// Analyze a stub Program then rebind the resulting global scope to the real
|
|
76
|
+
// Program. Analyzing the real Program directly causes infinite recursion in
|
|
77
|
+
// esrecurse because Glimmer subtree nodes carry parent back-links.
|
|
78
|
+
const stubProgram = {
|
|
79
|
+
type: 'Program',
|
|
80
|
+
body: [],
|
|
81
|
+
range: [0, code.length],
|
|
82
|
+
loc: program.loc,
|
|
83
|
+
};
|
|
84
|
+
const scopeManager = eslintScope.analyze(stubProgram, { range: true });
|
|
85
|
+
const globalScope = scopeManager.acquire(stubProgram);
|
|
86
|
+
globalScope.block = program;
|
|
87
|
+
scopeManager.__nodeToScope.delete(stubProgram);
|
|
88
|
+
scopeManager.__nodeToScope.set(program, [globalScope]);
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
// For HBS, all locals are assumed to be defined at runtime,
|
|
72
|
-
// so we don't track variable references (no no-undef errors).
|
|
73
|
-
const scopeManager = eslintScope.analyze(
|
|
74
|
-
{
|
|
75
|
-
type: 'Program',
|
|
76
|
-
body: [],
|
|
77
|
-
range: [0, code.length],
|
|
78
|
-
loc: program.loc,
|
|
79
|
-
},
|
|
80
|
-
{ range: true }
|
|
81
|
-
);
|
|
90
|
+
registerHBSScopes({ ast: program, scopeManager, visitorKeys: hbsVisitorKeys });
|
|
82
91
|
|
|
83
92
|
return {
|
|
84
93
|
ast: program,
|
|
85
94
|
scopeManager,
|
|
86
|
-
visitorKeys,
|
|
95
|
+
visitorKeys: hbsVisitorKeys,
|
|
87
96
|
services: {},
|
|
88
97
|
};
|
|
89
98
|
}
|
package/src/parser/transforms.js
CHANGED
|
@@ -28,13 +28,10 @@ try {
|
|
|
28
28
|
// ── Scope helpers ─────────────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
function findParentScope(scopeManager, nodePath) {
|
|
31
|
-
let scope = null;
|
|
32
31
|
let path = nodePath;
|
|
33
32
|
while (path) {
|
|
34
|
-
scope = scopeManager.acquire(path.node, true);
|
|
35
|
-
if (scope)
|
|
36
|
-
return scope;
|
|
37
|
-
}
|
|
33
|
+
const scope = scopeManager.acquire(path.node, true);
|
|
34
|
+
if (scope) return scope;
|
|
38
35
|
path = path.parentPath;
|
|
39
36
|
}
|
|
40
37
|
return null;
|
|
@@ -82,6 +79,7 @@ function isUpperCase(char) {
|
|
|
82
79
|
|
|
83
80
|
function registerBlockParams(node, path, scopeManager, isTypescript) {
|
|
84
81
|
const blockParamNodes = node.blockParamNodes || [];
|
|
82
|
+
if (blockParamNodes.length === 0) return;
|
|
85
83
|
const upperScope = findParentScope(scopeManager, path);
|
|
86
84
|
const scope = isTypescript
|
|
87
85
|
? new TypescriptScope.BlockScope(scopeManager, upperScope, node)
|
|
@@ -150,26 +148,33 @@ function registerElementNode(node, path, scopeManager) {
|
|
|
150
148
|
|
|
151
149
|
/**
|
|
152
150
|
* Build Glimmer visitors for toTree that register scopes during traversal.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* Returns null when scopeManager is unavailable so the caller can skip the walk.
|
|
152
|
+
* When `collectComments` is provided, appends Glimmer comment nodes to it so
|
|
153
|
+
* callers can forward them into program.comments after the walk.
|
|
154
|
+
* @param {object|null} scopeManager
|
|
155
155
|
* @param {boolean} isTypescript
|
|
156
|
-
* @
|
|
156
|
+
* @param {Array|null} [collectComments] - optional array to collect Glimmer comment nodes into
|
|
157
|
+
* @returns {object|null} visitors for toTree, or null to skip
|
|
157
158
|
*/
|
|
158
|
-
export function buildGlimmerVisitors(
|
|
159
|
-
return
|
|
159
|
+
export function buildGlimmerVisitors(scopeManager, isTypescript, collectComments) {
|
|
160
|
+
if (!scopeManager) return null;
|
|
161
|
+
const visitors = {
|
|
160
162
|
GlimmerPathExpression(node, path) {
|
|
161
|
-
|
|
162
|
-
if (sm) registerPathExpression(node, path, sm);
|
|
163
|
+
registerPathExpression(node, path, scopeManager);
|
|
163
164
|
},
|
|
164
165
|
GlimmerElementNode(node, path) {
|
|
165
|
-
|
|
166
|
-
if (sm) registerElementNode(node, path, sm);
|
|
166
|
+
registerElementNode(node, path, scopeManager);
|
|
167
167
|
},
|
|
168
168
|
GlimmerBlockParams(node, path) {
|
|
169
|
-
|
|
170
|
-
if (sm) registerBlockParams(node, path, sm, isTypescript);
|
|
169
|
+
registerBlockParams(node, path, scopeManager, isTypescript);
|
|
171
170
|
},
|
|
172
171
|
};
|
|
172
|
+
if (collectComments) {
|
|
173
|
+
const push = (node) => collectComments.push(node);
|
|
174
|
+
visitors.GlimmerMustacheCommentStatement = push;
|
|
175
|
+
visitors.GlimmerCommentStatement = push;
|
|
176
|
+
}
|
|
177
|
+
return visitors;
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
// ── registerGlimmerScopes (fallback for JS/oxc path) ──────────────────
|
|
@@ -240,6 +245,22 @@ export function registerGlimmerScopes(result) {
|
|
|
240
245
|
});
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Scope registration for the HBS parser. Unlike the gjs/gts path, we do not
|
|
250
|
+
* register references for free identifiers (`{{path}}`, `<Tag>`) — all
|
|
251
|
+
* template locals are treated as runtime-defined, so no-undef stays quiet.
|
|
252
|
+
* Only block params from `as |x|` constructs are declared.
|
|
253
|
+
*/
|
|
254
|
+
export function registerHBSScopes(result) {
|
|
255
|
+
traverse(result.visitorKeys, result.ast, (path) => {
|
|
256
|
+
const node = path.node;
|
|
257
|
+
if (!node) return;
|
|
258
|
+
if ('blockParams' in node && node.type.startsWith('Glimmer')) {
|
|
259
|
+
registerBlockParams(node, path, result.scopeManager, false);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
243
264
|
// ── transformForLint (used by ts-patch.js) ────────────────────────────
|
|
244
265
|
|
|
245
266
|
export const replaceRange = function replaceRange(s, start, end, substitute) {
|