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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-eslint-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"author": "",
|
|
12
|
+
"type": "module",
|
|
12
13
|
"exports": {
|
|
13
14
|
".": "./src/parser/gjs-gts-parser.js",
|
|
14
15
|
"./hbs": "./src/parser/hbs-parser.js",
|
|
@@ -19,11 +20,11 @@
|
|
|
19
20
|
"src"
|
|
20
21
|
],
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"@
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"eslint-scope": "^9.1.
|
|
23
|
+
"@glimmer/syntax": "^0.95.0",
|
|
24
|
+
"@typescript-eslint/tsconfig-utils": "^8.57.1",
|
|
25
|
+
"content-tag": "^4.1.1",
|
|
26
|
+
"ember-estree": "^0.4.2",
|
|
27
|
+
"eslint-scope": "^9.1.2",
|
|
27
28
|
"html-tags": "^5.1.0",
|
|
28
29
|
"mathml-tag-names": "^4.0.0",
|
|
29
30
|
"svg-tags": "^1.0.0"
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"eslint-plugin-promise": "^6.0.0",
|
|
44
45
|
"execa": "^8.0.1",
|
|
45
46
|
"fs-extra": "^11.2.0",
|
|
47
|
+
"mitata": "1.0.34",
|
|
46
48
|
"prettier": "^3.2.5",
|
|
47
49
|
"publint": "^0.2.6",
|
|
48
50
|
"release-plan": "^0.17.4",
|
|
@@ -51,7 +53,6 @@
|
|
|
51
53
|
"vitest": "^1.2.2"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
54
|
-
"@babel/core": "^7.23.6",
|
|
55
56
|
"@typescript-eslint/parser": "*"
|
|
56
57
|
},
|
|
57
58
|
"peerDependenciesMeta": {
|
|
@@ -67,7 +68,10 @@
|
|
|
67
68
|
"lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
|
|
68
69
|
"lint:js": "eslint . --max-warnings=0",
|
|
69
70
|
"lint:js:fix": "eslint . --fix --max-warnings=0",
|
|
70
|
-
"lint:package": "
|
|
71
|
+
"lint:package": "publint",
|
|
72
|
+
"bench": "node --expose-gc tests/parser.bench.mjs",
|
|
73
|
+
"bench:compare": "node scripts/bench-compare.mjs",
|
|
74
|
+
"bench:summary": "./scripts/local-bench-summary.sh",
|
|
71
75
|
"test": "vitest run"
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const { transformForLint, preprocessGlimmerTemplates, convertAst } = require('./transforms');
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import tsconfigUtils from '@typescript-eslint/tsconfig-utils';
|
|
3
|
+
import { registerParsedFile } from '../preprocessor/noop.js';
|
|
4
|
+
import { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } from './ts-patch.js';
|
|
5
|
+
import { buildGlimmerVisitors } from './transforms.js';
|
|
6
|
+
import { toTree } from 'ember-estree';
|
|
7
|
+
import * as eslintScope from 'eslint-scope';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* implements https://eslint.org/docs/latest/extend/custom-parsers
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* Uses ember-estree's toTree with a custom parser option to handle
|
|
15
|
+
* the full pipeline: template extraction, placeholder replacement,
|
|
16
|
+
* JS/TS parsing, Glimmer AST processing, and AST splicing.
|
|
17
|
+
*
|
|
18
|
+
* Scope registration happens via toTree's visitors API, eliminating
|
|
19
|
+
* a second AST traversal.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -79,12 +81,10 @@ function getAllowJsFromPrograms(programs) {
|
|
|
79
81
|
function getProjectServiceTsconfigPath(projectService) {
|
|
80
82
|
if (!projectService) return null;
|
|
81
83
|
|
|
82
|
-
// If projectService is true, use default behavior (nearest tsconfig.json, allowJs from config)
|
|
83
84
|
if (projectService === true) {
|
|
84
85
|
return 'tsconfig.json';
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
// If projectService is an object, handle ProjectServiceOptions
|
|
88
88
|
if (typeof projectService === 'object') {
|
|
89
89
|
if (typeof projectService.allowDefaultProject !== 'undefined') {
|
|
90
90
|
// eslint-disable-next-line no-console
|
|
@@ -131,86 +131,115 @@ function getAllowJs(options) {
|
|
|
131
131
|
/**
|
|
132
132
|
* @type {import('eslint').ParserModule}
|
|
133
133
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
parseForESLint(code, options) {
|
|
141
|
-
const allowGjsWasSet = options.allowGjs !== undefined;
|
|
142
|
-
const allowGjs = allowGjsWasSet ? options.allowGjs : getAllowJs(options);
|
|
143
|
-
let actualAllowGjs;
|
|
144
|
-
// Only patch TypeScript if we actually need it.
|
|
145
|
-
if (options.programs || options.projectService || options.project) {
|
|
146
|
-
({ allowGjs: actualAllowGjs } = patchTs({ allowGjs }));
|
|
147
|
-
}
|
|
148
|
-
registerParsedFile(options.filePath);
|
|
149
|
-
let jsCode = code;
|
|
150
|
-
const info = transformForLint(code, options.filePath);
|
|
151
|
-
jsCode = info.output;
|
|
134
|
+
export const meta = {
|
|
135
|
+
name: 'ember-eslint-parser',
|
|
136
|
+
version: '*',
|
|
137
|
+
};
|
|
152
138
|
|
|
153
|
-
|
|
154
|
-
|
|
139
|
+
export function parseForESLint(code, options) {
|
|
140
|
+
const allowGjsWasSet = options.allowGjs !== undefined;
|
|
141
|
+
const allowGjs = allowGjsWasSet ? options.allowGjs : getAllowJs(options);
|
|
142
|
+
let actualAllowGjs;
|
|
143
|
+
// Only patch TypeScript if we actually need it.
|
|
144
|
+
if (options.programs || options.projectService || options.project) {
|
|
145
|
+
({ allowGjs: actualAllowGjs } = patchTs({ allowGjs }));
|
|
146
|
+
}
|
|
147
|
+
registerParsedFile(options.filePath);
|
|
155
148
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
149
|
+
const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts');
|
|
150
|
+
let useTypescript = true;
|
|
159
151
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
jsCode = replaceExtensions(jsCode);
|
|
164
|
-
}
|
|
152
|
+
if (options.useBabel || !typescriptParser) {
|
|
153
|
+
useTypescript = false;
|
|
154
|
+
}
|
|
165
155
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
if (isTypescript && !typescriptParser) {
|
|
157
|
+
throw new Error('Please install typescript to process gts');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const filePath = options.filePath;
|
|
161
|
+
const useTS = isTypescript || useTypescript;
|
|
169
162
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
163
|
+
// Both paths create scopeManager inside the parser callback so it's
|
|
164
|
+
// available when toTree invokes visitors during splice — no second pass.
|
|
165
|
+
let scopeManager = null;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = toTree(code, {
|
|
169
|
+
filePath,
|
|
170
|
+
parser: useTS
|
|
171
|
+
? (placeholderJS) => {
|
|
172
|
+
let parseCode = placeholderJS;
|
|
173
|
+
if (options.project || options.projectService) {
|
|
174
|
+
parseCode = replaceExtensions(parseCode);
|
|
175
|
+
}
|
|
176
|
+
const tsResult = typescriptParser.parseForESLint(parseCode, {
|
|
174
177
|
...options,
|
|
175
178
|
ranges: true,
|
|
176
179
|
extraFileExtensions: ['.gts', '.gjs'],
|
|
177
180
|
filePath,
|
|
178
|
-
})
|
|
179
|
-
: babelParser.parseForESLint(jsCode, {
|
|
180
|
-
...options,
|
|
181
|
-
requireConfigFile: false,
|
|
182
|
-
ranges: true,
|
|
183
181
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
182
|
+
scopeManager = tsResult.scopeManager;
|
|
183
|
+
return tsResult;
|
|
184
|
+
}
|
|
185
|
+
: (placeholderJS) => {
|
|
186
|
+
// JS path: parse with oxc, create scope manager from placeholder AST
|
|
187
|
+
const { parseSync } = require('oxc-parser');
|
|
188
|
+
const oxcResult = parseSync(filePath || 'input.js', placeholderJS);
|
|
189
|
+
const program = oxcResult.program;
|
|
190
|
+
program.tokens = oxcResult.tokens || [];
|
|
191
|
+
program.comments = oxcResult.comments || [];
|
|
192
|
+
scopeManager = eslintScope.analyze(program, {
|
|
193
|
+
ecmaVersion: 2022,
|
|
194
|
+
sourceType: 'module',
|
|
195
|
+
range: true,
|
|
196
|
+
});
|
|
197
|
+
return { ast: program, scopeManager };
|
|
198
|
+
},
|
|
199
|
+
visitors: buildGlimmerVisitors(() => scopeManager, useTS),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!result.scopeManager) result.scopeManager = scopeManager;
|
|
203
|
+
|
|
204
|
+
if (result.services?.program) {
|
|
205
|
+
const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs;
|
|
206
|
+
if (
|
|
207
|
+
!allowGjsWasSet &&
|
|
208
|
+
programAllowJs !== undefined &&
|
|
209
|
+
actualAllowGjs !== undefined &&
|
|
210
|
+
actualAllowGjs !== programAllowJs
|
|
211
|
+
) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.warn(
|
|
214
|
+
'[ember-eslint-parser] allowJs does not match the actual program. Consider setting allowGjs explicitly.\n' +
|
|
215
|
+
` Current: ${allowGjs}, Program: ${programAllowJs}`
|
|
216
|
+
);
|
|
209
217
|
}
|
|
210
|
-
|
|
211
|
-
} catch (e) {
|
|
212
|
-
console.error(e);
|
|
213
|
-
throw e;
|
|
218
|
+
syncMtsGtsSourceFiles(result.services.program);
|
|
214
219
|
}
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
|
|
221
|
+
delete result.templateInfos;
|
|
222
|
+
delete result.isTypescript;
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.error(e);
|
|
228
|
+
// Convert content-tag parse errors to ESLint-compatible format
|
|
229
|
+
if (e.message?.includes('Parse Error at')) {
|
|
230
|
+
const [line, column] = e.message
|
|
231
|
+
.split(':')
|
|
232
|
+
.slice(-2)
|
|
233
|
+
.map((x) => parseInt(x));
|
|
234
|
+
const err = new Error(e.source_code || e.message);
|
|
235
|
+
err.lineNumber = line;
|
|
236
|
+
err.column = column;
|
|
237
|
+
err.fileName = filePath;
|
|
238
|
+
err.index = undefined;
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default { meta, parseForESLint };
|
package/src/parser/hbs-parser.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import * as eslintScope from 'eslint-scope';
|
|
2
|
+
import { toTree, glimmerVisitorKeys, DocumentLines } from 'ember-estree';
|
|
3
|
+
|
|
4
|
+
// Constant: Program + all Glimmer node types. Computed once at module load.
|
|
5
|
+
const hbsVisitorKeys = { Program: ['body'], ...glimmerVisitorKeys };
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* implements https://eslint.org/docs/latest/extend/custom-parsers
|
|
@@ -14,76 +16,76 @@ const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transform
|
|
|
14
16
|
/**
|
|
15
17
|
* @type {import('eslint').ParserModule}
|
|
16
18
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
19
|
+
export const meta = {
|
|
20
|
+
name: 'ember-eslint-parser/hbs',
|
|
21
|
+
version: '*',
|
|
22
|
+
};
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const codeLines = new DocumentLines(code);
|
|
24
|
+
export function parseForESLint(code, options) {
|
|
25
|
+
const filePath = (options && options.filePath) || '<hbs>';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
result = toTree(code, { templateOnly: true });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Transform glimmer parse error to ESLint-compatible error
|
|
32
|
+
const loc = e.location || (e.hash && e.hash.loc);
|
|
33
|
+
if (loc && loc.start) {
|
|
34
|
+
const codeLines = new DocumentLines(code);
|
|
35
|
+
const err = Object.assign(new SyntaxError(e.message), {
|
|
36
|
+
lineNumber: loc.start.line,
|
|
37
|
+
column: loc.start.column,
|
|
38
|
+
index: codeLines.positionToOffset(loc.start),
|
|
39
|
+
fileName: filePath,
|
|
33
40
|
});
|
|
34
|
-
|
|
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;
|
|
41
|
+
throw err;
|
|
47
42
|
}
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { ast: templateNode, comments } = result;
|
|
47
|
+
|
|
48
|
+
// Use the Template node's loc.end for the Program's end position
|
|
49
|
+
// (avoids creating a duplicate DocumentLines just for this)
|
|
50
|
+
const endLoc = templateNode.loc?.end || { line: 1, column: code.length };
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
// Wrap in a synthetic Program node (required by ESLint)
|
|
53
|
+
const program = {
|
|
54
|
+
type: 'Program',
|
|
55
|
+
body: [templateNode],
|
|
56
|
+
tokens: templateNode.tokens,
|
|
57
|
+
comments,
|
|
58
|
+
range: [0, code.length],
|
|
59
|
+
start: 0,
|
|
60
|
+
end: code.length,
|
|
61
|
+
loc: {
|
|
62
|
+
start: { line: 1, column: 0 },
|
|
63
|
+
end: endLoc,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
// Build visitor keys: Program + all Glimmer node types
|
|
68
|
+
const visitorKeys = hbsVisitorKeys;
|
|
69
|
+
|
|
70
|
+
// Create an empty scope manager.
|
|
71
|
+
// For HBS, all locals are assumed to be defined at runtime,
|
|
72
|
+
// so we don't track variable references (no no-undef errors).
|
|
73
|
+
const scopeManager = eslintScope.analyze(
|
|
74
|
+
{
|
|
53
75
|
type: 'Program',
|
|
54
|
-
body: [
|
|
55
|
-
tokens: templateNode.tokens,
|
|
56
|
-
comments,
|
|
76
|
+
body: [],
|
|
57
77
|
range: [0, code.length],
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end: codeLines.offsetToPosition(code.length),
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Build visitor keys: Program + all Glimmer node types
|
|
67
|
-
const visitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() };
|
|
78
|
+
loc: program.loc,
|
|
79
|
+
},
|
|
80
|
+
{ range: true }
|
|
81
|
+
);
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
range: [0, code.length],
|
|
77
|
-
loc: program.loc,
|
|
78
|
-
},
|
|
79
|
-
{ range: true }
|
|
80
|
-
);
|
|
83
|
+
return {
|
|
84
|
+
ast: program,
|
|
85
|
+
scopeManager,
|
|
86
|
+
visitorKeys,
|
|
87
|
+
services: {},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
ast: program,
|
|
84
|
-
scopeManager,
|
|
85
|
-
visitorKeys,
|
|
86
|
-
services: {},
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
};
|
|
91
|
+
export default { meta, parseForESLint };
|