ember-eslint-parser 0.8.0 → 0.10.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,107 @@ 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
+ if (blockParamNodes.length === 0) return;
86
+ const upperScope = findParentScope(scopeManager, path);
87
+ const scope = isTypescript
88
+ ? new TypescriptScope.BlockScope(scopeManager, upperScope, node)
89
+ : new Scope(scopeManager, 'block', upperScope, node);
90
+ const declaredVariables = scopeManager.declaredVariables || scopeManager.__declaredVariables;
91
+ const vars = [];
92
+ declaredVariables.set(node, vars);
93
+ const virtualJSParentNode = {
94
+ type: 'FunctionDeclaration',
95
+ params: blockParamNodes,
96
+ range: node.range,
97
+ loc: node.loc,
98
+ parent: path.parent,
99
+ };
100
+ for (const [i, b] of blockParamNodes.entries()) {
101
+ const v = new Variable(b.name, scope);
102
+ v.identifiers.push(b);
103
+ scope.variables.push(v);
104
+ scope.set.set(b.name, v);
105
+ vars.push(v);
106
+
107
+ const virtualJSNode = {
108
+ type: 'Identifier',
109
+ name: b.name,
110
+ range: b.range,
111
+ loc: b.loc,
112
+ parent: virtualJSParentNode,
113
+ };
114
+ v.defs.push(new Definition('Parameter', virtualJSNode, node, node, i, 'Block Param'));
115
+ v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
116
+ }
117
+ }
118
+
119
+ function registerPathExpression(node, path, scopeManager) {
120
+ if (node.head.type !== 'VarHead') return;
121
+ const name = node.head.name;
122
+ if (glimmerIsKeyword(name)) return;
123
+ const { scope, variable } = findVarInParentScopes(scopeManager, path, name) || {};
124
+ if (scope) {
125
+ node.head.parent = node;
126
+ registerNodeInScope(node.head, scope, variable);
108
127
  }
109
- if (!keys.GlimmerElementNode.includes('blockParamNodes')) {
110
- keys.GlimmerElementNode.push('blockParamNodes', 'parts');
128
+ }
129
+
130
+ function registerElementNode(node, path, scopeManager) {
131
+ const n = node.parts[0];
132
+ const { scope, variable } = findVarInParentScopes(scopeManager, path, n.name) || {};
133
+ const ignore =
134
+ n.name === 'this' ||
135
+ n.name.startsWith(':') ||
136
+ n.name.startsWith('@') ||
137
+ !scope ||
138
+ n.name.includes('-');
139
+
140
+ const registerUndef =
141
+ isUpperCase(n.name[0]) ||
142
+ node.name.includes('.') ||
143
+ (!htmlTagsSet.has(node.name) && !svgTagsSet.has(node.name) && !mathMLTagsSet.has(node.name));
144
+
145
+ if (!ignore && (variable || registerUndef)) {
146
+ registerNodeInScope(n, scope, variable);
111
147
  }
112
- keys.GlimmerProgram = ['body', 'blockParamNodes'];
113
- keys.GlimmerTemplate = ['body'];
114
- _cachedGlimmerVisitorKeys = keys;
115
- return keys;
116
148
  }
117
149
 
150
+ // ── Visitor builders ──────────────────────────────────────────────────
151
+
118
152
  /**
119
- * traverses all nodes using the {visitorKeys} calling the callback function, visitor
120
- * @param visitorKeys
121
- * @param node
122
- * @param visitor
153
+ * 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)
156
+ * @param {boolean} isTypescript
157
+ * @returns {object} visitors for toTree
123
158
  */
159
+ export function buildGlimmerVisitors(getScopeManager, isTypescript) {
160
+ return {
161
+ GlimmerPathExpression(node, path) {
162
+ const sm = getScopeManager();
163
+ if (sm) registerPathExpression(node, path, sm);
164
+ },
165
+ GlimmerElementNode(node, path) {
166
+ const sm = getScopeManager();
167
+ if (sm) registerElementNode(node, path, sm);
168
+ },
169
+ GlimmerBlockParams(node, path) {
170
+ const sm = getScopeManager();
171
+ if (sm) registerBlockParams(node, path, sm, isTypescript);
172
+ },
173
+ };
174
+ }
175
+
176
+ // ── registerGlimmerScopes (fallback for JS/oxc path) ──────────────────
177
+
124
178
  function traverse(visitorKeys, node, visitor) {
125
- const allVisitorKeys = { ...visitorKeys, ...buildGlimmerVisitorKeys() };
179
+ const allVisitorKeys = { ...visitorKeys, ...glimmerVisitorKeys };
126
180
  const queue = [];
127
181
 
128
182
  queue.push({
@@ -135,19 +189,12 @@ function traverse(visitorKeys, node, visitor) {
135
189
 
136
190
  while (queue.length > 0) {
137
191
  const currentPath = queue.pop();
138
-
139
192
  visitor(currentPath);
140
-
141
193
  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) {
194
+ const keys = allVisitorKeys[currentPath.node.type];
195
+ if (!keys) continue;
196
+ for (const visitorKey of keys) {
149
197
  const child = currentPath.node[visitorKey];
150
-
151
198
  if (!child) {
152
199
  continue;
153
200
  } else if (Array.isArray(child)) {
@@ -173,476 +220,50 @@ function traverse(visitorKeys, node, visitor) {
173
220
  }
174
221
  }
175
222
 
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
223
  /**
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[]}
224
+ * Full AST traversal for scope registration — used as fallback for JS/oxc path.
225
+ * For the TS path, toTree's visitor API handles this during splicing.
204
226
  */
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);
227
+ export function registerGlimmerScopes(result) {
228
+ // eslint-disable-next-line complexity
229
+ traverse(result.visitorKeys, result.ast, (path) => {
230
+ const node = path.node;
231
+ if (!node) return;
232
+ if (node.type === 'GlimmerPathExpression') {
233
+ registerPathExpression(node, path, result.scopeManager);
290
234
  }
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
- }
235
+ if (node.type === 'GlimmerElementNode') {
236
+ registerElementNode(node, path, result.scopeManager);
327
237
  }
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++]);
238
+ if ('blockParams' in node && node.type?.startsWith('Glimmer')) {
239
+ registerBlockParams(node, path, result.scopeManager, result.isTypescript);
344
240
  }
345
- result.push(token);
346
- }
347
- while (ti < sortedTextNodes.length) {
348
- result.push(sortedTextNodes[ti++]);
349
- }
350
-
351
- return result;
352
- }
353
-
354
- /**
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[] }}
364
- */
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
241
  });
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
242
  }
456
243
 
457
244
  /**
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
245
+ * Scope registration for the HBS parser. Unlike the gjs/gts path, we do not
246
+ * register references for free identifiers (`{{path}}`, `<Tag>`) — all
247
+ * template locals are treated as runtime-defined, so no-undef stays quiet.
248
+ * Only block params from `as |x|` constructs are declared.
504
249
  */
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
-
519
- // eslint-disable-next-line complexity
520
- traverse(visitorKeys, result.ast, (path) => {
250
+ export function registerHBSScopes(result) {
251
+ traverse(result.visitorKeys, result.ast, (path) => {
521
252
  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
- }
253
+ if (!node) return;
254
+ if ('blockParams' in node && node.type.startsWith('Glimmer')) {
255
+ registerBlockParams(node, path, result.scopeManager, false);
557
256
  }
558
- 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
- }
596
- }
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
- }
631
- }
632
- return null;
633
257
  });
258
+ }
634
259
 
635
- if (counter !== templateInfos.length) {
636
- throw new Error('failed to process all templates');
637
- }
638
- };
260
+ // ── transformForLint (used by ts-patch.js) ────────────────────────────
639
261
 
640
- const replaceRange = function replaceRange(s, start, end, substitute) {
262
+ export const replaceRange = function replaceRange(s, start, end, substitute) {
641
263
  return s.slice(0, start) + substitute + s.slice(end);
642
264
  };
643
- module.exports.replaceRange = replaceRange;
644
265
 
645
- const processor = new ContentTag.Preprocessor();
266
+ const processor = new Preprocessor();
646
267
 
647
268
  class EmberParserError extends Error {
648
269
  constructor(message, fileName, location) {
@@ -656,17 +277,14 @@ class EmberParserError extends Error {
656
277
  });
657
278
  }
658
279
 
659
- // For old version of ESLint https://github.com/typescript-eslint/typescript-eslint/pull/6556#discussion_r1123237311
660
280
  get index() {
661
281
  return this.location.start.offset;
662
282
  }
663
283
 
664
- // https://github.com/eslint/eslint/blob/b09a512107249a4eb19ef5a37b0bd672266eafdb/lib/linter/linter.js#L853
665
284
  get lineNumber() {
666
285
  return this.location.start.line;
667
286
  }
668
287
 
669
- // https://github.com/eslint/eslint/blob/b09a512107249a4eb19ef5a37b0bd672266eafdb/lib/linter/linter.js#L854
670
288
  get column() {
671
289
  return this.location.start.column;
672
290
  }
@@ -676,94 +294,44 @@ function createError(code, message, fileName, start, end = start) {
676
294
  return new EmberParserError(message, fileName, { end, start });
677
295
  }
678
296
 
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;
297
+ /**
298
+ * Transform code for TypeScript virtual file system.
299
+ * Replaces <template> regions with backtick/static-block placeholders.
300
+ * Used by ts-patch.js for type-aware linting.
301
+ */
302
+ export function transformForLint(code, fileName) {
303
+ let result;
722
304
  try {
723
305
  result = processor.parse(code);
724
306
  } catch (e) {
725
- // Parse Error at <anon>:1:19: 1:19
726
307
  if (e.message.includes('Parse Error at')) {
727
308
  const [line, column] = e.message
728
309
  .split(':')
729
310
  .slice(-2)
730
311
  .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
312
  throw createError(code, e.source_code, fileName, { line, column });
738
313
  }
739
314
  throw e;
740
315
  }
741
- for (const tplInfo of result.reverse()) {
316
+
317
+ // Build placeholder JS inline (same format as ember-estree's toPlaceholderJS)
318
+ let jsCode = code;
319
+ for (const tplInfo of [...result].reverse()) {
742
320
  const content = tplInfo.contents.replace(/`/g, '\\`').replace(/\$/g, '\\$');
321
+ const start = tplInfo.range.startUtf16Codepoint;
322
+ const end = tplInfo.range.endUtf16Codepoint;
323
+ const tplLength = end - start;
324
+ let replacement;
743
325
  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
- );
326
+ const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
327
+ replacement = `static{\`${content}${' '.repeat(Math.max(0, spaces))}\`}`;
754
328
  } 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
- );
329
+ const spaces = tplLength - content.length - 2; // "`" + "`" = 2
330
+ replacement = `\`${content}${' '.repeat(Math.max(0, spaces))}\``;
765
331
  }
332
+ jsCode = replaceRange(jsCode, start, end, replacement);
766
333
  }
334
+
767
335
  /* istanbul ignore next */
768
336
  if (jsCode.length !== code.length) {
769
337
  throw new Error('bad transform');
@@ -772,9 +340,6 @@ module.exports.transformForLint = function transformForLint(code, fileName) {
772
340
  templateInfos: result,
773
341
  output: jsCode,
774
342
  };
775
- };
343
+ }
776
344
 
777
- module.exports.traverse = traverse;
778
- module.exports.tokenize = tokenize;
779
- module.exports.processGlimmerTemplate = processGlimmerTemplate;
780
- module.exports.buildGlimmerVisitorKeys = buildGlimmerVisitorKeys;
345
+ export { traverse, glimmerVisitorKeys };