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