ember-eslint-parser 0.5.13 → 0.6.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/README.md +21 -1
- package/package.json +3 -2
- package/src/parser/hbs-parser.js +89 -0
- package/src/parser/transforms.js +218 -172
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ember-eslint-parser
|
|
2
2
|
|
|
3
|
-
This is the eslint parser for ember's gjs and gts files (using `<template>`).
|
|
3
|
+
This is the eslint parser for ember's gjs and gts files (using `<template>`), and also for Handlebars (`.hbs`) template files.
|
|
4
4
|
|
|
5
5
|
It is meant to be used with [eslint-plugin-ember](https://github.com/ember-cli/eslint-plugin-ember), which provides nice defaults for all the different file types in ember projects.
|
|
6
6
|
|
|
@@ -31,6 +31,26 @@ It's recommended to only use _overrides_ when defining your eslint config, so us
|
|
|
31
31
|
if we detect a typescript parser, it will also be used for all files, otherwise babel parser will be used.
|
|
32
32
|
If we cannot find a typescript parser when linting gts we throw an error.
|
|
33
33
|
|
|
34
|
+
## HBS (Handlebars) support
|
|
35
|
+
|
|
36
|
+
For `.hbs` template files, use the `ember-eslint-parser/hbs` parser. In ESLint's flat config format (ESLint 9+):
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
// eslint.config.mjs
|
|
40
|
+
import hbsParser from 'ember-eslint-parser/hbs';
|
|
41
|
+
|
|
42
|
+
export default [
|
|
43
|
+
{
|
|
44
|
+
files: ['**/*.hbs'],
|
|
45
|
+
languageOptions: {
|
|
46
|
+
parser: hbsParser,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **Note:** In `.hbs` files, all locals not defined in the template are assumed to be defined at runtime. This avoids false-positive `no-undef` errors for template identifiers. This is a known limitation of the classic HBS format — use `.gjs`/`.gts` for full static analysis.
|
|
53
|
+
|
|
34
54
|
## Support
|
|
35
55
|
|
|
36
56
|
eslint-plugin-ember is the primary consumer of this parser library, so SemVer _may_ not be respected for other consumers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-eslint-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"author": "",
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./src/parser/gjs-gts-parser.js",
|
|
14
|
+
"./hbs": "./src/parser/hbs-parser.js",
|
|
14
15
|
"./noop": "./src/preprocessor/noop.js"
|
|
15
16
|
},
|
|
16
17
|
"main": "src/parser/gjs-gts-parser.js",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"fs-extra": "^11.2.0",
|
|
45
46
|
"prettier": "^3.2.5",
|
|
46
47
|
"publint": "^0.2.6",
|
|
47
|
-
"release-plan": "^0.
|
|
48
|
+
"release-plan": "^0.17.4",
|
|
48
49
|
"typescript": "^5.3.3",
|
|
49
50
|
"vite": "^5.0.12",
|
|
50
51
|
"vitest": "^1.2.2"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const eslintScope = require('eslint-scope');
|
|
2
|
+
const DocumentLines = require('../utils/document');
|
|
3
|
+
const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transforms');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* implements https://eslint.org/docs/latest/extend/custom-parsers
|
|
7
|
+
* for Handlebars (.hbs) template files.
|
|
8
|
+
*
|
|
9
|
+
* The entire file is treated as a Glimmer template.
|
|
10
|
+
* All locals not defined in the template are assumed to be defined
|
|
11
|
+
* (no no-undef errors for template identifiers).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @type {import('eslint').ParserModule}
|
|
16
|
+
*/
|
|
17
|
+
module.exports = {
|
|
18
|
+
meta: {
|
|
19
|
+
name: 'ember-eslint-parser/hbs',
|
|
20
|
+
version: '*',
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
parseForESLint(code, options) {
|
|
24
|
+
const filePath = (options && options.filePath) || '<hbs>';
|
|
25
|
+
const codeLines = new DocumentLines(code);
|
|
26
|
+
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
result = processGlimmerTemplate({
|
|
30
|
+
templateContent: code,
|
|
31
|
+
codeLines,
|
|
32
|
+
templateRange: [0, code.length],
|
|
33
|
+
});
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Transform glimmer parse error to ESLint-compatible error
|
|
36
|
+
const loc = e.location || (e.hash && e.hash.loc);
|
|
37
|
+
if (loc && loc.start) {
|
|
38
|
+
const err = Object.assign(new SyntaxError(e.message), {
|
|
39
|
+
lineNumber: loc.start.line,
|
|
40
|
+
column: loc.start.column,
|
|
41
|
+
index: codeLines.positionToOffset(loc.start),
|
|
42
|
+
fileName: filePath,
|
|
43
|
+
});
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { ast: templateNode, comments } = result;
|
|
50
|
+
|
|
51
|
+
// Wrap in a synthetic Program node (required by ESLint)
|
|
52
|
+
const program = {
|
|
53
|
+
type: 'Program',
|
|
54
|
+
body: [templateNode],
|
|
55
|
+
tokens: templateNode.tokens,
|
|
56
|
+
comments,
|
|
57
|
+
range: [0, code.length],
|
|
58
|
+
start: 0,
|
|
59
|
+
end: code.length,
|
|
60
|
+
loc: {
|
|
61
|
+
start: { line: 1, column: 0 },
|
|
62
|
+
end: codeLines.offsetToPosition(code.length),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Build visitor keys: Program + all Glimmer node types
|
|
67
|
+
const visitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() };
|
|
68
|
+
|
|
69
|
+
// Create an empty scope manager.
|
|
70
|
+
// For HBS, all locals are assumed to be defined at runtime,
|
|
71
|
+
// so we don't track variable references (no no-undef errors).
|
|
72
|
+
const scopeManager = eslintScope.analyze(
|
|
73
|
+
{
|
|
74
|
+
type: 'Program',
|
|
75
|
+
body: [],
|
|
76
|
+
range: [0, code.length],
|
|
77
|
+
loc: program.loc,
|
|
78
|
+
},
|
|
79
|
+
{ range: true }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ast: program,
|
|
84
|
+
scopeManager,
|
|
85
|
+
visitorKeys,
|
|
86
|
+
services: {},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
package/src/parser/transforms.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const ContentTag = require('content-tag');
|
|
2
2
|
const glimmer = require('@glimmer/syntax');
|
|
3
|
+
const { visitorKeys: glimmerVisitorKeys } = glimmer;
|
|
3
4
|
const DocumentLines = require('../utils/document');
|
|
4
|
-
const { visitorKeys: glimmerVisitorKeys } = require('@glimmer/syntax');
|
|
5
5
|
const { Reference, Scope, Variable, Definition } = require('eslint-scope');
|
|
6
6
|
const htmlTags = require('html-tags');
|
|
7
7
|
const svgTags = require('svg-tags');
|
|
@@ -112,6 +112,24 @@ function registerNodeInScope(node, scope, variable) {
|
|
|
112
112
|
scope.references.push(ref);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Builds the complete Glimmer visitor keys map with "Glimmer" prefix and
|
|
117
|
+
* additional keys needed for traversal (blockParamNodes, parts, etc).
|
|
118
|
+
* @return {object}
|
|
119
|
+
*/
|
|
120
|
+
function buildGlimmerVisitorKeys() {
|
|
121
|
+
const keys = {};
|
|
122
|
+
for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
|
|
123
|
+
keys[`Glimmer${k}`] = [...v];
|
|
124
|
+
}
|
|
125
|
+
if (!keys.GlimmerElementNode.includes('blockParamNodes')) {
|
|
126
|
+
keys.GlimmerElementNode.push('blockParamNodes', 'parts');
|
|
127
|
+
}
|
|
128
|
+
keys.GlimmerProgram = ['body', 'blockParamNodes'];
|
|
129
|
+
keys.GlimmerTemplate = ['body'];
|
|
130
|
+
return keys;
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
/**
|
|
116
134
|
* traverses all nodes using the {visitorKeys} calling the callback function, visitor
|
|
117
135
|
* @param visitorKeys
|
|
@@ -119,7 +137,7 @@ function registerNodeInScope(node, scope, variable) {
|
|
|
119
137
|
* @param visitor
|
|
120
138
|
*/
|
|
121
139
|
function traverse(visitorKeys, node, visitor) {
|
|
122
|
-
const allVisitorKeys = visitorKeys;
|
|
140
|
+
const allVisitorKeys = { ...visitorKeys, ...buildGlimmerVisitorKeys() };
|
|
123
141
|
const queue = [];
|
|
124
142
|
|
|
125
143
|
queue.push({
|
|
@@ -137,17 +155,7 @@ function traverse(visitorKeys, node, visitor) {
|
|
|
137
155
|
|
|
138
156
|
if (!currentPath.node) continue;
|
|
139
157
|
|
|
140
|
-
const visitorKeys =
|
|
141
|
-
if (currentPath.node.type === 'GlimmerElementNode') {
|
|
142
|
-
if (!visitorKeys.includes('blockParamNodes')) {
|
|
143
|
-
visitorKeys.push('blockParamNodes', 'parts');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (currentPath.node.type === 'GlimmerProgram') {
|
|
147
|
-
if (!visitorKeys.includes('blockParamNodes')) {
|
|
148
|
-
visitorKeys.push('blockParamNodes');
|
|
149
|
-
}
|
|
150
|
-
}
|
|
158
|
+
const visitorKeys = allVisitorKeys[currentPath.node.type];
|
|
151
159
|
if (!visitorKeys) {
|
|
152
160
|
continue;
|
|
153
161
|
}
|
|
@@ -242,6 +250,187 @@ function tokenize(template, doc, startOffset) {
|
|
|
242
250
|
return tokens;
|
|
243
251
|
}
|
|
244
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
|
+
/**
|
|
301
|
+
* Builds the final token stream by filtering out tokens covered by comments
|
|
302
|
+
* or text nodes, then merging text nodes back in sorted order.
|
|
303
|
+
* @param {object[]} rawTokens
|
|
304
|
+
* @param {object[]} comments
|
|
305
|
+
* @param {object[]} textNodes
|
|
306
|
+
* @return {object[]}
|
|
307
|
+
*/
|
|
308
|
+
function buildTokenStream(rawTokens, comments, textNodes) {
|
|
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
|
+
|
|
245
434
|
/**
|
|
246
435
|
* Preprocesses the template info, parsing the template content to Glimmer AST,
|
|
247
436
|
* fixing the offsets and locations of all nodes
|
|
@@ -257,164 +446,34 @@ module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates(
|
|
|
257
446
|
templateRange: [r.range.start, r.range.end],
|
|
258
447
|
utf16Range: [byteToCharIndex(code, r.range.start), byteToCharIndex(code, r.range.end)],
|
|
259
448
|
}));
|
|
260
|
-
const templateVisitorKeys = {};
|
|
261
449
|
const codeLines = new DocumentLines(code);
|
|
262
|
-
const
|
|
450
|
+
const allComments = [];
|
|
451
|
+
const templateVisitorKeys = {};
|
|
452
|
+
|
|
263
453
|
for (const tpl of templateInfos) {
|
|
264
|
-
const currentComments = [];
|
|
265
|
-
const textNodes = [];
|
|
266
|
-
const emptyTextNodes = [];
|
|
267
454
|
const range = tpl.utf16Range;
|
|
268
|
-
const offset = range[0];
|
|
269
455
|
const template = code.slice(...range);
|
|
270
|
-
const docLines = new DocumentLines(template);
|
|
271
|
-
const ast = glimmer.preprocess(template, { mode: 'codemod' });
|
|
272
|
-
ast.tokens = tokenize(sliceByteRange(code, ...tpl.templateRange), codeLines, tpl.utf16Range[0]);
|
|
273
|
-
const allNodes = [];
|
|
274
|
-
glimmer.traverse(ast, {
|
|
275
|
-
All(node, path) {
|
|
276
|
-
const n = node;
|
|
277
|
-
n.parent = path.parentNode;
|
|
278
|
-
allNodes.push(node);
|
|
279
|
-
if (node.type === 'CommentStatement' || node.type === 'MustacheCommentStatement') {
|
|
280
|
-
comments.push(node);
|
|
281
|
-
currentComments.push(node);
|
|
282
|
-
}
|
|
283
|
-
if (node.type === 'TextNode') {
|
|
284
|
-
n.value = node.chars;
|
|
285
|
-
// empty text nodes are not allowed, it's empty if its only whitespace or line terminators
|
|
286
|
-
// it's okay for AttrNodes
|
|
287
|
-
if (n.value.trim().length !== 0 || n.parent.type === 'AttrNode') {
|
|
288
|
-
textNodes.push(node);
|
|
289
|
-
} else {
|
|
290
|
-
emptyTextNodes.push(node);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
ast.content = template;
|
|
296
|
-
const allNodeTypes = new Set();
|
|
297
|
-
for (const n of allNodes) {
|
|
298
|
-
if (n.type === 'PathExpression') {
|
|
299
|
-
n.head.range = [
|
|
300
|
-
offset + docLines.positionToOffset(n.head.loc.start),
|
|
301
|
-
offset + docLines.positionToOffset(n.head.loc.end),
|
|
302
|
-
];
|
|
303
|
-
n.head.loc = {
|
|
304
|
-
start: codeLines.offsetToPosition(n.head.range[0]),
|
|
305
|
-
end: codeLines.offsetToPosition(n.head.range[1]),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
n.range =
|
|
309
|
-
n.type === 'Template'
|
|
310
|
-
? [...tpl.utf16Range]
|
|
311
|
-
: [
|
|
312
|
-
offset + docLines.positionToOffset(n.loc.start),
|
|
313
|
-
offset + docLines.positionToOffset(n.loc.end),
|
|
314
|
-
];
|
|
315
|
-
n.start = n.range[0];
|
|
316
|
-
n.end = n.range[1];
|
|
317
|
-
n.loc = {
|
|
318
|
-
start: codeLines.offsetToPosition(n.range[0]),
|
|
319
|
-
end: codeLines.offsetToPosition(n.range[1]),
|
|
320
|
-
};
|
|
321
|
-
if (n.type === 'Template') {
|
|
322
|
-
n.loc.start = codeLines.offsetToPosition(tpl.utf16Range[0]);
|
|
323
|
-
n.loc.end = codeLines.offsetToPosition(tpl.utf16Range[1]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (n.type === 'ElementNode') {
|
|
327
|
-
n.name = n.tag;
|
|
328
|
-
n.parts = [n.path.head].map((p) => {
|
|
329
|
-
const range = [
|
|
330
|
-
offset + docLines.positionToOffset(p.loc.start),
|
|
331
|
-
offset + docLines.positionToOffset(p.loc.end),
|
|
332
|
-
];
|
|
333
|
-
const loc = {
|
|
334
|
-
start: codeLines.offsetToPosition(range[0]),
|
|
335
|
-
end: codeLines.offsetToPosition(range[1]),
|
|
336
|
-
};
|
|
337
|
-
return {
|
|
338
|
-
...p,
|
|
339
|
-
name: p.original,
|
|
340
|
-
parent: n,
|
|
341
|
-
type: 'GlimmerElementNodePart',
|
|
342
|
-
range,
|
|
343
|
-
loc,
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
456
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const loc = {
|
|
355
|
-
start: codeLines.offsetToPosition(range[0]),
|
|
356
|
-
end: codeLines.offsetToPosition(range[1]),
|
|
357
|
-
};
|
|
358
|
-
return {
|
|
359
|
-
...p,
|
|
360
|
-
type: 'BlockParam',
|
|
361
|
-
name: p.original,
|
|
362
|
-
parent: n,
|
|
363
|
-
range,
|
|
364
|
-
loc,
|
|
365
|
-
};
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
n.type = `Glimmer${n.type}`;
|
|
369
|
-
allNodeTypes.add(n.type);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// ast should not contains empty text nodes
|
|
373
|
-
for (const node of emptyTextNodes) {
|
|
374
|
-
const children = node.parent.children || node.parent.body || node.parent.parts;
|
|
375
|
-
const idx = children.indexOf(node);
|
|
376
|
-
children.splice(idx, 1);
|
|
377
|
-
}
|
|
457
|
+
const { ast, comments } = processGlimmerTemplate({
|
|
458
|
+
templateContent: template,
|
|
459
|
+
codeLines,
|
|
460
|
+
templateRange: [...tpl.utf16Range],
|
|
461
|
+
tokenSource: sliceByteRange(code, ...tpl.templateRange),
|
|
462
|
+
});
|
|
378
463
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const parentBody = comment.parent.body || comment.parent.children;
|
|
382
|
-
const idx = parentBody.indexOf(comment);
|
|
383
|
-
if (idx >= 0) {
|
|
384
|
-
parentBody.splice(idx, 1);
|
|
385
|
-
}
|
|
386
|
-
// comment type can be a block comment or a line comment
|
|
387
|
-
// mark comments as always block comment, this works for eslint in all cases
|
|
388
|
-
comment.type = 'Block';
|
|
389
|
-
}
|
|
390
|
-
// tokens should not contain tokens of comments
|
|
391
|
-
ast.tokens = ast.tokens.filter(
|
|
392
|
-
(t) => !comments.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
|
|
393
|
-
);
|
|
394
|
-
// tokens should not contain tokens of text nodes, but represent the whole node
|
|
395
|
-
// remove existing tokens
|
|
396
|
-
ast.tokens = ast.tokens.filter(
|
|
397
|
-
(t) => !textNodes.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
|
|
398
|
-
);
|
|
399
|
-
// merge in text nodes
|
|
400
|
-
let currentTextNode = textNodes.pop();
|
|
401
|
-
for (let i = ast.tokens.length - 1; i >= 0; i--) {
|
|
402
|
-
const t = ast.tokens[i];
|
|
403
|
-
while (currentTextNode && t.range[0] < currentTextNode.range[0]) {
|
|
404
|
-
ast.tokens.splice(i + 1, 0, currentTextNode);
|
|
405
|
-
currentTextNode = textNodes.pop();
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
ast.contents = template;
|
|
464
|
+
ast.content = template;
|
|
465
|
+
allComments.push(...comments);
|
|
409
466
|
tpl.ast = ast;
|
|
410
467
|
}
|
|
468
|
+
|
|
411
469
|
for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
|
|
412
470
|
templateVisitorKeys[`Glimmer${k}`] = [...v];
|
|
413
471
|
}
|
|
472
|
+
|
|
414
473
|
return {
|
|
415
474
|
templateVisitorKeys,
|
|
416
475
|
templateInfos,
|
|
417
|
-
comments,
|
|
476
|
+
comments: allComments,
|
|
418
477
|
};
|
|
419
478
|
};
|
|
420
479
|
|
|
@@ -557,21 +616,6 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi
|
|
|
557
616
|
return null;
|
|
558
617
|
});
|
|
559
618
|
|
|
560
|
-
traverse(visitorKeys, result.ast, (path) => {
|
|
561
|
-
const node = path.node;
|
|
562
|
-
if (!node) return null;
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
['GlimmerMustacheStatement', 'GlimmerBlockStatement', 'GlimmerSubExpression'].includes(
|
|
566
|
-
node.type
|
|
567
|
-
)
|
|
568
|
-
) {
|
|
569
|
-
if (node.hash && node.hash.pairs.length === 0) {
|
|
570
|
-
node.hash = null;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
|
|
575
619
|
if (counter !== templateInfos.length) {
|
|
576
620
|
throw new Error('failed to process all templates');
|
|
577
621
|
}
|
|
@@ -690,3 +734,5 @@ module.exports.transformForLint = function transformForLint(code, fileName) {
|
|
|
690
734
|
|
|
691
735
|
module.exports.traverse = traverse;
|
|
692
736
|
module.exports.tokenize = tokenize;
|
|
737
|
+
module.exports.processGlimmerTemplate = processGlimmerTemplate;
|
|
738
|
+
module.exports.buildGlimmerVisitorKeys = buildGlimmerVisitorKeys;
|