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.
- package/package.json +11 -9
- package/src/parser/gjs-gts-parser.js +116 -87
- package/src/parser/hbs-parser.js +66 -67
- package/src/parser/transforms.js +149 -601
- 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,106 @@ 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
|
+
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
|
-
|
|
110
|
-
|
|
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
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* @param
|
|
122
|
-
* @param
|
|
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, ...
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
*
|
|
356
|
-
*
|
|
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
|
|
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
|
|
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
|
-
}
|
|
230
|
+
if (!node) return;
|
|
231
|
+
if (node.type === 'GlimmerPathExpression') {
|
|
232
|
+
registerPathExpression(node, path, result.scopeManager);
|
|
557
233
|
}
|
|
558
234
|
if (node.type === 'GlimmerElementNode') {
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
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
|
-
);
|
|
309
|
+
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
|
|
310
|
+
replacement = `static{\`${content}${' '.repeat(Math.max(0, spaces))}\`}`;
|
|
754
311
|
} 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
|
-
);
|
|
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
|
-
|
|
778
|
-
module.exports.tokenize = tokenize;
|
|
779
|
-
module.exports.processGlimmerTemplate = processGlimmerTemplate;
|
|
780
|
-
module.exports.buildGlimmerVisitorKeys = buildGlimmerVisitorKeys;
|
|
328
|
+
export { traverse, glimmerVisitorKeys };
|