@utoo/lint 0.3.9 → 0.4.1
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 +11 -3
- package/bin/eslint.js +4 -0
- package/bin/fishlint-lint-staged.js +0 -0
- package/bin/fishlint.js +0 -0
- package/index.cjs +687 -290
- package/index.d.ts +11 -0
- package/index.js +687 -290
- package/lib/config-module.cjs +54 -0
- package/lib/estree-parser.cjs +662 -0
- package/lib/load-config.js +74 -1
- package/lib/native-rule-options.cjs +99 -0
- package/lib/plugin-runner.cjs +342 -0
- package/package.json +13 -10
- package/schema.json +24 -0
package/index.cjs
CHANGED
|
@@ -6,11 +6,16 @@ const { dirname, extname, isAbsolute, join, relative, resolve: resolvePath } = r
|
|
|
6
6
|
|
|
7
7
|
const { platformPackageName, resolveBinary } = require("./lib/binary.cjs");
|
|
8
8
|
const { createLegacyConfigResolver } = require("./lib/legacy-config.cjs");
|
|
9
|
+
const { nativeFlatConfigEntries, nativeRules } = require("./lib/native-rule-options.cjs");
|
|
9
10
|
const { formatESLintResults } = require("./lib/stylish-formatter.cjs");
|
|
10
11
|
const {
|
|
11
12
|
findConfigPath: findConfigPathFromDirectory,
|
|
13
|
+
isExecutableConfigPath,
|
|
12
14
|
readConfig
|
|
13
15
|
} = require("./lib/config-loader.cjs");
|
|
16
|
+
const configModule = require("./lib/config-module.cjs");
|
|
17
|
+
const estreeParser = require("./lib/estree-parser.cjs");
|
|
18
|
+
const pluginRunner = require("./lib/plugin-runner.cjs");
|
|
14
19
|
|
|
15
20
|
const version = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8")).version;
|
|
16
21
|
|
|
@@ -25,10 +30,6 @@ const JS_KEYWORDS = new Set([
|
|
|
25
30
|
"instanceof", "let", "new", "return", "super", "switch", "this", "throw", "try", "typeof",
|
|
26
31
|
"var", "void", "while", "with", "yield"
|
|
27
32
|
]);
|
|
28
|
-
const AST_TRAVERSAL_SKIP_KEYS = new Set([
|
|
29
|
-
"type", "parent", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments",
|
|
30
|
-
"innerComments", "raw", "value", "name", "regex", "bigint", "errors"
|
|
31
|
-
]);
|
|
32
33
|
const RULE_TESTER_INITIAL_CONFIG = { rules: {} };
|
|
33
34
|
let ruleTesterDefaultConfig = { rules: {} };
|
|
34
35
|
let ruleTesterDescribe = null;
|
|
@@ -480,6 +481,7 @@ const NATIVE_ONLY_RULE_IDS = [
|
|
|
480
481
|
"semantic-errors"
|
|
481
482
|
];
|
|
482
483
|
const NATIVE_RULE_IDS = [...BUILTIN_RULE_IDS, ...NATIVE_ONLY_RULE_IDS];
|
|
484
|
+
const NATIVE_RULE_ID_SET = new Set(NATIVE_RULE_IDS);
|
|
483
485
|
const FIXABLE_BUILTIN_RULE_IDS = new Set([
|
|
484
486
|
"@typescript-eslint/no-explicit-any",
|
|
485
487
|
"jest/no-deprecated-functions",
|
|
@@ -607,15 +609,15 @@ class UtooLint {
|
|
|
607
609
|
throw new Error("'results' must be an array");
|
|
608
610
|
}
|
|
609
611
|
const options = eslintConstructorOptions(this.options);
|
|
610
|
-
const customRuleConfig = [options.baseConfig, options.overrideConfig].filter(Boolean);
|
|
611
612
|
const rulesByFilePath = new Map();
|
|
613
|
+
const cachedOptions = withConfigCache(options);
|
|
612
614
|
return rulesMetaForResults(results, (ruleId, result) => {
|
|
613
|
-
if (
|
|
615
|
+
if (typeof result.filePath !== "string") {
|
|
614
616
|
return undefined;
|
|
615
617
|
}
|
|
616
618
|
const filePath = normalizeESLintFilePath(result.filePath, options.cwd);
|
|
617
619
|
if (!rulesByFilePath.has(filePath)) {
|
|
618
|
-
rulesByFilePath.set(filePath,
|
|
620
|
+
rulesByFilePath.set(filePath, jsPluginRuleMapForFile(cachedOptions, filePath));
|
|
619
621
|
}
|
|
620
622
|
return rulesByFilePath.get(filePath).get(ruleId)?.meta;
|
|
621
623
|
});
|
|
@@ -1229,21 +1231,210 @@ function lintFiles(paths, options = {}) {
|
|
|
1229
1231
|
}
|
|
1230
1232
|
|
|
1231
1233
|
const nativeFlatOptions = nativeFlatConfigOptions(options);
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1234
|
+
const report = nativeFlatOptions
|
|
1235
|
+
? runNativeLintReport(lintPaths, nativeFlatOptions)
|
|
1236
|
+
: runNativeConfigRunReports(lintPaths, options);
|
|
1237
|
+
// The flat-config path resolves one config for the whole run; only the
|
|
1238
|
+
// per-file path has already discovered (and cached) nested configs.
|
|
1239
|
+
appendPluginRuleDiagnostics(report, options, { perFileConfig: !nativeFlatOptions });
|
|
1240
|
+
return finalizeLintReport(report, ignoredDiagnostics, options);
|
|
1241
|
+
}
|
|
1236
1242
|
|
|
1243
|
+
function runNativeConfigRunReports(lintPaths, options) {
|
|
1237
1244
|
const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
|
|
1238
1245
|
if (configRuns) {
|
|
1239
|
-
|
|
1246
|
+
return mergeNativeReports(
|
|
1240
1247
|
configRuns.map(({ paths: runPaths, options: runOptions }) => runNativeLintReport(runPaths, runOptions))
|
|
1241
1248
|
);
|
|
1242
|
-
return finalizeLintReport(report, ignoredDiagnostics, options);
|
|
1243
1249
|
}
|
|
1244
1250
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1251
|
+
return runNativeLintReport(lintPaths, options);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function pluginFixModeForOptions(options) {
|
|
1255
|
+
const extraArgs = Array.isArray(options.extraArgs) ? options.extraArgs : [];
|
|
1256
|
+
if (options.fix || extraArgs.includes("--fix-dry-run")) {
|
|
1257
|
+
return "dry-run";
|
|
1258
|
+
}
|
|
1259
|
+
return extraArgs.includes("--fix") ? "write" : null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function pluginMessageToDiagnostic(message, filePath) {
|
|
1263
|
+
const toFix = (fix) => ({ range: [fix.range[0], fix.range[1]], text: fix.text });
|
|
1264
|
+
const diagnostic = {
|
|
1265
|
+
filePath,
|
|
1266
|
+
line: message.line,
|
|
1267
|
+
column: message.column,
|
|
1268
|
+
severity: message.severity === 2 ? "error" : "warning",
|
|
1269
|
+
message: message.message,
|
|
1270
|
+
ruleId: message.ruleId,
|
|
1271
|
+
fixes: ruleFixItems(message.fix).map(toFix),
|
|
1272
|
+
suggestions: (message.suggestions ?? []).map((suggestion) => ({
|
|
1273
|
+
desc: suggestion.desc,
|
|
1274
|
+
fix: ruleFixItems(suggestion.fix).map(toFix)
|
|
1275
|
+
}))
|
|
1276
|
+
};
|
|
1277
|
+
if (typeof message.endLine === "number") {
|
|
1278
|
+
diagnostic.endLine = message.endLine;
|
|
1279
|
+
}
|
|
1280
|
+
if (typeof message.endColumn === "number") {
|
|
1281
|
+
diagnostic.endColumn = message.endColumn;
|
|
1282
|
+
}
|
|
1283
|
+
return diagnostic;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function sortReportDiagnostics(report) {
|
|
1287
|
+
const order = new Map((report.filePaths ?? []).map((filePath, index) => [filePath, index]));
|
|
1288
|
+
const rank = (diagnostic) => order.get(diagnostic.filePath) ?? Number.MAX_SAFE_INTEGER;
|
|
1289
|
+
const compare = (left, right) => rank(left) - rank(right)
|
|
1290
|
+
|| (left.line ?? 0) - (right.line ?? 0)
|
|
1291
|
+
|| (left.column ?? 0) - (right.column ?? 0);
|
|
1292
|
+
report.diagnostics = [...(report.diagnostics ?? [])].sort(compare);
|
|
1293
|
+
report.suppressedDiagnostics = [...(report.suppressedDiagnostics ?? [])].sort(compare);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Runs the ESLint plugin rules a config mounts and merges their diagnostics
|
|
1297
|
+
// into the native report. Plugin rules never reach the native binary; see
|
|
1298
|
+
// `withoutJsPluginRules`.
|
|
1299
|
+
function appendPluginRuleDiagnostics(report, options, { perFileConfig = true } = {}) {
|
|
1300
|
+
if (options.noConfig && !options.baseConfig && !options.overrideConfig) {
|
|
1301
|
+
return report;
|
|
1302
|
+
}
|
|
1303
|
+
const filePaths = report.filePaths ?? [];
|
|
1304
|
+
if (filePaths.length === 0 || !anyConfigDeclaresPlugins(options)) {
|
|
1305
|
+
return report;
|
|
1306
|
+
}
|
|
1307
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1308
|
+
const singleMatchPath = filePaths.length === 1
|
|
1309
|
+
? options.filePath ?? options.filename ?? options.pluginTextFilePath
|
|
1310
|
+
: undefined;
|
|
1311
|
+
const fixMode = pluginFixModeForOptions(options);
|
|
1312
|
+
let appended = false;
|
|
1313
|
+
|
|
1314
|
+
for (const filePath of filePaths) {
|
|
1315
|
+
const matchPath = singleMatchPath ?? filePath;
|
|
1316
|
+
const ruleMap = jsPluginRuleMapForFile(options, matchPath, perFileConfig);
|
|
1317
|
+
if (ruleMap.size === 0) {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const ruleEntries = pluginRuleEntriesForFile(options, matchPath, ruleMap, perFileConfig);
|
|
1321
|
+
if (ruleEntries.length === 0) {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// The native report echoes targets as given, so relative paths resolve
|
|
1326
|
+
// against the lint cwd rather than the process cwd.
|
|
1327
|
+
const absolutePath = normalizeESLintFilePath(filePath, cwd);
|
|
1328
|
+
const outputIndex = (report.outputs ?? []).findIndex((fixed) => fixed.filePath === filePath);
|
|
1329
|
+
let code = outputIndex === -1 ? undefined : report.outputs[outputIndex].output;
|
|
1330
|
+
if (typeof code !== "string") {
|
|
1331
|
+
try {
|
|
1332
|
+
code = readFileSync(absolutePath, "utf8");
|
|
1333
|
+
} catch {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const config = pluginConfigSourcesForFile(options, matchPath, perFileConfig).map((source) => source.config);
|
|
1339
|
+
const parserOptions = { ...parserOptionsForConfig(config, matchPath, cwd), filePath: matchPath, requireParser: true };
|
|
1340
|
+
const lintOptions = {
|
|
1341
|
+
cwd,
|
|
1342
|
+
filename: matchPath,
|
|
1343
|
+
config: calculatedConfig({ cwd, noConfig: true, baseConfig: config }, matchPath)
|
|
1344
|
+
};
|
|
1345
|
+
const lintOnce = (text) => {
|
|
1346
|
+
const sourceCode = createLinterSourceCode(text, null, parserOptions);
|
|
1347
|
+
return applyDisableDirectives(runCustomLinterRules(ruleEntries, sourceCode, lintOptions), sourceCode);
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
let filtered = lintOnce(code);
|
|
1351
|
+
let relintedFromReport = false;
|
|
1352
|
+
if (fixMode) {
|
|
1353
|
+
let changed = false;
|
|
1354
|
+
for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
|
|
1355
|
+
const output = applyMessageFixes(code, filtered.messages);
|
|
1356
|
+
if (output === code) {
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
changed = true;
|
|
1360
|
+
code = output;
|
|
1361
|
+
filtered = lintOnce(code);
|
|
1362
|
+
}
|
|
1363
|
+
if (changed) {
|
|
1364
|
+
// Plugin fixes changed the text, so the native diagnostics for this
|
|
1365
|
+
// file describe stale source. Re-lint the fixed text through the whole
|
|
1366
|
+
// pipeline (native rules, plugin rules, further fixes) and replace this
|
|
1367
|
+
// file's diagnostics with the result.
|
|
1368
|
+
const depth = options.pluginFixDepth ?? 0;
|
|
1369
|
+
if (depth < MAX_AUTOFIX_PASSES) {
|
|
1370
|
+
const relinted = lintText(code, {
|
|
1371
|
+
...options,
|
|
1372
|
+
filePath: matchPath,
|
|
1373
|
+
filename: undefined,
|
|
1374
|
+
config: pluginConfigPathForFile(options, matchPath, perFileConfig) ?? options.config,
|
|
1375
|
+
fix: true,
|
|
1376
|
+
extraArgs: (options.extraArgs ?? []).filter((arg) => arg !== "--fix" && arg !== "--fix-dry-run"),
|
|
1377
|
+
useDefaultConfigTargets: undefined,
|
|
1378
|
+
pluginTextFilePath: undefined,
|
|
1379
|
+
pluginFixDepth: depth + 1
|
|
1380
|
+
});
|
|
1381
|
+
code = relinted.outputs?.find((fixed) => fixed.filePath === matchPath)?.output ?? code;
|
|
1382
|
+
filtered = {
|
|
1383
|
+
messages: relinted.diagnostics ?? [],
|
|
1384
|
+
suppressedMessages: relinted.suppressedDiagnostics ?? []
|
|
1385
|
+
};
|
|
1386
|
+
report.diagnostics = (report.diagnostics ?? []).filter((diagnostic) => diagnostic.filePath !== filePath);
|
|
1387
|
+
report.suppressedDiagnostics = (report.suppressedDiagnostics ?? []).filter((diagnostic) => diagnostic.filePath !== filePath);
|
|
1388
|
+
relintedFromReport = true;
|
|
1389
|
+
}
|
|
1390
|
+
if (fixMode === "write") {
|
|
1391
|
+
writeFileSync(absolutePath, code);
|
|
1392
|
+
} else if (outputIndex === -1) {
|
|
1393
|
+
report.outputs = [...(report.outputs ?? []), { filePath, output: code }];
|
|
1394
|
+
} else {
|
|
1395
|
+
report.outputs[outputIndex] = { ...report.outputs[outputIndex], output: code };
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (relintedFromReport) {
|
|
1401
|
+
// Already native-shaped diagnostics from the nested lint; only the
|
|
1402
|
+
// file path needs to match this report.
|
|
1403
|
+
report.diagnostics = [
|
|
1404
|
+
...(report.diagnostics ?? []),
|
|
1405
|
+
...filtered.messages.map((diagnostic) => ({ ...diagnostic, filePath }))
|
|
1406
|
+
];
|
|
1407
|
+
report.suppressedDiagnostics = [
|
|
1408
|
+
...(report.suppressedDiagnostics ?? []),
|
|
1409
|
+
...filtered.suppressedMessages.map((diagnostic) => ({ ...diagnostic, filePath }))
|
|
1410
|
+
];
|
|
1411
|
+
appended = true;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
report.diagnostics = [
|
|
1418
|
+
...(report.diagnostics ?? []),
|
|
1419
|
+
...filtered.messages.map((message) => pluginMessageToDiagnostic(message, filePath))
|
|
1420
|
+
];
|
|
1421
|
+
report.suppressedDiagnostics = [
|
|
1422
|
+
...(report.suppressedDiagnostics ?? []),
|
|
1423
|
+
...filtered.suppressedMessages.map((message) => ({
|
|
1424
|
+
...pluginMessageToDiagnostic(message, filePath),
|
|
1425
|
+
suppression: {
|
|
1426
|
+
kind: message.suppressions?.[0]?.kind ?? "directive",
|
|
1427
|
+
justification: message.suppressions?.[0]?.justification ?? ""
|
|
1428
|
+
}
|
|
1429
|
+
}))
|
|
1430
|
+
];
|
|
1431
|
+
appended = true;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (appended) {
|
|
1435
|
+
sortReportDiagnostics(report);
|
|
1436
|
+
}
|
|
1437
|
+
return report;
|
|
1247
1438
|
}
|
|
1248
1439
|
|
|
1249
1440
|
function nativeFlatConfigOptions(options) {
|
|
@@ -1273,6 +1464,10 @@ function nativeFlatConfigOptions(options) {
|
|
|
1273
1464
|
return undefined;
|
|
1274
1465
|
}
|
|
1275
1466
|
|
|
1467
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(config);
|
|
1468
|
+
if (jsPluginRuleIds.size > 0) {
|
|
1469
|
+
config = configWithoutCustomRules(config, jsPluginRuleIds);
|
|
1470
|
+
}
|
|
1276
1471
|
if (configCanUseNativeRulesFastPath(config)) {
|
|
1277
1472
|
return undefined;
|
|
1278
1473
|
}
|
|
@@ -1286,11 +1481,28 @@ function nativeFlatConfigOptions(options) {
|
|
|
1286
1481
|
};
|
|
1287
1482
|
}
|
|
1288
1483
|
|
|
1484
|
+
// Globals declared by a config entry, merged the way ESLint does: flat
|
|
1485
|
+
// `languageOptions.globals` wins over eslintrc-style `globals`.
|
|
1486
|
+
function nativeConfigGlobals(config) {
|
|
1487
|
+
if (!config || typeof config !== "object") {
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1490
|
+
const globals = {
|
|
1491
|
+
...(isGlobalsObject(config.globals) ? config.globals : {}),
|
|
1492
|
+
...(isGlobalsObject(config.languageOptions?.globals) ? config.languageOptions.globals : {})
|
|
1493
|
+
};
|
|
1494
|
+
return Object.keys(globals).length > 0 ? globals : undefined;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function isGlobalsObject(value) {
|
|
1498
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1289
1501
|
function configCanUseNativeRulesFastPath(config) {
|
|
1290
1502
|
const entries = Array.isArray(config) ? config : [config];
|
|
1291
1503
|
let hasEnabledRule = false;
|
|
1292
1504
|
for (const entry of entries) {
|
|
1293
|
-
if (!entry || typeof entry !== "object" || Object.keys(entry.settings ?? {}).length > 0) {
|
|
1505
|
+
if (!entry || typeof entry !== "object" || Object.keys(entry.settings ?? {}).length > 0 || nativeConfigGlobals(entry)) {
|
|
1294
1506
|
return false;
|
|
1295
1507
|
}
|
|
1296
1508
|
for (const ruleConfig of Object.values(entry.rules ?? {})) {
|
|
@@ -1313,11 +1525,16 @@ function nativeSerializableConfig(config) {
|
|
|
1313
1525
|
if (!config || typeof config !== "object") {
|
|
1314
1526
|
return config;
|
|
1315
1527
|
}
|
|
1316
|
-
|
|
1528
|
+
const serialized = Object.fromEntries(
|
|
1317
1529
|
["$schema", "name", "files", "ignores", "rules", "settings"]
|
|
1318
1530
|
.filter((key) => config[key] !== undefined)
|
|
1319
1531
|
.map((key) => [key, config[key]])
|
|
1320
1532
|
);
|
|
1533
|
+
const globals = nativeConfigGlobals(config);
|
|
1534
|
+
if (globals) {
|
|
1535
|
+
serialized.languageOptions = { globals };
|
|
1536
|
+
}
|
|
1537
|
+
return serialized;
|
|
1321
1538
|
}
|
|
1322
1539
|
|
|
1323
1540
|
function expandNativeConfigRunPaths(paths, options) {
|
|
@@ -1396,12 +1613,16 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
1396
1613
|
const calculated = configured ? calculatedConfig({ ...options, rules: undefined }, matchPath) : {};
|
|
1397
1614
|
let rules = calculated.rules;
|
|
1398
1615
|
const settings = calculated.settings;
|
|
1616
|
+
const globals = configured ? nativeConfigGlobals(calculated) : undefined;
|
|
1399
1617
|
if (configured && options.rules) {
|
|
1400
1618
|
rules = selectedRulesWithConfigOptions(rules, options.rules);
|
|
1401
1619
|
}
|
|
1402
|
-
|
|
1620
|
+
if (configured) {
|
|
1621
|
+
rules = withoutJsPluginRules(rules, jsPluginRuleIdsForOptions(options, matchPath));
|
|
1622
|
+
}
|
|
1623
|
+
const signature = configured ? stableConfigSignature({ rules, settings, globals }) : "<native-defaults>";
|
|
1403
1624
|
if (!groups.has(signature)) {
|
|
1404
|
-
groups.set(signature, { paths: [], rules, settings, configured });
|
|
1625
|
+
groups.set(signature, { paths: [], rules, settings, globals, configured });
|
|
1405
1626
|
}
|
|
1406
1627
|
groups.get(signature).paths.push(filePath);
|
|
1407
1628
|
}
|
|
@@ -1424,7 +1645,7 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
1424
1645
|
};
|
|
1425
1646
|
}
|
|
1426
1647
|
|
|
1427
|
-
const selectedRules = severityOnlyNativeRuleNames(group.rules, group.settings);
|
|
1648
|
+
const selectedRules = severityOnlyNativeRuleNames(group.rules, group.settings, group.globals);
|
|
1428
1649
|
if (selectedRules) {
|
|
1429
1650
|
return {
|
|
1430
1651
|
paths: group.paths,
|
|
@@ -1449,7 +1670,8 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
1449
1670
|
baseConfig: undefined,
|
|
1450
1671
|
overrideConfig: {
|
|
1451
1672
|
rules: allDisabledNativeRules(group.rules),
|
|
1452
|
-
...(group.settings ? { settings: group.settings } : {})
|
|
1673
|
+
...(group.settings ? { settings: group.settings } : {}),
|
|
1674
|
+
...(group.globals ? { languageOptions: { globals: group.globals } } : {})
|
|
1453
1675
|
},
|
|
1454
1676
|
forceMaterializedConfig: true
|
|
1455
1677
|
}
|
|
@@ -1457,8 +1679,8 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
1457
1679
|
});
|
|
1458
1680
|
}
|
|
1459
1681
|
|
|
1460
|
-
function severityOnlyNativeRuleNames(rules, settings) {
|
|
1461
|
-
if (Object.keys(settings ?? {}).length > 0) {
|
|
1682
|
+
function severityOnlyNativeRuleNames(rules, settings, globals) {
|
|
1683
|
+
if (globals || Object.keys(settings ?? {}).length > 0) {
|
|
1462
1684
|
return undefined;
|
|
1463
1685
|
}
|
|
1464
1686
|
|
|
@@ -1571,7 +1793,8 @@ function lintText(code, options = {}) {
|
|
|
1571
1793
|
cwd: options.cwd,
|
|
1572
1794
|
config: options.config ?? discoveredConfig,
|
|
1573
1795
|
noConfig: options.noConfig,
|
|
1574
|
-
deferDiagnosticConfigFiltering: true
|
|
1796
|
+
deferDiagnosticConfigFiltering: true,
|
|
1797
|
+
pluginTextFilePath: requestedPath
|
|
1575
1798
|
});
|
|
1576
1799
|
report.filePaths = (report.filePaths ?? []).map((filePath) => (filePath === tempFile ? requestedPath : filePath));
|
|
1577
1800
|
report.outputs = (report.outputs ?? []).map((fixed) => ({
|
|
@@ -1613,11 +1836,12 @@ function buildNativeLintArgs(paths, options) {
|
|
|
1613
1836
|
const configForRuleSelection = options.noConfig
|
|
1614
1837
|
? undefined
|
|
1615
1838
|
: options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
|
|
1839
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(configForRuleSelection, options.baseConfig, options.overrideConfig);
|
|
1616
1840
|
const enabledRules = enabledRuleNamesFromConfigs(
|
|
1617
1841
|
configForRuleSelection,
|
|
1618
1842
|
options.baseConfig,
|
|
1619
1843
|
options.overrideConfig
|
|
1620
|
-
);
|
|
1844
|
+
).filter((rule) => !jsPluginRuleIds.has(rule));
|
|
1621
1845
|
if (enabledRules.length > 0) {
|
|
1622
1846
|
cliArgs.push(`--rules=${enabledRules.join(",")}`);
|
|
1623
1847
|
}
|
|
@@ -1780,6 +2004,10 @@ function mergeConfigData(base, override) {
|
|
|
1780
2004
|
}
|
|
1781
2005
|
if (base.languageOptions || override.languageOptions) {
|
|
1782
2006
|
result.languageOptions = { ...(base.languageOptions ?? {}), ...(override.languageOptions ?? {}) };
|
|
2007
|
+
// ESLint merges `languageOptions.globals` per name across config objects.
|
|
2008
|
+
if (isGlobalsObject(base.languageOptions?.globals) && isGlobalsObject(override.languageOptions?.globals)) {
|
|
2009
|
+
result.languageOptions.globals = { ...base.languageOptions.globals, ...override.languageOptions.globals };
|
|
2010
|
+
}
|
|
1783
2011
|
}
|
|
1784
2012
|
if (base.parserOptions || override.parserOptions) {
|
|
1785
2013
|
result.parserOptions = { ...(base.parserOptions ?? {}), ...(override.parserOptions ?? {}) };
|
|
@@ -2075,7 +2303,7 @@ function withTemporaryConfig(options, callback) {
|
|
|
2075
2303
|
const tmp = mkdtempSync(join(tmpdir(), "utoo-lint-flat-config-"));
|
|
2076
2304
|
const configPath = join(tmp, "utlint.config.json");
|
|
2077
2305
|
try {
|
|
2078
|
-
writeFileSync(configPath, JSON.stringify(options.nativeFlatConfigData));
|
|
2306
|
+
writeFileSync(configPath, JSON.stringify(nativeFlatConfigEntries(options.nativeFlatConfigData)));
|
|
2079
2307
|
return callback({
|
|
2080
2308
|
...options,
|
|
2081
2309
|
config: configPath,
|
|
@@ -2095,9 +2323,11 @@ function withTemporaryConfig(options, callback) {
|
|
|
2095
2323
|
? readConfig(fileConfigPath, options.cwd, options.configCache)
|
|
2096
2324
|
: undefined;
|
|
2097
2325
|
const configs = [options.baseConfig, fileConfig, options.overrideConfig];
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
: runtimeRulesFromConfigs(...configs)
|
|
2326
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(...configs);
|
|
2327
|
+
const rules = withoutJsPluginRules(
|
|
2328
|
+
shouldMaterializeFileConfig ? materializedRulesFromConfigs(...configs) : runtimeRulesFromConfigs(...configs),
|
|
2329
|
+
jsPluginRuleIds
|
|
2330
|
+
);
|
|
2101
2331
|
const settings = configs.reduce(
|
|
2102
2332
|
(result, config) => ({
|
|
2103
2333
|
...result,
|
|
@@ -2106,7 +2336,15 @@ function withTemporaryConfig(options, callback) {
|
|
|
2106
2336
|
{}
|
|
2107
2337
|
);
|
|
2108
2338
|
const hasSettings = Object.keys(settings).length > 0;
|
|
2109
|
-
|
|
2339
|
+
const globals = configs.reduce(
|
|
2340
|
+
(result, config) => ({
|
|
2341
|
+
...result,
|
|
2342
|
+
...(nativeConfigGlobals(configDataFromConfig(config, options.filePath ?? options.filename, options.cwd)) ?? {})
|
|
2343
|
+
}),
|
|
2344
|
+
{}
|
|
2345
|
+
);
|
|
2346
|
+
const hasGlobals = Object.keys(globals).length > 0;
|
|
2347
|
+
if (Object.keys(rules).length === 0 && !hasSettings && !hasGlobals) {
|
|
2110
2348
|
if (shouldMaterializeFileConfig) {
|
|
2111
2349
|
return callback({
|
|
2112
2350
|
...options,
|
|
@@ -2116,9 +2354,9 @@ function withTemporaryConfig(options, callback) {
|
|
|
2116
2354
|
}
|
|
2117
2355
|
return callback(options);
|
|
2118
2356
|
}
|
|
2119
|
-
const enabledRules = enabledRuleNamesFromConfigs(...configs);
|
|
2357
|
+
const enabledRules = enabledRuleNamesFromConfigs(...configs).filter((rule) => !jsPluginRuleIds.has(rule));
|
|
2120
2358
|
const hasExplicitOffRules = Object.values(rules).some((value) => ruleConfigSeverity(value) === 0);
|
|
2121
|
-
if (!shouldMaterializeFileConfig && !hasSettings && !hasRuleOptions(rules) && !hasExplicitOffRules && !options.forceMaterializedConfig) {
|
|
2359
|
+
if (!shouldMaterializeFileConfig && !hasSettings && !hasGlobals && !hasRuleOptions(rules) && !hasExplicitOffRules && !options.forceMaterializedConfig) {
|
|
2122
2360
|
return callback({
|
|
2123
2361
|
...options,
|
|
2124
2362
|
config: shouldMaterializeFileConfig ? undefined : options.config,
|
|
@@ -2130,7 +2368,11 @@ function withTemporaryConfig(options, callback) {
|
|
|
2130
2368
|
const tmp = mkdtempSync(join(tmpdir(), "utoo-lint-config-"));
|
|
2131
2369
|
const configPath = join(tmp, "utlint.config.json");
|
|
2132
2370
|
try {
|
|
2133
|
-
writeFileSync(configPath, JSON.stringify({
|
|
2371
|
+
writeFileSync(configPath, JSON.stringify({
|
|
2372
|
+
rules: nativeRules(rules),
|
|
2373
|
+
...(hasSettings ? { settings } : {}),
|
|
2374
|
+
...(hasGlobals ? { languageOptions: { globals } } : {})
|
|
2375
|
+
}));
|
|
2134
2376
|
return callback({
|
|
2135
2377
|
...options,
|
|
2136
2378
|
config: shouldMaterializeFileConfig ? configPath : options.config ?? configPath,
|
|
@@ -2245,14 +2487,14 @@ class Linter {
|
|
|
2245
2487
|
const verifyOptions = typeof options === "string" ? { filename: options } : { ...options };
|
|
2246
2488
|
const filePath = verifyOptions.filename ?? verifyOptions.filePath ?? "input.js";
|
|
2247
2489
|
const normalizedFilePath = normalizeESLintFilePath(filePath, verifyOptions.cwd);
|
|
2490
|
+
const customRuleMap = customRuleMapForConfig(config, this.rules, normalizedFilePath, verifyOptions.cwd);
|
|
2491
|
+
const customRuleFilterMap = customRuleMapForConfig(config, this.rules);
|
|
2492
|
+
const customRules = customRuleEntriesForConfig(config, customRuleMap, normalizedFilePath, verifyOptions.cwd);
|
|
2248
2493
|
const sourceCode = createLinterSourceCode(
|
|
2249
2494
|
code,
|
|
2250
2495
|
parserForConfig(config, this.parsers, normalizedFilePath, verifyOptions.cwd),
|
|
2251
|
-
parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd)
|
|
2496
|
+
{ ...parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd), requireParser: customRules.length > 0 }
|
|
2252
2497
|
);
|
|
2253
|
-
const customRuleMap = customRuleMapForConfig(config, this.rules, normalizedFilePath, verifyOptions.cwd);
|
|
2254
|
-
const customRuleFilterMap = customRuleMapForConfig(config, this.rules);
|
|
2255
|
-
const customRules = customRuleEntriesForConfig(config, customRuleMap, normalizedFilePath, verifyOptions.cwd);
|
|
2256
2498
|
const nativeConfig = configWithoutCustomRules(config, customRuleFilterMap);
|
|
2257
2499
|
const lintOptions = {
|
|
2258
2500
|
cwd: verifyOptions.cwd,
|
|
@@ -2282,10 +2524,10 @@ class Linter {
|
|
|
2282
2524
|
];
|
|
2283
2525
|
this.times = { passes: [] };
|
|
2284
2526
|
this.fixPassCount = 0;
|
|
2285
|
-
return [
|
|
2527
|
+
return sortLintMessages([
|
|
2286
2528
|
...nativeRuleFilter.messages,
|
|
2287
2529
|
...customRuleFilter.messages
|
|
2288
|
-
];
|
|
2530
|
+
]);
|
|
2289
2531
|
}
|
|
2290
2532
|
|
|
2291
2533
|
verifyAndFix(code, config = {}, options = {}) {
|
|
@@ -2294,7 +2536,7 @@ class Linter {
|
|
|
2294
2536
|
let fixPassCount = 0;
|
|
2295
2537
|
for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
|
|
2296
2538
|
const messages = this.verify(output, config, options);
|
|
2297
|
-
const fixedOutput =
|
|
2539
|
+
const fixedOutput = applyMessageFixes(output, messages);
|
|
2298
2540
|
if (fixedOutput === output) {
|
|
2299
2541
|
this.fixPassCount = fixPassCount;
|
|
2300
2542
|
return {
|
|
@@ -2377,9 +2619,17 @@ function parserForConfig(config, parsers, filePath, cwd) {
|
|
|
2377
2619
|
|
|
2378
2620
|
function parserOptionsForConfig(config, filePath, cwd) {
|
|
2379
2621
|
const configData = configDataFromConfig(config, filePath, cwd);
|
|
2380
|
-
|
|
2622
|
+
const languageOptions = configData.languageOptions ?? {};
|
|
2623
|
+
const parserOptions = {
|
|
2381
2624
|
...(configData.parserOptions ?? {}),
|
|
2382
|
-
...(
|
|
2625
|
+
...(languageOptions.parserOptions ?? {})
|
|
2626
|
+
};
|
|
2627
|
+
return {
|
|
2628
|
+
...parserOptions,
|
|
2629
|
+
sourceType: languageOptions.sourceType ?? parserOptions.sourceType,
|
|
2630
|
+
ecmaVersion: languageOptions.ecmaVersion ?? parserOptions.ecmaVersion,
|
|
2631
|
+
ecmaFeatures: parserOptions.ecmaFeatures ?? {},
|
|
2632
|
+
globals: { ...(configData.globals ?? {}), ...(languageOptions.globals ?? {}) },
|
|
2383
2633
|
filename: filePath,
|
|
2384
2634
|
filePath,
|
|
2385
2635
|
cwd
|
|
@@ -2425,13 +2675,176 @@ function pluginRulesForConfig(config, filePath, cwd) {
|
|
|
2425
2675
|
continue;
|
|
2426
2676
|
}
|
|
2427
2677
|
for (const [ruleName, rule] of Object.entries(plugin.rules)) {
|
|
2428
|
-
|
|
2678
|
+
const ruleId = `${pluginName}/${ruleName}`;
|
|
2679
|
+
// A native rule takes precedence over a plugin rule mounted under the
|
|
2680
|
+
// same id, so a config copied from ESLint keeps its native speed.
|
|
2681
|
+
if (NATIVE_RULE_ID_SET.has(ruleId)) {
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
rules.set(ruleId, rule);
|
|
2429
2685
|
}
|
|
2430
2686
|
}
|
|
2431
2687
|
}
|
|
2432
2688
|
return rules;
|
|
2433
2689
|
}
|
|
2434
2690
|
|
|
2691
|
+
const pluginRuleIdsByConfig = new WeakMap();
|
|
2692
|
+
|
|
2693
|
+
function jsPluginRuleIdsFromConfigs(...configs) {
|
|
2694
|
+
const ids = new Set();
|
|
2695
|
+
for (const config of configs) {
|
|
2696
|
+
if (!config || typeof config !== "object") {
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
let ruleIds = pluginRuleIdsByConfig.get(config);
|
|
2700
|
+
if (!ruleIds) {
|
|
2701
|
+
ruleIds = [...pluginRulesForConfig(config).keys()];
|
|
2702
|
+
pluginRuleIdsByConfig.set(config, ruleIds);
|
|
2703
|
+
}
|
|
2704
|
+
for (const ruleId of ruleIds) {
|
|
2705
|
+
ids.add(ruleId);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
return ids;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
function withoutJsPluginRules(rules, jsPluginRuleIds) {
|
|
2712
|
+
if (!rules || typeof rules !== "object" || jsPluginRuleIds.size === 0) {
|
|
2713
|
+
return rules;
|
|
2714
|
+
}
|
|
2715
|
+
const filtered = {};
|
|
2716
|
+
for (const [ruleId, ruleConfig] of Object.entries(rules)) {
|
|
2717
|
+
if (!jsPluginRuleIds.has(ruleId)) {
|
|
2718
|
+
filtered[ruleId] = ruleConfig;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
return filtered;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
function configDeclaresPlugins(config) {
|
|
2725
|
+
if (Array.isArray(config)) {
|
|
2726
|
+
return config.some((entry) => configDeclaresPlugins(entry));
|
|
2727
|
+
}
|
|
2728
|
+
return Boolean(
|
|
2729
|
+
config
|
|
2730
|
+
&& typeof config === "object"
|
|
2731
|
+
&& config.plugins
|
|
2732
|
+
&& typeof config.plugins === "object"
|
|
2733
|
+
&& !Array.isArray(config.plugins)
|
|
2734
|
+
&& Object.keys(config.plugins).length > 0
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// Cheap pre-check so runs without plugins skip the per-file plugin work. Every
|
|
2739
|
+
// config the native run consulted was read through `options.configCache`.
|
|
2740
|
+
function anyConfigDeclaresPlugins(options) {
|
|
2741
|
+
if (configDeclaresPlugins(options.baseConfig) || configDeclaresPlugins(options.overrideConfig)) {
|
|
2742
|
+
return true;
|
|
2743
|
+
}
|
|
2744
|
+
if (options.noConfig) {
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
let sawConfig = false;
|
|
2748
|
+
for (const config of options.configCache?.values() ?? []) {
|
|
2749
|
+
if (config && typeof config === "object") {
|
|
2750
|
+
sawConfig = true;
|
|
2751
|
+
if (configDeclaresPlugins(config)) {
|
|
2752
|
+
return true;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
if (sawConfig) {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
const configPath = configPathForOptions(options);
|
|
2760
|
+
return Boolean(configPath) && configDeclaresPlugins(readConfig(configPath, options.cwd, options.configCache));
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// The native engine receives the JSON form of a config module. Plugin objects
|
|
2764
|
+
// (and their `create()` functions) only survive an in-process import, so a
|
|
2765
|
+
// config module that mounts plugins is loaded again in this process.
|
|
2766
|
+
function liveConfigForPath(configPath, options) {
|
|
2767
|
+
const config = readConfig(configPath, options.cwd, options.configCache);
|
|
2768
|
+
if (!isExecutableConfigPath(configPath) || !configDeclaresPlugins(config)) {
|
|
2769
|
+
return config;
|
|
2770
|
+
}
|
|
2771
|
+
return configModule.readConfigModule(configPath, options.cwd);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function pluginConfigPathForFile(options, matchPath, perFileConfig = true) {
|
|
2775
|
+
if (options.noConfig) {
|
|
2776
|
+
return undefined;
|
|
2777
|
+
}
|
|
2778
|
+
return perFileConfig && matchPath ? configPathForFile(options, matchPath) : configPathForOptions(options);
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function pluginConfigSourcesForFile(options, matchPath, perFileConfig = true) {
|
|
2782
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2783
|
+
const sources = [];
|
|
2784
|
+
if (options.baseConfig) {
|
|
2785
|
+
sources.push({ config: options.baseConfig, cwd });
|
|
2786
|
+
}
|
|
2787
|
+
const configPath = pluginConfigPathForFile(options, matchPath, perFileConfig);
|
|
2788
|
+
if (configPath) {
|
|
2789
|
+
sources.push({ config: liveConfigForPath(configPath, options), cwd: dirname(configPath) });
|
|
2790
|
+
}
|
|
2791
|
+
if (options.overrideConfig) {
|
|
2792
|
+
sources.push({ config: options.overrideConfig, cwd });
|
|
2793
|
+
}
|
|
2794
|
+
return sources;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function jsPluginRuleIdsForOptions(options, matchPath) {
|
|
2798
|
+
const configs = [options.baseConfig, options.overrideConfig];
|
|
2799
|
+
if (!options.noConfig) {
|
|
2800
|
+
const configPath = matchPath ? configPathForFile(options, matchPath) : configPathForOptions(options);
|
|
2801
|
+
if (configPath) {
|
|
2802
|
+
configs.push(readConfig(configPath, options.cwd, options.configCache));
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
return jsPluginRuleIdsFromConfigs(...configs);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function jsPluginRuleMapForFile(options, matchPath, perFileConfig = true) {
|
|
2809
|
+
const rules = new Map();
|
|
2810
|
+
for (const { config, cwd } of pluginConfigSourcesForFile(options, matchPath, perFileConfig)) {
|
|
2811
|
+
for (const [ruleId, rule] of pluginRulesForConfig(config, matchPath, cwd)) {
|
|
2812
|
+
rules.set(ruleId, rule);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
return rules;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
function pluginRuleEntriesForFile(options, matchPath, ruleMap, perFileConfig = true) {
|
|
2819
|
+
const entries = new Map();
|
|
2820
|
+
for (const { config, cwd } of pluginConfigSourcesForFile(options, matchPath, perFileConfig)) {
|
|
2821
|
+
for (const [ruleId, ruleConfig] of Object.entries(rulesFromConfig(config, matchPath, cwd))) {
|
|
2822
|
+
if (!ruleMap.has(ruleId)) {
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
const severity = ruleConfigSeverity(ruleConfig);
|
|
2826
|
+
if (severity === 0) {
|
|
2827
|
+
entries.delete(ruleId);
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
entries.set(ruleId, {
|
|
2831
|
+
ruleId,
|
|
2832
|
+
rule: ruleMap.get(ruleId),
|
|
2833
|
+
severity,
|
|
2834
|
+
options: Array.isArray(ruleConfig) ? ruleConfig.slice(1) : []
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
if (options.rules) {
|
|
2839
|
+
// `--rules` disables everything first and then enables only the listed
|
|
2840
|
+
// rules, keeping configured severities and options like the native path.
|
|
2841
|
+
return Object.keys(rulesFromNativeRuleList(options.rules))
|
|
2842
|
+
.filter((ruleId) => ruleMap.has(ruleId))
|
|
2843
|
+
.map((ruleId) => entries.get(ruleId) ?? { ruleId, rule: ruleMap.get(ruleId), severity: 1, options: [] });
|
|
2844
|
+
}
|
|
2845
|
+
return [...entries.values()];
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2435
2848
|
function matchingConfigEntries(config, filePath = undefined, cwd = undefined) {
|
|
2436
2849
|
if (!config) {
|
|
2437
2850
|
return [];
|
|
@@ -2483,7 +2896,7 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
|
|
|
2483
2896
|
}
|
|
2484
2897
|
let result = results.find((item) => item.filePath === filePath);
|
|
2485
2898
|
const effectiveCode = typeof result?.output === "string" ? result.output : code;
|
|
2486
|
-
const sourceCode = createLinterSourceCode(effectiveCode);
|
|
2899
|
+
const sourceCode = createLinterSourceCode(effectiveCode, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
|
|
2487
2900
|
const messages = runCustomLinterRules(customRules, sourceCode, {
|
|
2488
2901
|
cwd: options.cwd,
|
|
2489
2902
|
filename: filePath,
|
|
@@ -2498,13 +2911,74 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
|
|
|
2498
2911
|
result = emptyESLintResult(filePath, effectiveCode);
|
|
2499
2912
|
results.push(result);
|
|
2500
2913
|
}
|
|
2501
|
-
result
|
|
2914
|
+
if (relintAfterCustomFixes(result, effectiveCode, filePath, customRules, config, rules, options)) {
|
|
2915
|
+
return results;
|
|
2916
|
+
}
|
|
2917
|
+
result.messages = sortLintMessages([...result.messages, ...filtered.messages]);
|
|
2502
2918
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
2503
2919
|
applyResultFixes(result, effectiveCode, options);
|
|
2504
2920
|
finalizeESLintResult(result);
|
|
2505
2921
|
return results;
|
|
2506
2922
|
}
|
|
2507
2923
|
|
|
2924
|
+
// When custom fixes change a file, the native messages computed earlier are
|
|
2925
|
+
// stale. Re-lint the fixed text with native rules and custom rules together
|
|
2926
|
+
// until neither changes it, then rebuild the result from that final pass.
|
|
2927
|
+
function relintAfterCustomFixes(result, code, filePath, customRules, config, rules, options) {
|
|
2928
|
+
if (!options.fix) {
|
|
2929
|
+
return false;
|
|
2930
|
+
}
|
|
2931
|
+
const nativeOptions = {
|
|
2932
|
+
...lintOptionsWithoutCustomRules(options, rules),
|
|
2933
|
+
filePath,
|
|
2934
|
+
filename: undefined,
|
|
2935
|
+
fix: true,
|
|
2936
|
+
useDefaultConfigTargets: undefined,
|
|
2937
|
+
pluginTextFilePath: undefined
|
|
2938
|
+
};
|
|
2939
|
+
const parserOptions = { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true };
|
|
2940
|
+
const customLintOptions = {
|
|
2941
|
+
cwd: options.cwd,
|
|
2942
|
+
filename: filePath,
|
|
2943
|
+
config: calculatedConfig({ cwd: options.cwd, noConfig: true, baseConfig: options.baseConfig, overrideConfig: options.overrideConfig }, filePath)
|
|
2944
|
+
};
|
|
2945
|
+
const lintCustom = (text) => {
|
|
2946
|
+
const sourceCode = createLinterSourceCode(text, null, parserOptions);
|
|
2947
|
+
return applyDisableDirectives(runCustomLinterRules(customRules, sourceCode, customLintOptions), sourceCode);
|
|
2948
|
+
};
|
|
2949
|
+
|
|
2950
|
+
let output = code;
|
|
2951
|
+
let custom = lintCustom(output);
|
|
2952
|
+
let fixedOutput = applyMessageFixes(output, custom.messages);
|
|
2953
|
+
if (fixedOutput === output) {
|
|
2954
|
+
return false;
|
|
2955
|
+
}
|
|
2956
|
+
let nativeReport;
|
|
2957
|
+
for (let pass = 0; pass < MAX_AUTOFIX_PASSES && fixedOutput !== output; pass += 1) {
|
|
2958
|
+
nativeReport = lintText(fixedOutput, nativeOptions);
|
|
2959
|
+
output = nativeReport.outputs?.find((fixed) => fixed.filePath === filePath)?.output ?? fixedOutput;
|
|
2960
|
+
custom = lintCustom(output);
|
|
2961
|
+
fixedOutput = applyMessageFixes(output, custom.messages);
|
|
2962
|
+
}
|
|
2963
|
+
if (output !== fixedOutput) {
|
|
2964
|
+
output = fixedOutput;
|
|
2965
|
+
nativeReport = lintText(output, nativeOptions);
|
|
2966
|
+
custom = lintCustom(output);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
const nativeResult = reportToESLintResults(nativeReport, {
|
|
2970
|
+
source: result.source,
|
|
2971
|
+
filePath,
|
|
2972
|
+
includeEmptyTextResult: true,
|
|
2973
|
+
ruleSeverityForFile: (path) => ruleSeverityMapForOptions(nativeOptions, path)
|
|
2974
|
+
})[0];
|
|
2975
|
+
result.messages = sortLintMessages([...(nativeResult?.messages ?? []), ...custom.messages]);
|
|
2976
|
+
result.suppressedMessages = [...(nativeResult?.suppressedMessages ?? []), ...custom.suppressedMessages];
|
|
2977
|
+
result.output = output;
|
|
2978
|
+
finalizeESLintResult(result);
|
|
2979
|
+
return true;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2508
2982
|
function appendCustomLintFileMessages(results, config, options) {
|
|
2509
2983
|
for (const result of results) {
|
|
2510
2984
|
const filePath = result.filePath;
|
|
@@ -2521,7 +2995,7 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
2521
2995
|
continue;
|
|
2522
2996
|
}
|
|
2523
2997
|
}
|
|
2524
|
-
const sourceCode = createLinterSourceCode(code);
|
|
2998
|
+
const sourceCode = createLinterSourceCode(code, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
|
|
2525
2999
|
const messages = runCustomLinterRules(customRules, sourceCode, {
|
|
2526
3000
|
cwd: options.cwd,
|
|
2527
3001
|
filename: filePath,
|
|
@@ -2531,7 +3005,10 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
2531
3005
|
if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
|
|
2532
3006
|
continue;
|
|
2533
3007
|
}
|
|
2534
|
-
result
|
|
3008
|
+
if (relintAfterCustomFixes(result, code, filePath, customRules, config, rules, options)) {
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
result.messages = sortLintMessages([...result.messages, ...filtered.messages]);
|
|
2535
3012
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
2536
3013
|
applyResultFixes(result, code, options);
|
|
2537
3014
|
finalizeESLintResult(result);
|
|
@@ -2539,11 +3016,22 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
2539
3016
|
return results;
|
|
2540
3017
|
}
|
|
2541
3018
|
|
|
3019
|
+
// ESLint orders a file's messages by position; native and plugin messages
|
|
3020
|
+
// arrive as two separate batches.
|
|
3021
|
+
function sortLintMessages(messages) {
|
|
3022
|
+
return messages
|
|
3023
|
+
.map((message, index) => ({ message, index }))
|
|
3024
|
+
.sort((left, right) => (left.message.line ?? 0) - (right.message.line ?? 0)
|
|
3025
|
+
|| (left.message.column ?? 0) - (right.message.column ?? 0)
|
|
3026
|
+
|| left.index - right.index)
|
|
3027
|
+
.map(({ message }) => message);
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2542
3030
|
function applyResultFixes(result, code, options) {
|
|
2543
3031
|
if (!options.fix) {
|
|
2544
3032
|
return;
|
|
2545
3033
|
}
|
|
2546
|
-
const output =
|
|
3034
|
+
const output = applyMessageFixes(code, result.messages);
|
|
2547
3035
|
if (output !== code) {
|
|
2548
3036
|
result.output = output;
|
|
2549
3037
|
}
|
|
@@ -2556,14 +3044,39 @@ function runCustomLinterRules(ruleEntries, sourceCode, options) {
|
|
|
2556
3044
|
|
|
2557
3045
|
const messages = [];
|
|
2558
3046
|
const program = sourceCode.ast ?? { type: "Program", range: [0, sourceCode.text.length] };
|
|
3047
|
+
const hasAst = Array.isArray(program.body);
|
|
3048
|
+
const listenersBySelector = new Map();
|
|
2559
3049
|
for (const ruleEntry of ruleEntries) {
|
|
3050
|
+
if (typeof ruleEntry.rule !== "function" && typeof ruleEntry.rule?.create !== "function") {
|
|
3051
|
+
throw new Error(
|
|
3052
|
+
`utoo-lint: rule "${ruleEntry.ruleId}" has no create() function. ` +
|
|
3053
|
+
"ESLint plugins must be mounted from a utlint.config.ts (or .js) module so their rule objects are available at lint time."
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
2560
3056
|
const context = createCustomRuleContext(ruleEntry, sourceCode, options, messages);
|
|
2561
3057
|
const listeners = customRuleListeners(ruleEntry.rule, context);
|
|
2562
|
-
if (
|
|
2563
|
-
traverseCustomRuleAst(program, listeners, context, sourceCode.visitorKeys);
|
|
2564
|
-
} else {
|
|
3058
|
+
if (!hasAst) {
|
|
2565
3059
|
runCustomRuleTokenListeners(program, sourceCode.tokens, listeners, context);
|
|
3060
|
+
continue;
|
|
2566
3061
|
}
|
|
3062
|
+
for (const [selector, listener] of Object.entries(listeners)) {
|
|
3063
|
+
if (typeof listener !== "function") {
|
|
3064
|
+
continue;
|
|
3065
|
+
}
|
|
3066
|
+
if (!listenersBySelector.has(selector)) {
|
|
3067
|
+
listenersBySelector.set(selector, []);
|
|
3068
|
+
}
|
|
3069
|
+
listenersBySelector.get(selector).push({
|
|
3070
|
+
ruleId: ruleEntry.ruleId,
|
|
3071
|
+
listener(node) {
|
|
3072
|
+
context.setCurrentNode(node);
|
|
3073
|
+
listener(node);
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
if (hasAst && listenersBySelector.size > 0) {
|
|
3079
|
+
pluginRunner.runRuleListeners(program, listenersBySelector, { visitorKeys: sourceCode.visitorKeys });
|
|
2567
3080
|
}
|
|
2568
3081
|
return messages;
|
|
2569
3082
|
}
|
|
@@ -2720,231 +3233,6 @@ function disableDirectiveMatchesRule(directive, ruleId) {
|
|
|
2720
3233
|
return directive.ruleId == null || directive.ruleId === ruleId;
|
|
2721
3234
|
}
|
|
2722
3235
|
|
|
2723
|
-
function traverseCustomRuleAst(node, listeners, context, visitorKeys, seen = new Set(), ancestors = [], siblings = {}) {
|
|
2724
|
-
if (!node || typeof node !== "object" || typeof node.type !== "string" || seen.has(node)) {
|
|
2725
|
-
return;
|
|
2726
|
-
}
|
|
2727
|
-
seen.add(node);
|
|
2728
|
-
for (const listener of customRuleMatchingListeners(listeners, node, false, ancestors, siblings, visitorKeys)) {
|
|
2729
|
-
context.setCurrentNode(node);
|
|
2730
|
-
listener(node);
|
|
2731
|
-
}
|
|
2732
|
-
const children = customRuleChildNodes(node, visitorKeys);
|
|
2733
|
-
for (const [index, child] of children.entries()) {
|
|
2734
|
-
traverseCustomRuleAst(child, listeners, context, visitorKeys, seen, [...ancestors, node], {
|
|
2735
|
-
previous: children[index - 1] ?? null,
|
|
2736
|
-
previousAll: children.slice(0, index)
|
|
2737
|
-
});
|
|
2738
|
-
}
|
|
2739
|
-
for (const listener of customRuleMatchingListeners(listeners, node, true, ancestors, siblings, visitorKeys)) {
|
|
2740
|
-
context.setCurrentNode(node);
|
|
2741
|
-
listener(node);
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
function customRuleMatchingListeners(listeners, node, exit, ancestors = [], siblings = {}, visitorKeys = null) {
|
|
2746
|
-
const matches = [];
|
|
2747
|
-
for (const [selector, listener] of Object.entries(listeners)) {
|
|
2748
|
-
if (typeof listener !== "function" || !customRuleSelectorMatches(selector, node, exit, ancestors, siblings, visitorKeys)) {
|
|
2749
|
-
continue;
|
|
2750
|
-
}
|
|
2751
|
-
matches.push(listener);
|
|
2752
|
-
}
|
|
2753
|
-
return matches;
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
function customRuleSelectorMatches(selector, node, exit, ancestors = [], siblings = {}, visitorKeys = null) {
|
|
2757
|
-
const suffix = ":exit";
|
|
2758
|
-
const isExit = selector.endsWith(suffix);
|
|
2759
|
-
if (isExit !== exit) {
|
|
2760
|
-
return false;
|
|
2761
|
-
}
|
|
2762
|
-
const expression = isExit ? selector.slice(0, -suffix.length) : selector;
|
|
2763
|
-
const childSelector = customRuleSplitTopLevelSelector(expression, ">");
|
|
2764
|
-
if (childSelector) {
|
|
2765
|
-
const parent = ancestors.at(-1);
|
|
2766
|
-
return Boolean(parent)
|
|
2767
|
-
&& customRuleSimpleSelectorMatches(childSelector[0].trim(), parent, visitorKeys)
|
|
2768
|
-
&& customRuleSimpleSelectorMatches(childSelector[1].trim(), node, visitorKeys);
|
|
2769
|
-
}
|
|
2770
|
-
const adjacentSelector = customRuleSplitTopLevelSelector(expression, "+");
|
|
2771
|
-
if (adjacentSelector) {
|
|
2772
|
-
return Boolean(siblings.previous)
|
|
2773
|
-
&& customRuleSimpleSelectorMatches(adjacentSelector[0].trim(), siblings.previous, visitorKeys)
|
|
2774
|
-
&& customRuleSimpleSelectorMatches(adjacentSelector[1].trim(), node, visitorKeys);
|
|
2775
|
-
}
|
|
2776
|
-
const siblingSelector = customRuleSplitTopLevelSelector(expression, "~");
|
|
2777
|
-
if (siblingSelector) {
|
|
2778
|
-
return (siblings.previousAll ?? []).some((sibling) => customRuleSimpleSelectorMatches(siblingSelector[0].trim(), sibling, visitorKeys))
|
|
2779
|
-
&& customRuleSimpleSelectorMatches(siblingSelector[1].trim(), node, visitorKeys);
|
|
2780
|
-
}
|
|
2781
|
-
const descendantSelector = customRuleSplitTopLevelDescendantSelector(expression);
|
|
2782
|
-
if (descendantSelector) {
|
|
2783
|
-
return ancestors.some((ancestor) => customRuleSimpleSelectorMatches(descendantSelector[0].trim(), ancestor, visitorKeys))
|
|
2784
|
-
&& customRuleSimpleSelectorMatches(descendantSelector[1].trim(), node, visitorKeys);
|
|
2785
|
-
}
|
|
2786
|
-
return customRuleSimpleSelectorMatches(expression, node, visitorKeys);
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
function customRuleSimpleSelectorMatches(expression, node, visitorKeys = null) {
|
|
2790
|
-
const hasSelector = expression.match(/^(.+?):has\((.+)\)$/u);
|
|
2791
|
-
if (hasSelector) {
|
|
2792
|
-
return customRuleSimpleSelectorMatches(hasSelector[1].trim(), node, visitorKeys)
|
|
2793
|
-
&& customRuleDescendants(node, visitorKeys).some((descendant) => (
|
|
2794
|
-
customRuleSplitSelectorList(hasSelector[2]).some((selector) => customRuleSimpleSelectorMatches(selector.trim(), descendant, visitorKeys))
|
|
2795
|
-
));
|
|
2796
|
-
}
|
|
2797
|
-
const notSelector = expression.match(/^(.+?):not\((.+)\)$/u);
|
|
2798
|
-
if (notSelector) {
|
|
2799
|
-
return customRuleSimpleSelectorMatches(notSelector[1].trim(), node, visitorKeys)
|
|
2800
|
-
&& !customRuleSimpleSelectorMatches(notSelector[2].trim(), node, visitorKeys);
|
|
2801
|
-
}
|
|
2802
|
-
const matchesSelector = expression.match(/^(.*?):matches\((.+)\)$/u);
|
|
2803
|
-
if (matchesSelector) {
|
|
2804
|
-
const prefix = matchesSelector[1].trim();
|
|
2805
|
-
if (prefix && !customRuleSimpleSelectorMatches(prefix, node, visitorKeys)) {
|
|
2806
|
-
return false;
|
|
2807
|
-
}
|
|
2808
|
-
return customRuleSplitSelectorList(matchesSelector[2]).some((selector) => (
|
|
2809
|
-
customRuleSimpleSelectorMatches(selector.trim(), node, visitorKeys)
|
|
2810
|
-
));
|
|
2811
|
-
}
|
|
2812
|
-
if (expression === node.type) {
|
|
2813
|
-
return true;
|
|
2814
|
-
}
|
|
2815
|
-
const match = expression.match(/^([A-Za-z_$][\w$-]*|\*)?(?:\[([^\]]+)\])?$/u);
|
|
2816
|
-
if (!match) {
|
|
2817
|
-
return false;
|
|
2818
|
-
}
|
|
2819
|
-
const [, type = "*", attribute] = match;
|
|
2820
|
-
if (type !== "*" && type !== node.type) {
|
|
2821
|
-
return false;
|
|
2822
|
-
}
|
|
2823
|
-
return attribute ? customRuleAttributeSelectorMatches(node, attribute.trim()) : type === "*" || type === node.type;
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
function customRuleDescendants(node, visitorKeys, seen = new Set()) {
|
|
2827
|
-
const descendants = [];
|
|
2828
|
-
for (const child of customRuleChildNodes(node, visitorKeys)) {
|
|
2829
|
-
if (seen.has(child)) {
|
|
2830
|
-
continue;
|
|
2831
|
-
}
|
|
2832
|
-
seen.add(child);
|
|
2833
|
-
descendants.push(child, ...customRuleDescendants(child, visitorKeys, seen));
|
|
2834
|
-
}
|
|
2835
|
-
return descendants;
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
function customRuleSplitTopLevelSelector(expression, separator) {
|
|
2839
|
-
let state = customRuleSelectorScanState();
|
|
2840
|
-
for (let index = 0; index < expression.length; index += 1) {
|
|
2841
|
-
state = customRuleUpdateSelectorScanState(state, expression[index]);
|
|
2842
|
-
if (state.depth === 0 && !state.quote && expression[index] === separator) {
|
|
2843
|
-
return [expression.slice(0, index), expression.slice(index + 1)];
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
return null;
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
function customRuleSplitTopLevelDescendantSelector(expression) {
|
|
2850
|
-
let state = customRuleSelectorScanState();
|
|
2851
|
-
for (let index = 0; index < expression.length; index += 1) {
|
|
2852
|
-
state = customRuleUpdateSelectorScanState(state, expression[index]);
|
|
2853
|
-
if (state.depth === 0 && !state.quote && /\s/u.test(expression[index])) {
|
|
2854
|
-
const left = expression.slice(0, index).trim();
|
|
2855
|
-
const right = expression.slice(index).trim();
|
|
2856
|
-
return left && right ? [left, right] : null;
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
return null;
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
function customRuleSplitSelectorList(value) {
|
|
2863
|
-
const selectors = [];
|
|
2864
|
-
let state = customRuleSelectorScanState();
|
|
2865
|
-
let start = 0;
|
|
2866
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
2867
|
-
state = customRuleUpdateSelectorScanState(state, value[index]);
|
|
2868
|
-
if (state.depth === 0 && !state.quote && value[index] === ",") {
|
|
2869
|
-
selectors.push(value.slice(start, index));
|
|
2870
|
-
start = index + 1;
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
selectors.push(value.slice(start));
|
|
2874
|
-
return selectors.filter((selector) => selector.trim());
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
|
-
function customRuleSelectorScanState() {
|
|
2878
|
-
return { depth: 0, quote: null, escaped: false };
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
function customRuleUpdateSelectorScanState(state, char) {
|
|
2882
|
-
if (state.escaped) {
|
|
2883
|
-
return { ...state, escaped: false };
|
|
2884
|
-
}
|
|
2885
|
-
if (char === "\\") {
|
|
2886
|
-
return { ...state, escaped: true };
|
|
2887
|
-
}
|
|
2888
|
-
if (state.quote) {
|
|
2889
|
-
return char === state.quote ? { ...state, quote: null } : state;
|
|
2890
|
-
}
|
|
2891
|
-
if (char === "\"" || char === "'") {
|
|
2892
|
-
return { ...state, quote: char };
|
|
2893
|
-
}
|
|
2894
|
-
if (char === "[" || char === "(") {
|
|
2895
|
-
return { ...state, depth: state.depth + 1 };
|
|
2896
|
-
}
|
|
2897
|
-
if ((char === "]" || char === ")") && state.depth > 0) {
|
|
2898
|
-
return { ...state, depth: state.depth - 1 };
|
|
2899
|
-
}
|
|
2900
|
-
return state;
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
function customRuleAttributeSelectorMatches(node, attribute) {
|
|
2904
|
-
const match = attribute.match(/^([\w$.-]+)\s*(!=|=)\s*(.+)$/u);
|
|
2905
|
-
if (!match) {
|
|
2906
|
-
return customRuleValueByPath(node, attribute) != null;
|
|
2907
|
-
}
|
|
2908
|
-
const [, path, operator, rawExpected] = match;
|
|
2909
|
-
const actual = customRuleValueByPath(node, path);
|
|
2910
|
-
const expected = customRuleSelectorValue(rawExpected);
|
|
2911
|
-
return operator === "="
|
|
2912
|
-
? String(actual) === expected
|
|
2913
|
-
: String(actual) !== expected;
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
function customRuleSelectorValue(raw) {
|
|
2917
|
-
const value = raw.trim();
|
|
2918
|
-
const quote = value[0];
|
|
2919
|
-
if ((quote === "\"" || quote === "'") && value.at(-1) === quote) {
|
|
2920
|
-
return value.slice(1, -1);
|
|
2921
|
-
}
|
|
2922
|
-
return value;
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
function customRuleValueByPath(node, path) {
|
|
2926
|
-
return path.split(".").reduce((value, key) => (
|
|
2927
|
-
value && typeof value === "object" ? value[key] : undefined
|
|
2928
|
-
), node);
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
function customRuleChildNodes(node, visitorKeys) {
|
|
2932
|
-
const keys = Array.isArray(visitorKeys?.[node.type])
|
|
2933
|
-
? visitorKeys[node.type]
|
|
2934
|
-
: Object.keys(node).filter((key) => !AST_TRAVERSAL_SKIP_KEYS.has(key));
|
|
2935
|
-
const children = [];
|
|
2936
|
-
for (const key of keys) {
|
|
2937
|
-
const value = node[key];
|
|
2938
|
-
const values = Array.isArray(value) ? value : [value];
|
|
2939
|
-
for (const child of values) {
|
|
2940
|
-
if (child && typeof child === "object" && typeof child.type === "string") {
|
|
2941
|
-
children.push(child);
|
|
2942
|
-
}
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
return children;
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
3236
|
function runCustomRuleTokenListeners(program, tokens, listeners, context) {
|
|
2949
3237
|
if (typeof listeners.Program === "function") {
|
|
2950
3238
|
context.setCurrentNode(program);
|
|
@@ -2979,7 +3267,7 @@ function createCustomRuleContext(ruleEntry, sourceCode, options, messages) {
|
|
|
2979
3267
|
let currentNode = sourceCode.ast ?? null;
|
|
2980
3268
|
const context = {
|
|
2981
3269
|
id: ruleEntry.ruleId,
|
|
2982
|
-
options: ruleEntry.options,
|
|
3270
|
+
options: pluginRunner.applyRuleOptionDefaults(ruleEntry.rule, ruleEntry.options),
|
|
2983
3271
|
filename,
|
|
2984
3272
|
physicalFilename: filename,
|
|
2985
3273
|
cwd,
|
|
@@ -3197,7 +3485,7 @@ class SourceCode {
|
|
|
3197
3485
|
this.text = textOrConfig.text;
|
|
3198
3486
|
this.ast = textOrConfig.ast ?? null;
|
|
3199
3487
|
this.parserServices = textOrConfig.parserServices ?? {};
|
|
3200
|
-
this
|
|
3488
|
+
defineScopeManager(this, textOrConfig.scopeManager ?? null);
|
|
3201
3489
|
this.visitorKeys = textOrConfig.visitorKeys ?? null;
|
|
3202
3490
|
this.hasBOM = Boolean(textOrConfig.hasBOM);
|
|
3203
3491
|
} else {
|
|
@@ -3389,7 +3677,17 @@ class SourceCode {
|
|
|
3389
3677
|
}
|
|
3390
3678
|
|
|
3391
3679
|
getAncestors(node) {
|
|
3392
|
-
|
|
3680
|
+
if (!node) {
|
|
3681
|
+
return [];
|
|
3682
|
+
}
|
|
3683
|
+
if (node.parent !== undefined) {
|
|
3684
|
+
const ancestors = [];
|
|
3685
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
3686
|
+
ancestors.unshift(current);
|
|
3687
|
+
}
|
|
3688
|
+
return ancestors;
|
|
3689
|
+
}
|
|
3690
|
+
return sourceAncestorsForNode(this.ast, node) ?? [];
|
|
3393
3691
|
}
|
|
3394
3692
|
|
|
3395
3693
|
getDeclaredVariables(node) {
|
|
@@ -3397,10 +3695,20 @@ class SourceCode {
|
|
|
3397
3695
|
}
|
|
3398
3696
|
|
|
3399
3697
|
getScope(node) {
|
|
3400
|
-
|
|
3401
|
-
|
|
3698
|
+
const scopeManager = this.scopeManager;
|
|
3699
|
+
if (!scopeManager || typeof scopeManager.acquire !== "function") {
|
|
3700
|
+
return null;
|
|
3701
|
+
}
|
|
3702
|
+
if (node) {
|
|
3703
|
+
const inner = node.type !== "Program";
|
|
3704
|
+
for (let current = node; current; current = current.parent) {
|
|
3705
|
+
const scope = scopeManager.acquire(current, inner);
|
|
3706
|
+
if (scope) {
|
|
3707
|
+
return scope.type === "function-expression-name" ? scope.childScopes[0] : scope;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3402
3710
|
}
|
|
3403
|
-
return
|
|
3711
|
+
return scopeManager.scopes?.[0] ?? scopeManager.globalScope ?? null;
|
|
3404
3712
|
}
|
|
3405
3713
|
|
|
3406
3714
|
markVariableAsUsed(name, node) {
|
|
@@ -3436,6 +3744,31 @@ class SourceCode {
|
|
|
3436
3744
|
}
|
|
3437
3745
|
}
|
|
3438
3746
|
|
|
3747
|
+
function defineScopeManager(sourceCode, scopeManager) {
|
|
3748
|
+
if (typeof scopeManager !== "function") {
|
|
3749
|
+
sourceCode.scopeManager = scopeManager;
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
// Scope analysis is comparatively expensive; run it only when a rule asks.
|
|
3753
|
+
let computed = false;
|
|
3754
|
+
let value = null;
|
|
3755
|
+
Object.defineProperty(sourceCode, "scopeManager", {
|
|
3756
|
+
configurable: true,
|
|
3757
|
+
enumerable: true,
|
|
3758
|
+
get() {
|
|
3759
|
+
if (!computed) {
|
|
3760
|
+
computed = true;
|
|
3761
|
+
value = scopeManager() ?? null;
|
|
3762
|
+
}
|
|
3763
|
+
return value;
|
|
3764
|
+
},
|
|
3765
|
+
set(next) {
|
|
3766
|
+
computed = true;
|
|
3767
|
+
value = next;
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3439
3772
|
function sourceLineStartIndices(text) {
|
|
3440
3773
|
const indices = [0];
|
|
3441
3774
|
for (let index = 0; index < text.length; index += 1) {
|
|
@@ -3725,12 +4058,10 @@ class RuleTester {
|
|
|
3725
4058
|
RuleTester[testCase.only ? "itOnly" : "it"](testCase.name ?? testCase.code, () => {
|
|
3726
4059
|
const config = ruleTesterConfig(ruleName, this.config, testCase, "error");
|
|
3727
4060
|
const options = ruleTesterOptions(testCase);
|
|
3728
|
-
const
|
|
3729
|
-
? linter.verifyAndFix(testCase.code, config, options)
|
|
3730
|
-
: { messages: linter.verify(testCase.code, config, options), output: testCase.code };
|
|
3731
|
-
const messages = result.messages;
|
|
4061
|
+
const messages = linter.verify(testCase.code, config, options);
|
|
3732
4062
|
assertRuleTesterErrors(testCase, messages, rule);
|
|
3733
|
-
|
|
4063
|
+
// Like ESLint's RuleTester, `output` is the result of one autofix pass.
|
|
4064
|
+
assertRuleTesterOutput(testCase, Object.hasOwn(testCase, "output") ? applyMessageFixes(testCase.code, messages) : testCase.code);
|
|
3734
4065
|
});
|
|
3735
4066
|
}
|
|
3736
4067
|
});
|
|
@@ -3877,6 +4208,47 @@ function applyRuleFixes(code, fix) {
|
|
|
3877
4208
|
.reduce((output, item) => output.slice(0, item.range[0]) + item.text + output.slice(item.range[1]), code);
|
|
3878
4209
|
}
|
|
3879
4210
|
|
|
4211
|
+
// Applies one non-overlapping fix per message, like ESLint's SourceCodeFixer.
|
|
4212
|
+
// Fixes that overlap an earlier one are left for a later pass.
|
|
4213
|
+
function applyMessageFixes(code, messages) {
|
|
4214
|
+
const fixes = [];
|
|
4215
|
+
for (const message of messages) {
|
|
4216
|
+
const items = ruleFixItems(message?.fix)
|
|
4217
|
+
.slice()
|
|
4218
|
+
.sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
4219
|
+
if (items.length === 0) {
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
let text = "";
|
|
4223
|
+
let cursor = items[0].range[0];
|
|
4224
|
+
let valid = true;
|
|
4225
|
+
for (const item of items) {
|
|
4226
|
+
if (item.range[0] < cursor) {
|
|
4227
|
+
valid = false;
|
|
4228
|
+
break;
|
|
4229
|
+
}
|
|
4230
|
+
text += code.slice(cursor, item.range[0]) + item.text;
|
|
4231
|
+
cursor = item.range[1];
|
|
4232
|
+
}
|
|
4233
|
+
if (valid) {
|
|
4234
|
+
fixes.push({ range: [items[0].range[0], cursor], text });
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
fixes.sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
4238
|
+
let output = "";
|
|
4239
|
+
let position = 0;
|
|
4240
|
+
let lastEnd = -Infinity;
|
|
4241
|
+
for (const fix of fixes) {
|
|
4242
|
+
if (lastEnd >= fix.range[0]) {
|
|
4243
|
+
continue;
|
|
4244
|
+
}
|
|
4245
|
+
output += code.slice(position, fix.range[0]) + fix.text;
|
|
4246
|
+
position = fix.range[1];
|
|
4247
|
+
lastEnd = fix.range[1];
|
|
4248
|
+
}
|
|
4249
|
+
return output + code.slice(position);
|
|
4250
|
+
}
|
|
4251
|
+
|
|
3880
4252
|
function ruleFixItems(fix) {
|
|
3881
4253
|
if (!fix) {
|
|
3882
4254
|
return [];
|
|
@@ -3929,10 +4301,29 @@ function createLinterSourceCode(text, parser = null, parserOptions = {}) {
|
|
|
3929
4301
|
});
|
|
3930
4302
|
}
|
|
3931
4303
|
|
|
3932
|
-
function
|
|
3933
|
-
|
|
4304
|
+
function parseSourceCodeWithDefaultParser(text, parserOptions) {
|
|
4305
|
+
let parsed;
|
|
4306
|
+
try {
|
|
4307
|
+
parsed = estreeParser.parseForESLint(text, parserOptions);
|
|
4308
|
+
} catch (error) {
|
|
4309
|
+
if (parserOptions.requireParser) {
|
|
4310
|
+
throw error;
|
|
4311
|
+
}
|
|
3934
4312
|
return null;
|
|
3935
4313
|
}
|
|
4314
|
+
return new SourceCode({
|
|
4315
|
+
text,
|
|
4316
|
+
ast: parsed.ast,
|
|
4317
|
+
parserServices: parsed.services ?? {},
|
|
4318
|
+
scopeManager: parsed.scopeManager ?? null,
|
|
4319
|
+
visitorKeys: parsed.visitorKeys ?? null
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
function parseSourceCode(text, parser, parserOptions = {}) {
|
|
4324
|
+
if (!parser) {
|
|
4325
|
+
return parserOptions.tokensOnly ? null : parseSourceCodeWithDefaultParser(text, parserOptions);
|
|
4326
|
+
}
|
|
3936
4327
|
if (typeof parser.parseForESLint === "function") {
|
|
3937
4328
|
const result = parser.parseForESLint(text, parserOptions) ?? {};
|
|
3938
4329
|
return new SourceCode({
|
|
@@ -4198,7 +4589,7 @@ function applyCompatibilityDisableDirectives(result, textOptions) {
|
|
|
4198
4589
|
return;
|
|
4199
4590
|
}
|
|
4200
4591
|
|
|
4201
|
-
const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source));
|
|
4592
|
+
const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source, null, { tokensOnly: true }));
|
|
4202
4593
|
result.messages = filtered.messages;
|
|
4203
4594
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
4204
4595
|
}
|
|
@@ -4932,6 +5323,12 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
|
|
|
4932
5323
|
column: diagnostic.column,
|
|
4933
5324
|
nodeType: null
|
|
4934
5325
|
};
|
|
5326
|
+
if (typeof diagnostic.endLine === "number") {
|
|
5327
|
+
message.endLine = diagnostic.endLine;
|
|
5328
|
+
}
|
|
5329
|
+
if (typeof diagnostic.endColumn === "number") {
|
|
5330
|
+
message.endColumn = diagnostic.endColumn;
|
|
5331
|
+
}
|
|
4935
5332
|
if (diagnostic.endLine != null) {
|
|
4936
5333
|
message.endLine = diagnostic.endLine;
|
|
4937
5334
|
}
|