ember-eslint-parser 0.10.0 → 0.11.1

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.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -23,8 +23,9 @@
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
+ "espree": "^10.4.0",
28
29
  "html-tags": "^5.1.0",
29
30
  "mathml-tag-names": "^4.0.0",
30
31
  "svg-tags": "^1.0.0"
@@ -5,6 +5,7 @@ import { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } f
5
5
  import { buildGlimmerVisitors } from './transforms.js';
6
6
  import { toTree } from 'ember-estree';
7
7
  import * as eslintScope from 'eslint-scope';
8
+ import * as espree from 'espree';
8
9
 
9
10
  const require = createRequire(import.meta.url);
10
11
 
@@ -161,12 +162,14 @@ export function parseForESLint(code, options) {
161
162
  const useTS = isTypescript || useTypescript;
162
163
 
163
164
  // Both paths create scopeManager inside the parser callback so it's
164
- // available when toTree invokes visitors during splice — no second pass.
165
+ // available when toTree invokes the visitor factory — no second pass.
165
166
  let scopeManager = null;
167
+ const glimmerComments = [];
166
168
 
167
169
  try {
168
170
  const result = toTree(code, {
169
171
  filePath,
172
+ tokens: true,
170
173
  parser: useTS
171
174
  ? (placeholderJS) => {
172
175
  let parseCode = placeholderJS;
@@ -183,12 +186,24 @@ export function parseForESLint(code, options) {
183
186
  return tsResult;
184
187
  }
185
188
  : (placeholderJS) => {
186
- // JS path: parse with oxc, create scope manager from placeholder AST
187
- const { parseSync } = require('oxc-parser');
188
- const oxcResult = parseSync(filePath || 'input.js', placeholderJS);
189
- const program = oxcResult.program;
190
- program.tokens = oxcResult.tokens || [];
191
- program.comments = oxcResult.comments || [];
189
+ // JS path: parse with espree so the AST already carries loc/range,
190
+ // tokens, and comments — what ESLint validates and rules consume.
191
+ //
192
+ // We'd prefer oxc-parser here (faster, already used elsewhere in
193
+ // the pipeline), but its JS API exposes only `start`/`end` byte
194
+ // offsets and an opt-in `range` array — no `loc`, no token
195
+ // stream — so its output fails ESLint's SourceCode validation
196
+ // on the Program node and breaks any rule that walks tokens.
197
+ // Switch back once oxc lands native loc support:
198
+ // https://github.com/oxc-project/oxc/issues/10307
199
+ const program = espree.parse(placeholderJS, {
200
+ ecmaVersion: 'latest',
201
+ sourceType: 'module',
202
+ loc: true,
203
+ range: true,
204
+ tokens: true,
205
+ comment: true,
206
+ });
192
207
  scopeManager = eslintScope.analyze(program, {
193
208
  ecmaVersion: 2022,
194
209
  sourceType: 'module',
@@ -196,11 +211,44 @@ export function parseForESLint(code, options) {
196
211
  });
197
212
  return { ast: program, scopeManager };
198
213
  },
199
- visitors: buildGlimmerVisitors(() => scopeManager, useTS),
214
+ // Factory form: scopeManager is set by the parser callback before this
215
+ // runs. Returns null when unavailable so ember-estree skips the walk.
216
+ // The comments array is passed through so comment handlers share a
217
+ // single closure over it.
218
+ visitors: () => buildGlimmerVisitors(scopeManager, useTS, glimmerComments),
200
219
  });
201
220
 
202
221
  if (!result.scopeManager) result.scopeManager = scopeManager;
203
222
 
223
+ // Forward each placeholder's tsNode onto its GlimmerTemplate replacement,
224
+ // so typed rules calling `getTypeAtLocation(glimmerTemplate)` see the
225
+ // placeholder's type (string for expression templates, void for class-
226
+ // member static blocks) instead of the TS error intrinsic.
227
+ const esMap = result.services?.esTreeNodeToTSNodeMap;
228
+ if (esMap && result.templateInfos) {
229
+ for (const ti of result.templateInfos) {
230
+ const ts = esMap.get(ti.placeholder);
231
+ if (ts && !esMap.has(ti.ast)) esMap.set(ti.ast, ts);
232
+ }
233
+ }
234
+
235
+ // Surface Glimmer template comments in program.comments as type:'Block'
236
+ // so ESLint's inline-config scanner and plugin rules recognise them.
237
+ if (glimmerComments.length > 0) {
238
+ const programNode = result.ast?.program ?? result.ast;
239
+ if (programNode) {
240
+ programNode.comments = [
241
+ ...(programNode.comments || []),
242
+ ...glimmerComments.map((c) => ({
243
+ type: 'Block',
244
+ value: c.value,
245
+ range: c.range,
246
+ loc: c.loc,
247
+ })),
248
+ ].sort((a, b) => a.range[0] - b.range[0]);
249
+ }
250
+ }
251
+
204
252
  if (result.services?.program) {
205
253
  const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs;
206
254
  if (
@@ -231,7 +279,14 @@ export function parseForESLint(code, options) {
231
279
  .split(':')
232
280
  .slice(-2)
233
281
  .map((x) => parseInt(x));
234
- const err = new Error(e.source_code || e.message);
282
+ // e.source_code is the full diagnostic block. Strip ANSI escape codes
283
+ // (content-tag colours output in TTY environments, which breaks
284
+ // grep-based test:check assertions) and trim leading/trailing blank
285
+ // lines so consumers see a clean message.
286
+ // eslint-disable-next-line no-control-regex
287
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
288
+ const errorText = e.source_code ? stripAnsi(e.source_code).trim() : e.message;
289
+ const err = new Error(errorText);
235
290
  err.lineNumber = line;
236
291
  err.column = column;
237
292
  err.fileName = filePath;
@@ -27,7 +27,7 @@ export function parseForESLint(code, options) {
27
27
 
28
28
  let result;
29
29
  try {
30
- result = toTree(code, { templateOnly: true });
30
+ result = toTree(code, { templateOnly: true, tokens: true });
31
31
  } catch (e) {
32
32
  // Transform glimmer parse error to ESLint-compatible error
33
33
  const loc = e.location || (e.hash && e.hash.loc);
@@ -55,7 +55,14 @@ export function parseForESLint(code, options) {
55
55
  type: 'Program',
56
56
  body: [templateNode],
57
57
  tokens: templateNode.tokens,
58
- 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
+ })),
59
66
  range: [0, code.length],
60
67
  start: 0,
61
68
  end: code.length,
@@ -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;
@@ -151,26 +148,33 @@ function registerElementNode(node, path, scopeManager) {
151
148
 
152
149
  /**
153
150
  * Build Glimmer visitors for toTree that register scopes during traversal.
154
- * Uses a getter for scopeManager so it's available after the parser callback runs.
155
- * @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
156
155
  * @param {boolean} isTypescript
157
- * @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
158
158
  */
159
- export function buildGlimmerVisitors(getScopeManager, isTypescript) {
160
- return {
159
+ export function buildGlimmerVisitors(scopeManager, isTypescript, collectComments) {
160
+ if (!scopeManager) return null;
161
+ const visitors = {
161
162
  GlimmerPathExpression(node, path) {
162
- const sm = getScopeManager();
163
- if (sm) registerPathExpression(node, path, sm);
163
+ registerPathExpression(node, path, scopeManager);
164
164
  },
165
165
  GlimmerElementNode(node, path) {
166
- const sm = getScopeManager();
167
- if (sm) registerElementNode(node, path, sm);
166
+ registerElementNode(node, path, scopeManager);
168
167
  },
169
168
  GlimmerBlockParams(node, path) {
170
- const sm = getScopeManager();
171
- if (sm) registerBlockParams(node, path, sm, isTypescript);
169
+ registerBlockParams(node, path, scopeManager, isTypescript);
172
170
  },
173
171
  };
172
+ if (collectComments) {
173
+ const push = (node) => collectComments.push(node);
174
+ visitors.GlimmerMustacheCommentStatement = push;
175
+ visitors.GlimmerCommentStatement = push;
176
+ }
177
+ return visitors;
174
178
  }
175
179
 
176
180
  // ── registerGlimmerScopes (fallback for JS/oxc path) ──────────────────