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.
- package/package.json +13 -11
- package/src/parser/gjs-gts-parser.js +116 -87
- package/src/parser/hbs-parser.js +69 -68
- package/src/parser/transforms.js +162 -597
- package/src/parser/ts-patch.js +6 -9
- package/src/preprocessor/noop.js +23 -20
- package/src/utils/document.js +0 -102
package/src/parser/transforms.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* @param
|
|
122
|
-
* @param
|
|
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, ...
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
*
|
|
200
|
-
*
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
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
|
-
|
|
506
|
-
|
|
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
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
|
745
|
-
|
|
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
|
|
756
|
-
|
|
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
|
-
|
|
778
|
-
module.exports.tokenize = tokenize;
|
|
779
|
-
module.exports.processGlimmerTemplate = processGlimmerTemplate;
|
|
780
|
-
module.exports.buildGlimmerVisitorKeys = buildGlimmerVisitorKeys;
|
|
345
|
+
export { traverse, glimmerVisitorKeys };
|