ember-eslint-parser 0.7.0 → 0.9.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.
@@ -1,11 +1,17 @@
1
- const ContentTag = require('content-tag');
2
- const glimmer = require('@glimmer/syntax');
3
- const { visitorKeys: glimmerVisitorKeys } = glimmer;
4
- const DocumentLines = require('../utils/document');
5
- const { Reference, Scope, Variable, Definition } = require('eslint-scope');
6
- const htmlTags = require('html-tags').default;
7
- const svgTags = require('svg-tags');
8
- const mathMLTags = require('mathml-tag-names').mathmlTagNames;
1
+ import { createRequire } from 'node:module';
2
+ import { Preprocessor } from 'content-tag';
3
+ import { isKeyword as glimmerIsKeyword } from '@glimmer/syntax';
4
+ import { glimmerVisitorKeys } from 'ember-estree';
5
+ import { Reference, Scope, Variable, Definition } from 'eslint-scope';
6
+ import htmlTags from 'html-tags';
7
+ import svgTags from 'svg-tags';
8
+ import { mathmlTagNames } from 'mathml-tag-names';
9
+
10
+ const htmlTagsSet = new Set(htmlTags);
11
+ const svgTagsSet = new Set(svgTags);
12
+ const mathMLTagsSet = new Set(mathmlTagNames);
13
+
14
+ const require = createRequire(import.meta.url);
9
15
 
10
16
  let TypescriptScope = null;
11
17
  try {
@@ -19,37 +25,8 @@ try {
19
25
  // not available
20
26
  }
21
27
 
22
- const BufferMap = new Map();
23
-
24
- function getBuffer(str) {
25
- let buf = BufferMap.get(str);
26
- if (!buf) {
27
- buf = Buffer.from(str);
28
- BufferMap.set(str, buf);
29
- }
30
- return buf;
31
- }
32
-
33
- function sliceByteRange(str, a, b) {
34
- const buf = getBuffer(str);
35
- return buf.slice(a, b).toString();
36
- }
28
+ // ── Scope helpers ─────────────────────────────────────────────────────
37
29
 
38
- function byteToCharIndex(str, byteOffset) {
39
- const buf = getBuffer(str);
40
- return buf.slice(0, byteOffset).toString().length;
41
- }
42
-
43
- function byteLength(str) {
44
- return getBuffer(str).length;
45
- }
46
-
47
- /**
48
- * finds the nearest node scope
49
- * @param scopeManager
50
- * @param nodePath
51
- * @return {*|null}
52
- */
53
30
  function findParentScope(scopeManager, nodePath) {
54
31
  let scope = null;
55
32
  let path = nodePath;
@@ -63,46 +40,33 @@ function findParentScope(scopeManager, nodePath) {
63
40
  return null;
64
41
  }
65
42
 
66
- /**
67
- * tries to find the variable names {name} in any parent scope
68
- * if the variable is not found it just returns the nearest scope,
69
- * so that it's usage can be registered.
70
- * @param scopeManager
71
- * @param nodePath
72
- * @param name
73
- * @return {{scope: null, variable: *}|{scope: (*|null)}}
74
- */
75
43
  function findVarInParentScopes(scopeManager, nodePath, name) {
76
- let scope = null;
44
+ let defScope = null;
45
+ let currentScope = null;
77
46
  let path = nodePath;
78
47
  while (path) {
79
- scope = scopeManager.acquire(path.node, true);
80
- if (scope && scope.set.has(name)) {
81
- break;
48
+ const s = scopeManager.acquire(path.node, true);
49
+ if (s) {
50
+ if (!currentScope) currentScope = s;
51
+ if (s.set.has(name)) {
52
+ defScope = s;
53
+ break;
54
+ }
82
55
  }
83
56
  path = path.parentPath;
84
57
  }
85
- const currentScope = findParentScope(scopeManager, nodePath);
86
- if (!scope) {
58
+ if (!defScope) {
87
59
  return { scope: currentScope };
88
60
  }
89
-
90
- return { scope: currentScope, variable: scope.set.get(name) };
61
+ return { scope: currentScope, variable: defScope.set.get(name) };
91
62
  }
92
63
 
93
- /**
94
- * registers a node variable usage in the scope.
95
- * @param node
96
- * @param scope
97
- * @param variable
98
- */
99
64
  function registerNodeInScope(node, scope, variable) {
100
65
  const ref = new Reference(node, scope, Reference.READ);
101
66
  if (variable) {
102
67
  variable.references.push(ref);
103
68
  ref.resolved = variable;
104
69
  } else {
105
- // register missing variable in most upper scope.
106
70
  let s = scope;
107
71
  while (s.upper) {
108
72
  s = s.upper;
@@ -112,32 +76,106 @@ function registerNodeInScope(node, scope, variable) {
112
76
  scope.references.push(ref);
113
77
  }
114
78
 
115
- /**
116
- * Builds the complete Glimmer visitor keys map with "Glimmer" prefix and
117
- * additional keys needed for traversal (blockParamNodes, parts, etc).
118
- * @return {object}
119
- */
120
- function buildGlimmerVisitorKeys() {
121
- const keys = {};
122
- for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
123
- keys[`Glimmer${k}`] = [...v];
79
+ function isUpperCase(char) {
80
+ return char.toUpperCase() === char;
81
+ }
82
+
83
+ function registerBlockParams(node, path, scopeManager, isTypescript) {
84
+ const blockParamNodes = node.blockParamNodes || [];
85
+ const upperScope = findParentScope(scopeManager, path);
86
+ const scope = isTypescript
87
+ ? new TypescriptScope.BlockScope(scopeManager, upperScope, node)
88
+ : new Scope(scopeManager, 'block', upperScope, node);
89
+ const declaredVariables = scopeManager.declaredVariables || scopeManager.__declaredVariables;
90
+ const vars = [];
91
+ declaredVariables.set(node, vars);
92
+ const virtualJSParentNode = {
93
+ type: 'FunctionDeclaration',
94
+ params: blockParamNodes,
95
+ range: node.range,
96
+ loc: node.loc,
97
+ parent: path.parent,
98
+ };
99
+ for (const [i, b] of blockParamNodes.entries()) {
100
+ const v = new Variable(b.name, scope);
101
+ v.identifiers.push(b);
102
+ scope.variables.push(v);
103
+ scope.set.set(b.name, v);
104
+ vars.push(v);
105
+
106
+ const virtualJSNode = {
107
+ type: 'Identifier',
108
+ name: b.name,
109
+ range: b.range,
110
+ loc: b.loc,
111
+ parent: virtualJSParentNode,
112
+ };
113
+ v.defs.push(new Definition('Parameter', virtualJSNode, node, node, i, 'Block Param'));
114
+ v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
124
115
  }
125
- if (!keys.GlimmerElementNode.includes('blockParamNodes')) {
126
- keys.GlimmerElementNode.push('blockParamNodes', 'parts');
116
+ }
117
+
118
+ function registerPathExpression(node, path, scopeManager) {
119
+ if (node.head.type !== 'VarHead') return;
120
+ const name = node.head.name;
121
+ if (glimmerIsKeyword(name)) return;
122
+ const { scope, variable } = findVarInParentScopes(scopeManager, path, name) || {};
123
+ if (scope) {
124
+ node.head.parent = node;
125
+ registerNodeInScope(node.head, scope, variable);
126
+ }
127
+ }
128
+
129
+ function registerElementNode(node, path, scopeManager) {
130
+ const n = node.parts[0];
131
+ const { scope, variable } = findVarInParentScopes(scopeManager, path, n.name) || {};
132
+ const ignore =
133
+ n.name === 'this' ||
134
+ n.name.startsWith(':') ||
135
+ n.name.startsWith('@') ||
136
+ !scope ||
137
+ n.name.includes('-');
138
+
139
+ const registerUndef =
140
+ isUpperCase(n.name[0]) ||
141
+ node.name.includes('.') ||
142
+ (!htmlTagsSet.has(node.name) && !svgTagsSet.has(node.name) && !mathMLTagsSet.has(node.name));
143
+
144
+ if (!ignore && (variable || registerUndef)) {
145
+ registerNodeInScope(n, scope, variable);
127
146
  }
128
- keys.GlimmerProgram = ['body', 'blockParamNodes'];
129
- keys.GlimmerTemplate = ['body'];
130
- return keys;
131
147
  }
132
148
 
149
+ // ── Visitor builders ──────────────────────────────────────────────────
150
+
133
151
  /**
134
- * traverses all nodes using the {visitorKeys} calling the callback function, visitor
135
- * @param visitorKeys
136
- * @param node
137
- * @param visitor
152
+ * 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)
155
+ * @param {boolean} isTypescript
156
+ * @returns {object} visitors for toTree
138
157
  */
158
+ export function buildGlimmerVisitors(getScopeManager, isTypescript) {
159
+ return {
160
+ GlimmerPathExpression(node, path) {
161
+ const sm = getScopeManager();
162
+ if (sm) registerPathExpression(node, path, sm);
163
+ },
164
+ GlimmerElementNode(node, path) {
165
+ const sm = getScopeManager();
166
+ if (sm) registerElementNode(node, path, sm);
167
+ },
168
+ GlimmerBlockParams(node, path) {
169
+ const sm = getScopeManager();
170
+ if (sm) registerBlockParams(node, path, sm, isTypescript);
171
+ },
172
+ };
173
+ }
174
+
175
+ // ── registerGlimmerScopes (fallback for JS/oxc path) ──────────────────
176
+
139
177
  function traverse(visitorKeys, node, visitor) {
140
- const allVisitorKeys = { ...visitorKeys, ...buildGlimmerVisitorKeys() };
178
+ const allVisitorKeys = { ...visitorKeys, ...glimmerVisitorKeys };
141
179
  const queue = [];
142
180
 
143
181
  queue.push({
@@ -150,19 +188,12 @@ function traverse(visitorKeys, node, visitor) {
150
188
 
151
189
  while (queue.length > 0) {
152
190
  const currentPath = queue.pop();
153
-
154
191
  visitor(currentPath);
155
-
156
192
  if (!currentPath.node) continue;
157
-
158
- const visitorKeys = allVisitorKeys[currentPath.node.type];
159
- if (!visitorKeys) {
160
- continue;
161
- }
162
-
163
- for (const visitorKey of visitorKeys) {
193
+ const keys = allVisitorKeys[currentPath.node.type];
194
+ if (!keys) continue;
195
+ for (const visitorKey of keys) {
164
196
  const child = currentPath.node[visitorKey];
165
-
166
197
  if (!child) {
167
198
  continue;
168
199
  } else if (Array.isArray(child)) {
@@ -170,7 +201,7 @@ function traverse(visitorKeys, node, visitor) {
170
201
  queue.push({
171
202
  node: item,
172
203
  parent: currentPath.node,
173
- context: Object.assign({}, currentPath.context),
204
+ context: currentPath.context,
174
205
  parentKey: visitorKey,
175
206
  parentPath: currentPath,
176
207
  });
@@ -179,7 +210,7 @@ function traverse(visitorKeys, node, visitor) {
179
210
  queue.push({
180
211
  node: child,
181
212
  parent: currentPath.node,
182
- context: Object.assign({}, currentPath.context),
213
+ context: currentPath.context,
183
214
  parentKey: visitorKey,
184
215
  parentPath: currentPath,
185
216
  });
@@ -188,445 +219,34 @@ function traverse(visitorKeys, node, visitor) {
188
219
  }
189
220
  }
190
221
 
191
- function isUpperCase(char) {
192
- return char.toUpperCase() === char;
193
- }
194
-
195
- function isAlphaNumeric(code) {
196
- return !(
197
- !(code > 47 && code < 58) && // numeric (0-9)
198
- !(code > 64 && code < 91) && // upper alpha (A-Z)
199
- !(code > 96 && code < 123)
200
- );
201
- }
202
-
203
- function isWhiteSpace(code) {
204
- return code === ' ' || code === '\t' || code === '\r' || code === '\n' || code === '\v';
205
- }
206
-
207
- /**
208
- * simple tokenizer for templates, just splits it up into words and punctuators
209
- * @param template {string}
210
- * @param startOffset {number}
211
- * @param doc {DocumentLines}
212
- * @return {Token[]}
213
- */
214
- function tokenize(template, doc, startOffset) {
215
- const tokens = [];
216
- let current = '';
217
- let start = 0;
218
- function pushToken(value, type, range) {
219
- const t = {
220
- type,
221
- value,
222
- range,
223
- start: range[0],
224
- end: range[1],
225
- loc: {
226
- start: { ...doc.offsetToPosition(range[0]), index: range[0] },
227
- end: { ...doc.offsetToPosition(range[1]), index: range[1] },
228
- },
229
- };
230
- tokens.push(t);
231
- }
232
- for (const [i, c] of [...template].entries()) {
233
- if (isAlphaNumeric(c.codePointAt(0))) {
234
- if (current.length === 0) {
235
- start = i;
236
- }
237
- current += c;
238
- } else {
239
- let range = [startOffset + start, startOffset + i];
240
- if (current.length > 0) {
241
- pushToken(current, 'word', range);
242
- current = '';
243
- }
244
- range = [startOffset + i, startOffset + i + 1];
245
- if (!isWhiteSpace(c)) {
246
- pushToken(c, 'Punctuator', range);
247
- }
248
- }
249
- }
250
- return tokens;
251
- }
252
-
253
- /**
254
- * Traverses a Glimmer AST, sets parent references, and categorizes nodes.
255
- * @param {object} ast
256
- * @return {{ allNodes: object[], comments: object[], textNodes: object[], emptyTextNodes: object[] }}
257
- */
258
- function collectNodes(ast) {
259
- const allNodes = [];
260
- const comments = [];
261
- const textNodes = [];
262
- const emptyTextNodes = [];
263
-
264
- glimmer.traverse(ast, {
265
- All(node, path) {
266
- node.parent = path.parentNode;
267
- allNodes.push(node);
268
- if (node.type === 'CommentStatement' || node.type === 'MustacheCommentStatement') {
269
- comments.push(node);
270
- }
271
- if (node.type === 'TextNode') {
272
- node.value = node.chars;
273
- if (node.value.trim().length !== 0 || (node.parent && node.parent.type === 'AttrNode')) {
274
- textNodes.push(node);
275
- } else {
276
- emptyTextNodes.push(node);
277
- }
278
- }
279
- },
280
- });
281
-
282
- return { allNodes, comments, textNodes, emptyTextNodes };
283
- }
284
-
285
- /**
286
- * Removes nodes from their parent's children/body/parts arrays.
287
- * @param {object[]} nodes
288
- */
289
- function removeFromParent(nodes) {
290
- for (const node of nodes) {
291
- const children =
292
- (node.parent && (node.parent.children || node.parent.body || node.parent.parts)) || [];
293
- const idx = children.indexOf(node);
294
- if (idx >= 0) {
295
- children.splice(idx, 1);
296
- }
297
- }
298
- }
299
-
300
222
  /**
301
- * Builds the final token stream by filtering out tokens covered by comments
302
- * or text nodes, then merging text nodes back in sorted order.
303
- * @param {object[]} rawTokens
304
- * @param {object[]} comments
305
- * @param {object[]} textNodes
306
- * @return {object[]}
223
+ * Full AST traversal for scope registration — used as fallback for JS/oxc path.
224
+ * For the TS path, toTree's visitor API handles this during splicing.
307
225
  */
308
- function buildTokenStream(rawTokens, comments, textNodes) {
309
- let tokens = rawTokens.filter(
310
- (t) => !comments.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
311
- );
312
- tokens = tokens.filter(
313
- (t) => !textNodes.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
314
- );
315
-
316
- const sortedTextNodes = [...textNodes];
317
- let currentTextNode = sortedTextNodes.pop();
318
- for (let i = tokens.length - 1; i >= 0; i--) {
319
- const t = tokens[i];
320
- while (currentTextNode && t.range[0] < currentTextNode.range[0]) {
321
- tokens.splice(i + 1, 0, currentTextNode);
322
- currentTextNode = sortedTextNodes.pop();
323
- }
324
- }
325
- if (currentTextNode) {
326
- tokens.unshift(currentTextNode);
327
- }
328
-
329
- return tokens;
330
- }
331
-
332
- /**
333
- * Parses a Glimmer template and produces a processed AST ready for ESLint.
334
- * Shared between hbs-parser (standalone .hbs files) and gjs/gts parser (embedded templates).
335
- *
336
- * @param {object} options
337
- * @param {string} options.templateContent - The template string to parse with glimmer
338
- * @param {DocumentLines} options.codeLines - DocumentLines for the full source file
339
- * @param {[number, number]} options.templateRange - Range [start, end] for the Template root node
340
- * @param {string} [options.tokenSource] - String to tokenize (defaults to templateContent)
341
- * @return {{ ast: object, comments: object[] }}
342
- */
343
- function processGlimmerTemplate({ templateContent, codeLines, templateRange, tokenSource }) {
344
- const offset = templateRange[0];
345
- const docLines = new DocumentLines(templateContent);
346
-
347
- /** Convert a Glimmer loc to a file-level [start, end] range */
348
- const toFileRange = (loc) => [
349
- offset + docLines.positionToOffset(loc.start),
350
- offset + docLines.positionToOffset(loc.end),
351
- ];
352
- /** Convert a file-level range to a file-level loc */
353
- const toFileLoc = (range) => ({
354
- start: codeLines.offsetToPosition(range[0]),
355
- end: codeLines.offsetToPosition(range[1]),
356
- });
357
-
358
- const ast = glimmer.preprocess(templateContent, { mode: 'codemod' });
359
- const { allNodes, comments, textNodes, emptyTextNodes } = collectNodes(ast);
360
-
361
- // Fix ranges, locs, and prefix types with "Glimmer"
362
- for (const n of allNodes) {
363
- if (n.type === 'PathExpression') {
364
- n.head.range = toFileRange(n.head.loc);
365
- n.head.loc = toFileLoc(n.head.range);
366
- }
367
-
368
- n.range = n.type === 'Template' ? [...templateRange] : toFileRange(n.loc);
369
- n.start = n.range[0];
370
- n.end = n.range[1];
371
- n.loc = toFileLoc(n.range);
372
-
373
- if (n.type === 'ElementNode') {
374
- n.name = n.tag;
375
- n.parts = [n.path.head].map((p) => {
376
- const range = toFileRange(p.loc);
377
- return {
378
- ...p,
379
- name: p.original,
380
- parent: n,
381
- type: 'GlimmerElementNodePart',
382
- range,
383
- loc: toFileLoc(range),
384
- };
385
- });
386
- }
387
-
388
- if ('blockParams' in n) {
389
- n.params = (n.params || []).map((p) => {
390
- const range = toFileRange(p.loc);
391
- return {
392
- ...p,
393
- type: 'BlockParam',
394
- name: p.original,
395
- parent: n,
396
- range,
397
- loc: toFileLoc(range),
398
- };
399
- });
400
- }
401
-
402
- n.type = `Glimmer${n.type}`;
403
- }
404
-
405
- // Clean up AST structure
406
- removeFromParent(emptyTextNodes);
407
- removeFromParent(comments);
408
- for (const comment of comments) {
409
- comment.type = 'Block';
410
- }
411
-
412
- // Nullify empty hashes
413
- for (const n of allNodes) {
414
- if (
415
- ['GlimmerMustacheStatement', 'GlimmerBlockStatement', 'GlimmerSubExpression'].includes(n.type)
416
- ) {
417
- if (n.hash && n.hash.pairs && n.hash.pairs.length === 0) {
418
- n.hash = null;
419
- }
420
- }
421
- }
422
-
423
- // Build final token stream
424
- ast.tokens = buildTokenStream(
425
- tokenize(tokenSource || templateContent, codeLines, offset),
426
- comments,
427
- textNodes
428
- );
429
- ast.contents = templateContent;
430
-
431
- return { ast, comments };
432
- }
433
-
434
- /**
435
- * Preprocesses the template info, parsing the template content to Glimmer AST,
436
- * fixing the offsets and locations of all nodes
437
- * also calculates the block params locations & ranges
438
- * and adding it to the info
439
- * @param info
440
- * @param code
441
- * @return {{templateVisitorKeys: {}, comments: *[], templateInfos: {templateRange: *, range: *, replacedRange: *}[]}}
442
- */
443
- module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates(info, code) {
444
- const templateInfos = info.templateInfos.map((r) => ({
445
- range: [r.contentRange.startByte, r.contentRange.endByte],
446
- templateRange: [r.range.startByte, r.range.endByte],
447
- utf16Range: [byteToCharIndex(code, r.range.startByte), byteToCharIndex(code, r.range.endByte)],
448
- }));
449
- const codeLines = new DocumentLines(code);
450
- const allComments = [];
451
- const templateVisitorKeys = {};
452
-
453
- for (const tpl of templateInfos) {
454
- const range = tpl.utf16Range;
455
- const template = code.slice(...range);
456
-
457
- const { ast, comments } = processGlimmerTemplate({
458
- templateContent: template,
459
- codeLines,
460
- templateRange: [...tpl.utf16Range],
461
- tokenSource: sliceByteRange(code, ...tpl.templateRange),
462
- });
463
-
464
- ast.content = template;
465
- allComments.push(...comments);
466
- tpl.ast = ast;
467
- }
468
-
469
- for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
470
- templateVisitorKeys[`Glimmer${k}`] = [...v];
471
- }
472
-
473
- return {
474
- templateVisitorKeys,
475
- templateInfos,
476
- comments: allComments,
477
- };
478
- };
479
-
480
- /**
481
- * traverses the AST and replaces the transformed template parts with the Glimmer
482
- * AST.
483
- * This also creates the scopes for the Glimmer Blocks and registers the block params
484
- * in the scope, and also any usages of variables in path expressions
485
- * this allows the basic eslint rules no-undef and no-unsused to work also for the
486
- * templates without needing any custom rules
487
- * @param result
488
- * @param preprocessedResult
489
- * @param visitorKeys
490
- */
491
- module.exports.convertAst = function convertAst(result, preprocessedResult, visitorKeys) {
492
- const templateInfos = preprocessedResult.templateInfos;
493
- let counter = 0;
494
- result.ast.comments.push(...preprocessedResult.comments);
495
-
496
- for (const ti of templateInfos) {
497
- const firstIdx = result.ast.tokens.findIndex((t) => t.range[0] === ti.utf16Range[0]);
498
- const lastIdx = result.ast.tokens.findIndex((t) => t.range[1] === ti.utf16Range[1]);
499
- result.ast.tokens.splice(firstIdx, lastIdx - firstIdx + 1, ...ti.ast.tokens);
500
- }
501
-
226
+ export function registerGlimmerScopes(result) {
502
227
  // eslint-disable-next-line complexity
503
- traverse(visitorKeys, result.ast, (path) => {
228
+ traverse(result.visitorKeys, result.ast, (path) => {
504
229
  const node = path.node;
505
- if (!node) return null;
506
-
507
- if (
508
- node.type === 'ExpressionStatement' ||
509
- node.type === 'StaticBlock' ||
510
- node.type === 'TemplateLiteral' ||
511
- node.type === 'ExportDefaultDeclaration'
512
- ) {
513
- let range = node.range;
514
- if (node.type === 'ExportDefaultDeclaration') {
515
- range = [node.declaration.range[0], node.declaration.range[1]];
516
- }
517
-
518
- const template = templateInfos.find(
519
- (t) =>
520
- t.utf16Range[0] === range[0] &&
521
- (t.utf16Range[1] === range[1] || t.utf16Range[1] === range[1] + 1)
522
- );
523
- if (!template) {
524
- return null;
525
- }
526
- counter++;
527
- const ast = template.ast;
528
- Object.assign(node, ast);
529
- }
530
-
531
- if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') {
532
- const name = node.head.name;
533
- if (glimmer.isKeyword(name)) {
534
- return null;
535
- }
536
- const { scope, variable } = findVarInParentScopes(result.scopeManager, path, name) || {};
537
- if (scope) {
538
- node.head.parent = node;
539
- registerNodeInScope(node.head, scope, variable);
540
- }
230
+ if (!node) return;
231
+ if (node.type === 'GlimmerPathExpression') {
232
+ registerPathExpression(node, path, result.scopeManager);
541
233
  }
542
234
  if (node.type === 'GlimmerElementNode') {
543
- // always reference first part of tag name, this also has the advantage
544
- // that errors regarding this tag will only mark the tag name instead of
545
- // the whole tag + children
546
- const n = node.parts[0];
547
- const { scope, variable } = findVarInParentScopes(result.scopeManager, path, n.name) || {};
548
- /*
549
- register a node in scope if we found a variable
550
- we ignore named-blocks and args as we know that it doesn't reference anything in current scope
551
- we also ignore `this`
552
- if we do not find a variable we register it with a missing variable if
553
- * it starts with upper case, it should be a component with a reference
554
- * it includes a dot, it's a path which should have a reference
555
- * it's NOT a standard html, svg or mathml tag, it should have a referenced variable
556
- */
557
- const ignore =
558
- // Local instance access
559
- n.name === 'this' ||
560
- // named block
561
- n.name.startsWith(':') ||
562
- // argument
563
- n.name.startsWith('@') ||
564
- // defined locally
565
- !scope ||
566
- // custom-elements are allowed to be used even if they don't exist
567
- // and are undefined
568
- n.name.includes('-');
569
-
570
- const registerUndef =
571
- isUpperCase(n.name[0]) ||
572
- node.name.includes('.') ||
573
- (!htmlTags.includes(node.name) &&
574
- !svgTags.includes(node.name) &&
575
- !mathMLTags.includes(node.name));
576
-
577
- if (!ignore && (variable || registerUndef)) {
578
- registerNodeInScope(n, scope, variable);
579
- }
235
+ registerElementNode(node, path, result.scopeManager);
580
236
  }
581
-
582
- if ('blockParams' in node) {
583
- const upperScope = findParentScope(result.scopeManager, path);
584
- const scope = result.isTypescript
585
- ? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
586
- : new Scope(result.scopeManager, 'block', upperScope, node);
587
- const declaredVariables =
588
- result.scopeManager.declaredVariables || result.scopeManager.__declaredVariables;
589
- const vars = [];
590
- declaredVariables.set(node, vars);
591
- const virtualJSParentNode = {
592
- type: 'FunctionDeclaration',
593
- params: node.params,
594
- range: node.range,
595
- loc: node.loc,
596
- parent: path.parent,
597
- };
598
- for (const [i, b] of node.params.entries()) {
599
- const v = new Variable(b.name, scope);
600
- v.identifiers.push(b);
601
- scope.variables.push(v);
602
- scope.set.set(b.name, v);
603
- vars.push(v);
604
-
605
- const virtualJSNode = {
606
- type: 'Identifier',
607
- name: b.name,
608
- range: b.range,
609
- loc: b.loc,
610
- parent: virtualJSParentNode,
611
- };
612
- v.defs.push(new Definition('Parameter', virtualJSNode, node, node, i, 'Block Param'));
613
- v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
614
- }
237
+ if ('blockParams' in node && node.type?.startsWith('Glimmer')) {
238
+ registerBlockParams(node, path, result.scopeManager, result.isTypescript);
615
239
  }
616
- return null;
617
240
  });
241
+ }
618
242
 
619
- if (counter !== templateInfos.length) {
620
- throw new Error('failed to process all templates');
621
- }
622
- };
243
+ // ── transformForLint (used by ts-patch.js) ────────────────────────────
623
244
 
624
- const replaceRange = function replaceRange(s, start, end, substitute) {
625
- return sliceByteRange(s, 0, start) + substitute + sliceByteRange(s, end);
245
+ export const replaceRange = function replaceRange(s, start, end, substitute) {
246
+ return s.slice(0, start) + substitute + s.slice(end);
626
247
  };
627
- module.exports.replaceRange = replaceRange;
628
248
 
629
- const processor = new ContentTag.Preprocessor();
249
+ const processor = new Preprocessor();
630
250
 
631
251
  class EmberParserError extends Error {
632
252
  constructor(message, fileName, location) {
@@ -640,17 +260,14 @@ class EmberParserError extends Error {
640
260
  });
641
261
  }
642
262
 
643
- // For old version of ESLint https://github.com/typescript-eslint/typescript-eslint/pull/6556#discussion_r1123237311
644
263
  get index() {
645
264
  return this.location.start.offset;
646
265
  }
647
266
 
648
- // https://github.com/eslint/eslint/blob/b09a512107249a4eb19ef5a37b0bd672266eafdb/lib/linter/linter.js#L853
649
267
  get lineNumber() {
650
268
  return this.location.start.line;
651
269
  }
652
270
 
653
- // https://github.com/eslint/eslint/blob/b09a512107249a4eb19ef5a37b0bd672266eafdb/lib/linter/linter.js#L854
654
271
  get column() {
655
272
  return this.location.start.column;
656
273
  }
@@ -660,94 +277,44 @@ function createError(code, message, fileName, start, end = start) {
660
277
  return new EmberParserError(message, fileName, { end, start });
661
278
  }
662
279
 
663
- module.exports.transformForLint = function transformForLint(code, fileName) {
664
- let jsCode = code;
665
- /**
666
- *
667
- * @type {{
668
- * type: 'expression' | 'class-member';
669
- * tagName: 'template';
670
- * contents: string;
671
- * range: {
672
- * startByte: number;
673
- * endByte: number;
674
- * startChar: number;
675
- * endChar: number;
676
- * startUtf16Codepoint: number;
677
- * endUtf16Codepoint: number;
678
- * };
679
- * contentRange: {
680
- * startByte: number;
681
- * endByte: number;
682
- * startChar: number;
683
- * endChar: number;
684
- * startUtf16Codepoint: number;
685
- * endUtf16Codepoint: number;
686
- * };
687
- * startRange: {
688
- * startByte: number;
689
- * endByte: number;
690
- * startChar: number;
691
- * endChar: number;
692
- * startUtf16Codepoint: number;
693
- * endUtf16Codepoint: number;
694
- * };
695
- * endRange: {
696
- * startByte: number;
697
- * endByte: number;
698
- * startChar: number;
699
- * endChar: number;
700
- * startUtf16Codepoint: number;
701
- * endUtf16Codepoint: number;
702
- * };
703
- * }[]}
704
- */
705
- let result = null;
280
+ /**
281
+ * Transform code for TypeScript virtual file system.
282
+ * Replaces <template> regions with backtick/static-block placeholders.
283
+ * Used by ts-patch.js for type-aware linting.
284
+ */
285
+ export function transformForLint(code, fileName) {
286
+ let result;
706
287
  try {
707
288
  result = processor.parse(code);
708
289
  } catch (e) {
709
- // Parse Error at <anon>:1:19: 1:19
710
290
  if (e.message.includes('Parse Error at')) {
711
291
  const [line, column] = e.message
712
292
  .split(':')
713
293
  .slice(-2)
714
294
  .map((x) => parseInt(x));
715
- // e.source_code has actually usable info, e.g × Expected ',', got 'string literal (, '')'
716
- // ╭─[9:1]
717
- // 9 │
718
- // 10 │ console.log(test'');
719
- // · ──
720
- // ╰────
721
295
  throw createError(code, e.source_code, fileName, { line, column });
722
296
  }
723
297
  throw e;
724
298
  }
725
- for (const tplInfo of result.reverse()) {
299
+
300
+ // Build placeholder JS inline (same format as ember-estree's toPlaceholderJS)
301
+ let jsCode = code;
302
+ for (const tplInfo of [...result].reverse()) {
726
303
  const content = tplInfo.contents.replace(/`/g, '\\`').replace(/\$/g, '\\$');
304
+ const start = tplInfo.range.startUtf16Codepoint;
305
+ const end = tplInfo.range.endUtf16Codepoint;
306
+ const tplLength = end - start;
307
+ let replacement;
727
308
  if (tplInfo.type === 'class-member') {
728
- const tplLength = tplInfo.range.endByte - tplInfo.range.startByte;
729
- const spaces = tplLength - byteLength(content) - 'static{`'.length - '`}'.length;
730
- const total = content + ' '.repeat(spaces);
731
- const replacementCode = `static{\`${total}\`}`;
732
- jsCode = replaceRange(
733
- jsCode,
734
- tplInfo.range.startByte,
735
- tplInfo.range.endByte,
736
- replacementCode
737
- );
309
+ const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
310
+ replacement = `static{\`${content}${' '.repeat(Math.max(0, spaces))}\`}`;
738
311
  } else {
739
- const tplLength = tplInfo.range.endByte - tplInfo.range.startByte;
740
- const spaces = tplLength - byteLength(content) - '`'.length - '`'.length;
741
- const total = content + ' '.repeat(spaces);
742
- const replacementCode = `\`${total}\``;
743
- jsCode = replaceRange(
744
- jsCode,
745
- tplInfo.range.startByte,
746
- tplInfo.range.endByte,
747
- replacementCode
748
- );
312
+ const spaces = tplLength - content.length - 2; // "`" + "`" = 2
313
+ replacement = `\`${content}${' '.repeat(Math.max(0, spaces))}\``;
749
314
  }
315
+ jsCode = replaceRange(jsCode, start, end, replacement);
750
316
  }
317
+
751
318
  /* istanbul ignore next */
752
319
  if (jsCode.length !== code.length) {
753
320
  throw new Error('bad transform');
@@ -756,9 +323,6 @@ module.exports.transformForLint = function transformForLint(code, fileName) {
756
323
  templateInfos: result,
757
324
  output: jsCode,
758
325
  };
759
- };
326
+ }
760
327
 
761
- module.exports.traverse = traverse;
762
- module.exports.tokenize = tokenize;
763
- module.exports.processGlimmerTemplate = processGlimmerTemplate;
764
- module.exports.buildGlimmerVisitorKeys = buildGlimmerVisitorKeys;
328
+ export { traverse, glimmerVisitorKeys };