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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-eslint-parser",
3
- "version": "0.9.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.4.2",
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.17.4",
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 visitors during splice — no second pass.
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
- visitors: buildGlimmerVisitors(() => scopeManager, useTS),
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
- const err = new Error(e.source_code || e.message);
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;
@@ -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
- comments,
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
- // Build visitor keys: Program + all Glimmer node types
68
- const visitorKeys = hbsVisitorKeys;
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
- // Create an empty scope manager.
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
  }
@@ -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
- * Uses a getter for scopeManager so it's available after the parser callback runs.
154
- * @param {function} getScopeManager - returns the scopeManager (may be null initially)
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
- * @returns {object} visitors for toTree
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(getScopeManager, isTypescript) {
159
- return {
159
+ export function buildGlimmerVisitors(scopeManager, isTypescript, collectComments) {
160
+ if (!scopeManager) return null;
161
+ const visitors = {
160
162
  GlimmerPathExpression(node, path) {
161
- const sm = getScopeManager();
162
- if (sm) registerPathExpression(node, path, sm);
163
+ registerPathExpression(node, path, scopeManager);
163
164
  },
164
165
  GlimmerElementNode(node, path) {
165
- const sm = getScopeManager();
166
- if (sm) registerElementNode(node, path, sm);
166
+ registerElementNode(node, path, scopeManager);
167
167
  },
168
168
  GlimmerBlockParams(node, path) {
169
- const sm = getScopeManager();
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) {