@utoo/lint 0.3.9 → 0.4.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 +3 -0
- package/index.cjs +634 -278
- package/index.d.ts +11 -0
- package/index.js +634 -278
- package/lib/config-module.cjs +54 -0
- package/lib/estree-parser.cjs +662 -0
- package/lib/load-config.js +67 -1
- package/lib/plugin-runner.cjs +342 -0
- package/package.json +12 -7
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ESLint-compatible parser adapter used to run community ESLint plugin rules.
|
|
4
|
+
//
|
|
5
|
+
// The native lint engine keeps its own AST in Zig. Plugin rules are JavaScript
|
|
6
|
+
// and expect the ESTree shape that ESLint hands to `create(context)`, so this
|
|
7
|
+
// module parses the file a second time with yuku-parser (the same parser the
|
|
8
|
+
// engine is built on), then decorates the tree with `range`, `loc`, `parent`,
|
|
9
|
+
// a token stream, comments, and a lazily computed scope manager.
|
|
10
|
+
//
|
|
11
|
+
// This file is CommonJS on purpose: it is shared by the ESM (`index.js`) and
|
|
12
|
+
// CommonJS (`index.cjs`) entry points.
|
|
13
|
+
|
|
14
|
+
const { extname } = require("node:path");
|
|
15
|
+
const { visitorKeys: TS_VISITOR_KEYS } = require("@typescript-eslint/visitor-keys");
|
|
16
|
+
|
|
17
|
+
const VISITOR_KEYS = Object.freeze({ ...TS_VISITOR_KEYS });
|
|
18
|
+
|
|
19
|
+
const NON_CHILD_KEYS = new Set(["type", "start", "end", "range", "loc", "parent", "comments", "tokens"]);
|
|
20
|
+
|
|
21
|
+
const JS_KEYWORDS = new Set([
|
|
22
|
+
"break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
|
|
23
|
+
"else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof",
|
|
24
|
+
"let", "new", "return", "static", "super", "switch", "this", "throw", "try", "typeof", "var",
|
|
25
|
+
"void", "while", "with", "yield"
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const TS_KEYWORDS = new Set([
|
|
29
|
+
"abstract", "any", "as", "asserts", "async", "await", "bigint", "boolean", "declare", "enum",
|
|
30
|
+
"from", "get", "global", "implements", "infer", "interface", "is", "keyof", "module", "namespace",
|
|
31
|
+
"never", "number", "object", "of", "out", "override", "private", "protected", "public", "readonly",
|
|
32
|
+
"require", "satisfies", "set", "string", "symbol", "type", "undefined", "unique", "unknown"
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const PUNCTUATORS = [
|
|
36
|
+
">>>=", "...", "===", "!==", "**=", "<<=", ">>=", ">>>", "&&=", "||=", "??=",
|
|
37
|
+
"=>", "==", "!=", "<=", ">=", "&&", "||", "??", "?.", "++", "--", "+=", "-=", "*=", "/=",
|
|
38
|
+
"%=", "&=", "|=", "^=", "**", "<<", ">>"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const ECMASCRIPT_GLOBALS = [
|
|
42
|
+
"AggregateError", "Array", "ArrayBuffer", "Atomics", "BigInt", "BigInt64Array", "BigUint64Array",
|
|
43
|
+
"Boolean", "DataView", "Date", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent",
|
|
44
|
+
"Error", "escape", "eval", "EvalError", "FinalizationRegistry", "Float16Array", "Float32Array",
|
|
45
|
+
"Float64Array", "Function", "globalThis", "Infinity", "Int16Array", "Int32Array", "Int8Array",
|
|
46
|
+
"Intl", "isFinite", "isNaN", "Iterator", "JSON", "Map", "Math", "NaN", "Number", "Object",
|
|
47
|
+
"parseFloat", "parseInt", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp",
|
|
48
|
+
"Set", "SharedArrayBuffer", "String", "Symbol", "SyntaxError", "TypeError", "Uint16Array",
|
|
49
|
+
"Uint32Array", "Uint8Array", "Uint8ClampedArray", "undefined", "unescape", "URIError", "WeakMap",
|
|
50
|
+
"WeakRef", "WeakSet"
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const COMMONJS_GLOBALS = {
|
|
54
|
+
exports: "writable",
|
|
55
|
+
global: "readonly",
|
|
56
|
+
module: "readonly",
|
|
57
|
+
require: "readonly",
|
|
58
|
+
__dirname: "readonly",
|
|
59
|
+
__filename: "readonly"
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let yukuModule = null;
|
|
63
|
+
let yukuLoadError = null;
|
|
64
|
+
|
|
65
|
+
function loadYuku() {
|
|
66
|
+
if (yukuModule) {
|
|
67
|
+
return yukuModule;
|
|
68
|
+
}
|
|
69
|
+
if (yukuLoadError) {
|
|
70
|
+
throw yukuLoadError;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
// yuku-parser is an ES module. Loading it synchronously from CommonJS
|
|
74
|
+
// relies on require(esm), which Node.js 20.19+ and 22.12+ support.
|
|
75
|
+
yukuModule = require("yuku-parser");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
yukuLoadError = new Error(
|
|
78
|
+
"utoo-lint: unable to load yuku-parser, which is required to run ESLint plugin rules. " +
|
|
79
|
+
"Plugin rules need Node.js 20.19 or newer (or 22.12 or newer). " +
|
|
80
|
+
`Underlying error: ${error?.message ?? error}`
|
|
81
|
+
);
|
|
82
|
+
yukuLoadError.cause = error;
|
|
83
|
+
throw yukuLoadError;
|
|
84
|
+
}
|
|
85
|
+
return yukuModule;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isNativeParserAvailable() {
|
|
89
|
+
try {
|
|
90
|
+
loadYuku();
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function languageForFile(filePath, ecmaFeatures) {
|
|
98
|
+
const lower = String(filePath ?? "").toLowerCase();
|
|
99
|
+
if (lower.endsWith(".d.ts") || lower.endsWith(".d.mts") || lower.endsWith(".d.cts")) {
|
|
100
|
+
return "dts";
|
|
101
|
+
}
|
|
102
|
+
const extension = extname(lower);
|
|
103
|
+
if (extension === ".tsx") {
|
|
104
|
+
return "tsx";
|
|
105
|
+
}
|
|
106
|
+
if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
|
|
107
|
+
return "ts";
|
|
108
|
+
}
|
|
109
|
+
if (extension === ".jsx") {
|
|
110
|
+
return "jsx";
|
|
111
|
+
}
|
|
112
|
+
return ecmaFeatures?.jsx === false ? "js" : "jsx";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isTypeScriptLanguage(lang) {
|
|
116
|
+
return lang === "ts" || lang === "tsx" || lang === "dts";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sourceTypeForFile(filePath, requested) {
|
|
120
|
+
if (requested === "script" || requested === "module" || requested === "commonjs") {
|
|
121
|
+
return requested;
|
|
122
|
+
}
|
|
123
|
+
const extension = extname(String(filePath ?? "").toLowerCase());
|
|
124
|
+
return extension === ".cjs" || extension === ".cts" ? "commonjs" : "module";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function lineStartIndices(text) {
|
|
128
|
+
const indices = [0];
|
|
129
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
130
|
+
if (text[index] === "\n") {
|
|
131
|
+
indices.push(index + 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return indices;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createLocator(text) {
|
|
138
|
+
const lineStarts = lineStartIndices(text);
|
|
139
|
+
return (index) => {
|
|
140
|
+
let low = 0;
|
|
141
|
+
let high = lineStarts.length - 1;
|
|
142
|
+
while (low < high) {
|
|
143
|
+
const middle = (low + high + 1) >> 1;
|
|
144
|
+
if (lineStarts[middle] <= index) {
|
|
145
|
+
low = middle;
|
|
146
|
+
} else {
|
|
147
|
+
high = middle - 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { line: low + 1, column: index - lineStarts[low] };
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isNode(value) {
|
|
155
|
+
return value !== null && typeof value === "object" && typeof value.type === "string";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function childKeys(node) {
|
|
159
|
+
const keys = VISITOR_KEYS[node.type];
|
|
160
|
+
if (keys) {
|
|
161
|
+
return keys;
|
|
162
|
+
}
|
|
163
|
+
return Object.keys(node).filter((key) => !NON_CHILD_KEYS.has(key));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function forEachChild(node, callback) {
|
|
167
|
+
for (const key of childKeys(node)) {
|
|
168
|
+
const value = node[key];
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
for (const item of value) {
|
|
171
|
+
if (isNode(item)) {
|
|
172
|
+
callback(item);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else if (isNode(value)) {
|
|
176
|
+
callback(value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const JSX_ENTITIES = {
|
|
182
|
+
amp: "&", lt: "<", gt: ">", quot: "\"", apos: "'", nbsp: "\u00a0", copy: "\u00a9", reg: "\u00ae",
|
|
183
|
+
trade: "\u2122", hellip: "\u2026", mdash: "\u2014", ndash: "\u2013", laquo: "\u00ab", raquo: "\u00bb",
|
|
184
|
+
lsquo: "\u2018", rsquo: "\u2019", ldquo: "\u201c", rdquo: "\u201d", times: "\u00d7", divide: "\u00f7",
|
|
185
|
+
euro: "\u20ac", pound: "\u00a3", yen: "\u00a5", cent: "\u00a2", deg: "\u00b0", plusmn: "\u00b1",
|
|
186
|
+
middot: "\u00b7", bull: "\u2022", larr: "\u2190", rarr: "\u2192", uarr: "\u2191", darr: "\u2193",
|
|
187
|
+
hearts: "\u2665", sect: "\u00a7", para: "\u00b6", iexcl: "\u00a1", iquest: "\u00bf", frac12: "\u00bd",
|
|
188
|
+
frac14: "\u00bc", frac34: "\u00be", micro: "\u00b5", ensp: "\u2002", emsp: "\u2003", thinsp: "\u2009",
|
|
189
|
+
zwnj: "\u200c", zwj: "\u200d", shy: "\u00ad", infin: "\u221e", ne: "\u2260", le: "\u2264", ge: "\u2265"
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ESLint's parsers expose entity-decoded JSX text and attribute values while
|
|
193
|
+
// keeping `raw` intact; mirror that for the common entities.
|
|
194
|
+
function decodeJsxEntities(value) {
|
|
195
|
+
if (typeof value !== "string" || !value.includes("&")) {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
return value.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/gu, (match, entity) => {
|
|
199
|
+
if (entity[0] === "#") {
|
|
200
|
+
const codePoint = entity[1] === "x" || entity[1] === "X" ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
|
|
201
|
+
return Number.isFinite(codePoint) && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : match;
|
|
202
|
+
}
|
|
203
|
+
return Object.hasOwn(JSX_ENTITIES, entity) ? JSX_ENTITIES[entity] : match;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setParent(node, parent) {
|
|
208
|
+
Object.defineProperty(node, "parent", {
|
|
209
|
+
value: parent,
|
|
210
|
+
writable: true,
|
|
211
|
+
configurable: true,
|
|
212
|
+
enumerable: false
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function decorateNode(node, locate) {
|
|
217
|
+
node.range = [node.start, node.end];
|
|
218
|
+
node.loc = { start: locate(node.start), end: locate(node.end) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeTree(program, text, locate) {
|
|
222
|
+
const stack = [[program, null]];
|
|
223
|
+
while (stack.length > 0) {
|
|
224
|
+
const [node, parent] = stack.pop();
|
|
225
|
+
if (node.type === "TemplateElement") {
|
|
226
|
+
// ESLint's parsers include the surrounding backticks and `${`/`}`
|
|
227
|
+
// delimiters in a template element's range.
|
|
228
|
+
node.start -= 1;
|
|
229
|
+
node.end += node.tail ? 1 : 2;
|
|
230
|
+
}
|
|
231
|
+
decorateNode(node, locate);
|
|
232
|
+
setParent(node, parent);
|
|
233
|
+
if (node.type === "JSXText" || (node.type === "Literal" && parent?.type === "JSXAttribute")) {
|
|
234
|
+
node.value = decodeJsxEntities(node.value);
|
|
235
|
+
}
|
|
236
|
+
forEachChild(node, (child) => stack.push([child, node]));
|
|
237
|
+
}
|
|
238
|
+
program.start = 0;
|
|
239
|
+
program.end = text.length;
|
|
240
|
+
decorateNode(program, locate);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeComments(program, comments, text, locate) {
|
|
244
|
+
const result = [];
|
|
245
|
+
if (program.hashbang && typeof program.hashbang === "object") {
|
|
246
|
+
const { start, end } = program.hashbang;
|
|
247
|
+
result.push({ type: "Hashbang", value: text.slice(start + 2, end), start, end, range: [start, end], loc: { start: locate(start), end: locate(end) } });
|
|
248
|
+
}
|
|
249
|
+
delete program.hashbang;
|
|
250
|
+
for (const comment of comments) {
|
|
251
|
+
result.push({
|
|
252
|
+
type: comment.type,
|
|
253
|
+
value: comment.value,
|
|
254
|
+
start: comment.start,
|
|
255
|
+
end: comment.end,
|
|
256
|
+
range: [comment.start, comment.end],
|
|
257
|
+
loc: { start: locate(comment.start), end: locate(comment.end) }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
result.sort((left, right) => left.start - right.start);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function token(type, text, start, end, locate) {
|
|
265
|
+
return {
|
|
266
|
+
type,
|
|
267
|
+
value: text.slice(start, end),
|
|
268
|
+
start,
|
|
269
|
+
end,
|
|
270
|
+
range: [start, end],
|
|
271
|
+
loc: { start: locate(start), end: locate(end) }
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function identifierRange(node, text) {
|
|
276
|
+
const name = node.name;
|
|
277
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
278
|
+
return [node.start, node.end];
|
|
279
|
+
}
|
|
280
|
+
if (text.startsWith(name, node.start)) {
|
|
281
|
+
return [node.start, node.start + name.length];
|
|
282
|
+
}
|
|
283
|
+
const index = text.indexOf(name, node.start);
|
|
284
|
+
if (index !== -1 && index + name.length <= node.end) {
|
|
285
|
+
return [index, index + name.length];
|
|
286
|
+
}
|
|
287
|
+
return [node.start, node.end];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function literalTokenType(node) {
|
|
291
|
+
if (node.regex && typeof node.regex === "object") {
|
|
292
|
+
return "RegularExpression";
|
|
293
|
+
}
|
|
294
|
+
if (typeof node.value === "string") {
|
|
295
|
+
return node.parent?.type === "JSXAttribute" ? "JSXText" : "String";
|
|
296
|
+
}
|
|
297
|
+
if (typeof node.value === "boolean") {
|
|
298
|
+
return "Boolean";
|
|
299
|
+
}
|
|
300
|
+
if (node.value === null && node.raw === "null") {
|
|
301
|
+
return "Null";
|
|
302
|
+
}
|
|
303
|
+
return "Numeric";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function collectLeafTokens(program, text, locate) {
|
|
307
|
+
const leaves = [];
|
|
308
|
+
const singleCharPositions = new Set();
|
|
309
|
+
const stack = [program];
|
|
310
|
+
while (stack.length > 0) {
|
|
311
|
+
const node = stack.pop();
|
|
312
|
+
switch (node.type) {
|
|
313
|
+
case "Identifier": {
|
|
314
|
+
const [start, end] = identifierRange(node, text);
|
|
315
|
+
const isMeta = node.parent?.type === "MetaProperty" && node.parent.meta === node;
|
|
316
|
+
leaves.push(token(isMeta ? "Keyword" : "Identifier", text, start, end, locate));
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case "PrivateIdentifier": {
|
|
320
|
+
const leaf = token("PrivateIdentifier", text, node.start, node.end, locate);
|
|
321
|
+
leaf.value = node.name;
|
|
322
|
+
leaves.push(leaf);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case "JSXIdentifier":
|
|
326
|
+
leaves.push(token("JSXIdentifier", text, node.start, node.end, locate));
|
|
327
|
+
break;
|
|
328
|
+
case "JSXText":
|
|
329
|
+
if (node.end > node.start) {
|
|
330
|
+
const leaf = token("JSXText", text, node.start, node.end, locate);
|
|
331
|
+
if (typeof node.value === "string") {
|
|
332
|
+
leaf.value = node.value;
|
|
333
|
+
}
|
|
334
|
+
leaves.push(leaf);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case "Literal":
|
|
338
|
+
leaves.push(token(literalTokenType(node), text, node.start, node.end, locate));
|
|
339
|
+
break;
|
|
340
|
+
case "TemplateElement":
|
|
341
|
+
leaves.push(token("Template", text, node.start, node.end, locate));
|
|
342
|
+
break;
|
|
343
|
+
case "TSTypeParameterInstantiation":
|
|
344
|
+
case "TSTypeParameterDeclaration":
|
|
345
|
+
singleCharPositions.add(node.start);
|
|
346
|
+
singleCharPositions.add(node.end - 1);
|
|
347
|
+
break;
|
|
348
|
+
default:
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
forEachChild(node, (child) => stack.push(child));
|
|
352
|
+
}
|
|
353
|
+
leaves.sort((left, right) => left.start - right.start);
|
|
354
|
+
return { leaves, singleCharPositions };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isWhitespace(code) {
|
|
358
|
+
return code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d || code === 0x0b || code === 0x0c
|
|
359
|
+
|| code === 0xa0 || code === 0xfeff || code === 0x2028 || code === 0x2029
|
|
360
|
+
|| (code >= 0x1680 && /\s/u.test(String.fromCharCode(code)));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const IDENTIFIER_START = /[$_\p{ID_Start}]/u;
|
|
364
|
+
const IDENTIFIER_PART = /[$_\p{ID_Continue}]/u;
|
|
365
|
+
|
|
366
|
+
function readWord(text, index, limit) {
|
|
367
|
+
let end = index;
|
|
368
|
+
while (end < limit) {
|
|
369
|
+
const char = text[end];
|
|
370
|
+
if (char === "\\" && text[end + 1] === "u") {
|
|
371
|
+
end += 2;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const codePoint = text.codePointAt(end);
|
|
375
|
+
const chars = String.fromCodePoint(codePoint);
|
|
376
|
+
if (!(end === index ? IDENTIFIER_START : IDENTIFIER_PART).test(chars)) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
end += chars.length;
|
|
380
|
+
}
|
|
381
|
+
return end;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function skipLineComment(text, index, limit) {
|
|
385
|
+
while (index < limit) {
|
|
386
|
+
const code = text.charCodeAt(index);
|
|
387
|
+
if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) {
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
index += 1;
|
|
391
|
+
}
|
|
392
|
+
return index;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function readNumber(text, index, limit) {
|
|
396
|
+
let end = index;
|
|
397
|
+
while (end < limit && /[0-9a-zA-Z_.]/u.test(text[end])) {
|
|
398
|
+
end += 1;
|
|
399
|
+
}
|
|
400
|
+
return end;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function readQuoted(text, index, limit) {
|
|
404
|
+
const quote = text[index];
|
|
405
|
+
let end = index + 1;
|
|
406
|
+
while (end < limit) {
|
|
407
|
+
if (text[end] === "\\") {
|
|
408
|
+
end += 2;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (text[end] === quote) {
|
|
412
|
+
return end + 1;
|
|
413
|
+
}
|
|
414
|
+
end += 1;
|
|
415
|
+
}
|
|
416
|
+
return limit;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function lexGap(text, from, to, tokens, locate, keywords, singleCharPositions) {
|
|
420
|
+
let index = from;
|
|
421
|
+
while (index < to) {
|
|
422
|
+
const code = text.charCodeAt(index);
|
|
423
|
+
if (isWhitespace(code)) {
|
|
424
|
+
index += 1;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const char = text[index];
|
|
428
|
+
const next = text[index + 1];
|
|
429
|
+
if (char === "/" && next === "/") {
|
|
430
|
+
index = skipLineComment(text, index + 2, to);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (char === "/" && next === "*") {
|
|
434
|
+
const close = text.indexOf("*/", index + 2);
|
|
435
|
+
index = close === -1 || close + 2 > to ? to : close + 2;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (char === "#" && next === "!" && index === 0) {
|
|
439
|
+
index = skipLineComment(text, index + 2, to);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (char === "#" && index + 1 < to && IDENTIFIER_START.test(String.fromCodePoint(text.codePointAt(index + 1)))) {
|
|
443
|
+
const end = readWord(text, index + 1, to);
|
|
444
|
+
const leaf = token("PrivateIdentifier", text, index, end, locate);
|
|
445
|
+
leaf.value = text.slice(index + 1, end);
|
|
446
|
+
tokens.push(leaf);
|
|
447
|
+
index = end;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (IDENTIFIER_START.test(String.fromCodePoint(text.codePointAt(index))) || (char === "\\" && next === "u")) {
|
|
451
|
+
const end = readWord(text, index, to);
|
|
452
|
+
const word = text.slice(index, end);
|
|
453
|
+
let type = "Identifier";
|
|
454
|
+
if (word === "null") {
|
|
455
|
+
type = "Null";
|
|
456
|
+
} else if (word === "true" || word === "false") {
|
|
457
|
+
type = "Boolean";
|
|
458
|
+
} else if (keywords.has(word)) {
|
|
459
|
+
type = "Keyword";
|
|
460
|
+
}
|
|
461
|
+
tokens.push(token(type, text, index, end, locate));
|
|
462
|
+
index = end;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (/[0-9]/u.test(char) || (char === "." && /[0-9]/u.test(next ?? ""))) {
|
|
466
|
+
const end = readNumber(text, index + 1, to);
|
|
467
|
+
tokens.push(token("Numeric", text, index, end, locate));
|
|
468
|
+
index = end;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (char === "\"" || char === "'") {
|
|
472
|
+
const end = readQuoted(text, index, to);
|
|
473
|
+
tokens.push(token("String", text, index, end, locate));
|
|
474
|
+
index = end;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (char === "`") {
|
|
478
|
+
const end = readQuoted(text, index, to);
|
|
479
|
+
tokens.push(token("Template", text, index, end, locate));
|
|
480
|
+
index = end;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
let length = 1;
|
|
484
|
+
if (!singleCharPositions.has(index)) {
|
|
485
|
+
const punctuator = PUNCTUATORS.find((candidate) => text.startsWith(candidate, index) && index + candidate.length <= to);
|
|
486
|
+
if (punctuator && !(punctuator === "?." && /[0-9]/u.test(text[index + 2] ?? ""))) {
|
|
487
|
+
length = punctuator.length;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
tokens.push(token("Punctuator", text, index, index + length, locate));
|
|
491
|
+
index += length;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildTokens(program, text, locate, lang) {
|
|
496
|
+
const { leaves, singleCharPositions } = collectLeafTokens(program, text, locate);
|
|
497
|
+
const keywords = isTypeScriptLanguage(lang) ? new Set([...JS_KEYWORDS, ...TS_KEYWORDS]) : JS_KEYWORDS;
|
|
498
|
+
const tokens = [];
|
|
499
|
+
let position = 0;
|
|
500
|
+
for (const leaf of leaves) {
|
|
501
|
+
if (leaf.start < position) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
lexGap(text, position, leaf.start, tokens, locate, keywords, singleCharPositions);
|
|
505
|
+
tokens.push(leaf);
|
|
506
|
+
position = leaf.end;
|
|
507
|
+
}
|
|
508
|
+
lexGap(text, position, text.length, tokens, locate, keywords, singleCharPositions);
|
|
509
|
+
return tokens;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeGlobalAccess(value) {
|
|
513
|
+
if (value === "off") {
|
|
514
|
+
return "off";
|
|
515
|
+
}
|
|
516
|
+
if (value === true || value === "writable" || value === "writeable" || value === "write") {
|
|
517
|
+
return "writable";
|
|
518
|
+
}
|
|
519
|
+
return "readonly";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function declareGlobals(scopeManager, globals, createVariable) {
|
|
523
|
+
const globalScope = scopeManager.globalScope ?? scopeManager.scopes?.[0];
|
|
524
|
+
if (!globalScope || !(globalScope.set instanceof Map)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
for (const [name, rawAccess] of Object.entries(globals)) {
|
|
528
|
+
const access = normalizeGlobalAccess(rawAccess);
|
|
529
|
+
if (access === "off") {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
let variable = globalScope.set.get(name);
|
|
533
|
+
if (!variable) {
|
|
534
|
+
variable = createVariable(globalScope, name, access);
|
|
535
|
+
globalScope.set.set(name, variable);
|
|
536
|
+
globalScope.variables.push(variable);
|
|
537
|
+
}
|
|
538
|
+
variable.eslintImplicitGlobalSetting = access;
|
|
539
|
+
variable.eslintExplicitGlobal = true;
|
|
540
|
+
variable.writeable = access === "writable";
|
|
541
|
+
}
|
|
542
|
+
if (Array.isArray(globalScope.through)) {
|
|
543
|
+
globalScope.through = globalScope.through.filter((reference) => {
|
|
544
|
+
const variable = globalScope.set.get(reference.identifier?.name);
|
|
545
|
+
if (!variable) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
reference.resolved = variable;
|
|
549
|
+
variable.references.push(reference);
|
|
550
|
+
return false;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function collectGlobals(options, sourceType) {
|
|
556
|
+
const globals = {};
|
|
557
|
+
for (const name of ECMASCRIPT_GLOBALS) {
|
|
558
|
+
globals[name] = "readonly";
|
|
559
|
+
}
|
|
560
|
+
if (sourceType === "commonjs") {
|
|
561
|
+
Object.assign(globals, COMMONJS_GLOBALS);
|
|
562
|
+
}
|
|
563
|
+
for (const [name, access] of Object.entries(options.globals ?? {})) {
|
|
564
|
+
globals[name] = access;
|
|
565
|
+
}
|
|
566
|
+
return globals;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function createScopeManagerFactory(program, options) {
|
|
570
|
+
let cached = null;
|
|
571
|
+
let computed = false;
|
|
572
|
+
return () => {
|
|
573
|
+
if (computed) {
|
|
574
|
+
return cached;
|
|
575
|
+
}
|
|
576
|
+
computed = true;
|
|
577
|
+
cached = analyzeScopes(program, options);
|
|
578
|
+
return cached;
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function analyzeScopes(program, options) {
|
|
583
|
+
const { lang, sourceType, ecmaFeatures } = options;
|
|
584
|
+
const globalReturn = ecmaFeatures?.globalReturn === true || sourceType === "commonjs";
|
|
585
|
+
const impliedStrict = ecmaFeatures?.impliedStrict === true;
|
|
586
|
+
const globals = collectGlobals(options, sourceType);
|
|
587
|
+
|
|
588
|
+
if (isTypeScriptLanguage(lang)) {
|
|
589
|
+
const tsScope = require("@typescript-eslint/scope-manager");
|
|
590
|
+
const scopeManager = tsScope.analyze(program, {
|
|
591
|
+
sourceType: sourceType === "commonjs" ? "script" : sourceType,
|
|
592
|
+
globalReturn,
|
|
593
|
+
impliedStrict,
|
|
594
|
+
jsxPragma: options.jsxPragma ?? "React",
|
|
595
|
+
jsxFragmentName: options.jsxFragmentName ?? null,
|
|
596
|
+
lib: ["esnext"],
|
|
597
|
+
childVisitorKeys: VISITOR_KEYS
|
|
598
|
+
});
|
|
599
|
+
declareGlobals(scopeManager, globals, (globalScope, name, access) => new tsScope.ImplicitLibVariable(globalScope, name, {
|
|
600
|
+
isTypeVariable: false,
|
|
601
|
+
isValueVariable: true,
|
|
602
|
+
writeable: access === "writable",
|
|
603
|
+
eslintImplicitGlobalSetting: access
|
|
604
|
+
}));
|
|
605
|
+
return scopeManager;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const eslintScope = require("eslint-scope");
|
|
609
|
+
const scopeManager = eslintScope.analyze(program, {
|
|
610
|
+
ignoreEval: true,
|
|
611
|
+
nodejsScope: globalReturn,
|
|
612
|
+
impliedStrict,
|
|
613
|
+
ecmaVersion: typeof options.ecmaVersion === "number" ? Math.max(options.ecmaVersion, 6) : 2026,
|
|
614
|
+
sourceType,
|
|
615
|
+
jsx: lang === "jsx",
|
|
616
|
+
childVisitorKeys: VISITOR_KEYS,
|
|
617
|
+
fallback: "iteration"
|
|
618
|
+
});
|
|
619
|
+
declareGlobals(scopeManager, globals, (globalScope, name) => new eslintScope.Variable(name, globalScope));
|
|
620
|
+
return scopeManager;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Parse `text` and return `{ ast, scopeManager, visitorKeys, services }` in
|
|
625
|
+
* the shape ESLint expects from a parser's `parseForESLint`.
|
|
626
|
+
*
|
|
627
|
+
* `scopeManager` is a function so scope analysis only runs when a rule asks
|
|
628
|
+
* for it; `SourceCode` in the entry points turns it into a lazy getter.
|
|
629
|
+
*/
|
|
630
|
+
function parseForESLint(text, options = {}) {
|
|
631
|
+
if (typeof text !== "string") {
|
|
632
|
+
throw new TypeError("parseForESLint requires source text");
|
|
633
|
+
}
|
|
634
|
+
const { parse } = loadYuku();
|
|
635
|
+
const filePath = options.filePath ?? options.filename ?? "input.js";
|
|
636
|
+
const ecmaFeatures = options.ecmaFeatures ?? {};
|
|
637
|
+
const lang = languageForFile(filePath, ecmaFeatures);
|
|
638
|
+
const sourceType = sourceTypeForFile(filePath, options.sourceType);
|
|
639
|
+
const result = parse(text, { lang, sourceType, preserveParens: false });
|
|
640
|
+
const program = result.program;
|
|
641
|
+
const locate = createLocator(text);
|
|
642
|
+
|
|
643
|
+
normalizeTree(program, text, locate);
|
|
644
|
+
program.comments = normalizeComments(program, result.comments ?? [], text, locate);
|
|
645
|
+
program.tokens = buildTokens(program, text, locate, lang);
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
ast: program,
|
|
649
|
+
scopeManager: createScopeManagerFactory(program, { ...options, lang, sourceType, ecmaFeatures }),
|
|
650
|
+
visitorKeys: VISITOR_KEYS,
|
|
651
|
+
services: {},
|
|
652
|
+
diagnostics: result.diagnostics ?? []
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
module.exports = {
|
|
657
|
+
ECMASCRIPT_GLOBALS,
|
|
658
|
+
VISITOR_KEYS,
|
|
659
|
+
isNativeParserAvailable,
|
|
660
|
+
languageForFile,
|
|
661
|
+
parseForESLint
|
|
662
|
+
};
|