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