@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.js
CHANGED
|
@@ -7,11 +7,16 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
|
|
8
8
|
import { resolveBinary } from "./lib/binary.js";
|
|
9
9
|
import { createLegacyConfigResolver } from "./lib/legacy-config.cjs";
|
|
10
|
+
import { nativeFlatConfigEntries, nativeRules } from "./lib/native-rule-options.cjs";
|
|
10
11
|
import { formatESLintResults } from "./lib/stylish-formatter.cjs";
|
|
11
12
|
import {
|
|
12
13
|
findConfigPath as findConfigPathFromDirectory,
|
|
14
|
+
isExecutableConfigPath,
|
|
13
15
|
readConfig
|
|
14
16
|
} from "./lib/config-loader.js";
|
|
17
|
+
import configModule from "./lib/config-module.cjs";
|
|
18
|
+
import estreeParser from "./lib/estree-parser.cjs";
|
|
19
|
+
import pluginRunner from "./lib/plugin-runner.cjs";
|
|
15
20
|
|
|
16
21
|
export { platformPackageName, resolveBinary } from "./lib/binary.js";
|
|
17
22
|
|
|
@@ -28,10 +33,6 @@ const JS_KEYWORDS = new Set([
|
|
|
28
33
|
"instanceof", "let", "new", "return", "super", "switch", "this", "throw", "try", "typeof",
|
|
29
34
|
"var", "void", "while", "with", "yield"
|
|
30
35
|
]);
|
|
31
|
-
const AST_TRAVERSAL_SKIP_KEYS = new Set([
|
|
32
|
-
"type", "parent", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments",
|
|
33
|
-
"innerComments", "raw", "value", "name", "regex", "bigint", "errors"
|
|
34
|
-
]);
|
|
35
36
|
const RULE_TESTER_INITIAL_CONFIG = { rules: {} };
|
|
36
37
|
let ruleTesterDefaultConfig = { rules: {} };
|
|
37
38
|
let ruleTesterDescribe = null;
|
|
@@ -483,6 +484,7 @@ const NATIVE_ONLY_RULE_IDS = [
|
|
|
483
484
|
"semantic-errors"
|
|
484
485
|
];
|
|
485
486
|
const NATIVE_RULE_IDS = [...BUILTIN_RULE_IDS, ...NATIVE_ONLY_RULE_IDS];
|
|
487
|
+
const NATIVE_RULE_ID_SET = new Set(NATIVE_RULE_IDS);
|
|
486
488
|
const FIXABLE_BUILTIN_RULE_IDS = new Set([
|
|
487
489
|
"@typescript-eslint/no-explicit-any",
|
|
488
490
|
"jest/no-deprecated-functions",
|
|
@@ -610,15 +612,15 @@ export class UtooLint {
|
|
|
610
612
|
throw new Error("'results' must be an array");
|
|
611
613
|
}
|
|
612
614
|
const options = eslintConstructorOptions(this.options);
|
|
613
|
-
const customRuleConfig = [options.baseConfig, options.overrideConfig].filter(Boolean);
|
|
614
615
|
const rulesByFilePath = new Map();
|
|
616
|
+
const cachedOptions = withConfigCache(options);
|
|
615
617
|
return rulesMetaForResults(results, (ruleId, result) => {
|
|
616
|
-
if (
|
|
618
|
+
if (typeof result.filePath !== "string") {
|
|
617
619
|
return undefined;
|
|
618
620
|
}
|
|
619
621
|
const filePath = normalizeESLintFilePath(result.filePath, options.cwd);
|
|
620
622
|
if (!rulesByFilePath.has(filePath)) {
|
|
621
|
-
rulesByFilePath.set(filePath,
|
|
623
|
+
rulesByFilePath.set(filePath, jsPluginRuleMapForFile(cachedOptions, filePath));
|
|
622
624
|
}
|
|
623
625
|
return rulesByFilePath.get(filePath).get(ruleId)?.meta;
|
|
624
626
|
});
|
|
@@ -669,14 +671,14 @@ export class Linter {
|
|
|
669
671
|
const verifyOptions = typeof options === "string" ? { filename: options } : { ...options };
|
|
670
672
|
const filePath = verifyOptions.filename ?? verifyOptions.filePath ?? "input.js";
|
|
671
673
|
const normalizedFilePath = normalizeESLintFilePath(filePath, verifyOptions.cwd);
|
|
674
|
+
const customRuleMap = customRuleMapForConfig(config, this.rules, normalizedFilePath, verifyOptions.cwd);
|
|
675
|
+
const customRuleFilterMap = customRuleMapForConfig(config, this.rules);
|
|
676
|
+
const customRules = customRuleEntriesForConfig(config, customRuleMap, normalizedFilePath, verifyOptions.cwd);
|
|
672
677
|
const sourceCode = createLinterSourceCode(
|
|
673
678
|
code,
|
|
674
679
|
parserForConfig(config, this.parsers, normalizedFilePath, verifyOptions.cwd),
|
|
675
|
-
parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd)
|
|
680
|
+
{ ...parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd), requireParser: customRules.length > 0 }
|
|
676
681
|
);
|
|
677
|
-
const customRuleMap = customRuleMapForConfig(config, this.rules, normalizedFilePath, verifyOptions.cwd);
|
|
678
|
-
const customRuleFilterMap = customRuleMapForConfig(config, this.rules);
|
|
679
|
-
const customRules = customRuleEntriesForConfig(config, customRuleMap, normalizedFilePath, verifyOptions.cwd);
|
|
680
682
|
const nativeConfig = configWithoutCustomRules(config, customRuleFilterMap);
|
|
681
683
|
const lintOptions = {
|
|
682
684
|
cwd: verifyOptions.cwd,
|
|
@@ -706,10 +708,10 @@ export class Linter {
|
|
|
706
708
|
];
|
|
707
709
|
this.times = { passes: [] };
|
|
708
710
|
this.fixPassCount = 0;
|
|
709
|
-
return [
|
|
711
|
+
return sortLintMessages([
|
|
710
712
|
...nativeRuleFilter.messages,
|
|
711
713
|
...customRuleFilter.messages
|
|
712
|
-
];
|
|
714
|
+
]);
|
|
713
715
|
}
|
|
714
716
|
|
|
715
717
|
verifyAndFix(code, config = {}, options = {}) {
|
|
@@ -718,7 +720,7 @@ export class Linter {
|
|
|
718
720
|
let fixPassCount = 0;
|
|
719
721
|
for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
|
|
720
722
|
const messages = this.verify(output, config, options);
|
|
721
|
-
const fixedOutput =
|
|
723
|
+
const fixedOutput = applyMessageFixes(output, messages);
|
|
722
724
|
if (fixedOutput === output) {
|
|
723
725
|
this.fixPassCount = fixPassCount;
|
|
724
726
|
return {
|
|
@@ -801,9 +803,17 @@ function parserForConfig(config, parsers, filePath, cwd) {
|
|
|
801
803
|
|
|
802
804
|
function parserOptionsForConfig(config, filePath, cwd) {
|
|
803
805
|
const configData = configDataFromConfig(config, filePath, cwd);
|
|
804
|
-
|
|
806
|
+
const languageOptions = configData.languageOptions ?? {};
|
|
807
|
+
const parserOptions = {
|
|
805
808
|
...(configData.parserOptions ?? {}),
|
|
806
|
-
...(
|
|
809
|
+
...(languageOptions.parserOptions ?? {})
|
|
810
|
+
};
|
|
811
|
+
return {
|
|
812
|
+
...parserOptions,
|
|
813
|
+
sourceType: languageOptions.sourceType ?? parserOptions.sourceType,
|
|
814
|
+
ecmaVersion: languageOptions.ecmaVersion ?? parserOptions.ecmaVersion,
|
|
815
|
+
ecmaFeatures: parserOptions.ecmaFeatures ?? {},
|
|
816
|
+
globals: { ...(configData.globals ?? {}), ...(languageOptions.globals ?? {}) },
|
|
807
817
|
filename: filePath,
|
|
808
818
|
filePath,
|
|
809
819
|
cwd
|
|
@@ -849,13 +859,176 @@ function pluginRulesForConfig(config, filePath, cwd) {
|
|
|
849
859
|
continue;
|
|
850
860
|
}
|
|
851
861
|
for (const [ruleName, rule] of Object.entries(plugin.rules)) {
|
|
852
|
-
|
|
862
|
+
const ruleId = `${pluginName}/${ruleName}`;
|
|
863
|
+
// A native rule takes precedence over a plugin rule mounted under the
|
|
864
|
+
// same id, so a config copied from ESLint keeps its native speed.
|
|
865
|
+
if (NATIVE_RULE_ID_SET.has(ruleId)) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
rules.set(ruleId, rule);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return rules;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const pluginRuleIdsByConfig = new WeakMap();
|
|
876
|
+
|
|
877
|
+
function jsPluginRuleIdsFromConfigs(...configs) {
|
|
878
|
+
const ids = new Set();
|
|
879
|
+
for (const config of configs) {
|
|
880
|
+
if (!config || typeof config !== "object") {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
let ruleIds = pluginRuleIdsByConfig.get(config);
|
|
884
|
+
if (!ruleIds) {
|
|
885
|
+
ruleIds = [...pluginRulesForConfig(config).keys()];
|
|
886
|
+
pluginRuleIdsByConfig.set(config, ruleIds);
|
|
887
|
+
}
|
|
888
|
+
for (const ruleId of ruleIds) {
|
|
889
|
+
ids.add(ruleId);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return ids;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function withoutJsPluginRules(rules, jsPluginRuleIds) {
|
|
896
|
+
if (!rules || typeof rules !== "object" || jsPluginRuleIds.size === 0) {
|
|
897
|
+
return rules;
|
|
898
|
+
}
|
|
899
|
+
const filtered = {};
|
|
900
|
+
for (const [ruleId, ruleConfig] of Object.entries(rules)) {
|
|
901
|
+
if (!jsPluginRuleIds.has(ruleId)) {
|
|
902
|
+
filtered[ruleId] = ruleConfig;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return filtered;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function configDeclaresPlugins(config) {
|
|
909
|
+
if (Array.isArray(config)) {
|
|
910
|
+
return config.some((entry) => configDeclaresPlugins(entry));
|
|
911
|
+
}
|
|
912
|
+
return Boolean(
|
|
913
|
+
config
|
|
914
|
+
&& typeof config === "object"
|
|
915
|
+
&& config.plugins
|
|
916
|
+
&& typeof config.plugins === "object"
|
|
917
|
+
&& !Array.isArray(config.plugins)
|
|
918
|
+
&& Object.keys(config.plugins).length > 0
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Cheap pre-check so runs without plugins skip the per-file plugin work. Every
|
|
923
|
+
// config the native run consulted was read through `options.configCache`.
|
|
924
|
+
function anyConfigDeclaresPlugins(options) {
|
|
925
|
+
if (configDeclaresPlugins(options.baseConfig) || configDeclaresPlugins(options.overrideConfig)) {
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
if (options.noConfig) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
let sawConfig = false;
|
|
932
|
+
for (const config of options.configCache?.values() ?? []) {
|
|
933
|
+
if (config && typeof config === "object") {
|
|
934
|
+
sawConfig = true;
|
|
935
|
+
if (configDeclaresPlugins(config)) {
|
|
936
|
+
return true;
|
|
853
937
|
}
|
|
854
938
|
}
|
|
855
939
|
}
|
|
940
|
+
if (sawConfig) {
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
const configPath = configPathForOptions(options);
|
|
944
|
+
return Boolean(configPath) && configDeclaresPlugins(readConfig(configPath, options.cwd, options.configCache));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// The native engine receives the JSON form of a config module. Plugin objects
|
|
948
|
+
// (and their `create()` functions) only survive an in-process import, so a
|
|
949
|
+
// config module that mounts plugins is loaded again in this process.
|
|
950
|
+
function liveConfigForPath(configPath, options) {
|
|
951
|
+
const config = readConfig(configPath, options.cwd, options.configCache);
|
|
952
|
+
if (!isExecutableConfigPath(configPath) || !configDeclaresPlugins(config)) {
|
|
953
|
+
return config;
|
|
954
|
+
}
|
|
955
|
+
return configModule.readConfigModule(configPath, options.cwd);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function pluginConfigPathForFile(options, matchPath, perFileConfig = true) {
|
|
959
|
+
if (options.noConfig) {
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
return perFileConfig && matchPath ? configPathForFile(options, matchPath) : configPathForOptions(options);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function pluginConfigSourcesForFile(options, matchPath, perFileConfig = true) {
|
|
966
|
+
const cwd = options.cwd ?? process.cwd();
|
|
967
|
+
const sources = [];
|
|
968
|
+
if (options.baseConfig) {
|
|
969
|
+
sources.push({ config: options.baseConfig, cwd });
|
|
970
|
+
}
|
|
971
|
+
const configPath = pluginConfigPathForFile(options, matchPath, perFileConfig);
|
|
972
|
+
if (configPath) {
|
|
973
|
+
sources.push({ config: liveConfigForPath(configPath, options), cwd: dirname(configPath) });
|
|
974
|
+
}
|
|
975
|
+
if (options.overrideConfig) {
|
|
976
|
+
sources.push({ config: options.overrideConfig, cwd });
|
|
977
|
+
}
|
|
978
|
+
return sources;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function jsPluginRuleIdsForOptions(options, matchPath) {
|
|
982
|
+
const configs = [options.baseConfig, options.overrideConfig];
|
|
983
|
+
if (!options.noConfig) {
|
|
984
|
+
const configPath = matchPath ? configPathForFile(options, matchPath) : configPathForOptions(options);
|
|
985
|
+
if (configPath) {
|
|
986
|
+
configs.push(readConfig(configPath, options.cwd, options.configCache));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return jsPluginRuleIdsFromConfigs(...configs);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function jsPluginRuleMapForFile(options, matchPath, perFileConfig = true) {
|
|
993
|
+
const rules = new Map();
|
|
994
|
+
for (const { config, cwd } of pluginConfigSourcesForFile(options, matchPath, perFileConfig)) {
|
|
995
|
+
for (const [ruleId, rule] of pluginRulesForConfig(config, matchPath, cwd)) {
|
|
996
|
+
rules.set(ruleId, rule);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
856
999
|
return rules;
|
|
857
1000
|
}
|
|
858
1001
|
|
|
1002
|
+
function pluginRuleEntriesForFile(options, matchPath, ruleMap, perFileConfig = true) {
|
|
1003
|
+
const entries = new Map();
|
|
1004
|
+
for (const { config, cwd } of pluginConfigSourcesForFile(options, matchPath, perFileConfig)) {
|
|
1005
|
+
for (const [ruleId, ruleConfig] of Object.entries(rulesFromConfig(config, matchPath, cwd))) {
|
|
1006
|
+
if (!ruleMap.has(ruleId)) {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
const severity = ruleConfigSeverity(ruleConfig);
|
|
1010
|
+
if (severity === 0) {
|
|
1011
|
+
entries.delete(ruleId);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
entries.set(ruleId, {
|
|
1015
|
+
ruleId,
|
|
1016
|
+
rule: ruleMap.get(ruleId),
|
|
1017
|
+
severity,
|
|
1018
|
+
options: Array.isArray(ruleConfig) ? ruleConfig.slice(1) : []
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (options.rules) {
|
|
1023
|
+
// `--rules` disables everything first and then enables only the listed
|
|
1024
|
+
// rules, keeping configured severities and options like the native path.
|
|
1025
|
+
return Object.keys(rulesFromNativeRuleList(options.rules))
|
|
1026
|
+
.filter((ruleId) => ruleMap.has(ruleId))
|
|
1027
|
+
.map((ruleId) => entries.get(ruleId) ?? { ruleId, rule: ruleMap.get(ruleId), severity: 1, options: [] });
|
|
1028
|
+
}
|
|
1029
|
+
return [...entries.values()];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
859
1032
|
function matchingConfigEntries(config, filePath = undefined, cwd = undefined) {
|
|
860
1033
|
if (!config) {
|
|
861
1034
|
return [];
|
|
@@ -907,7 +1080,7 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
|
|
|
907
1080
|
}
|
|
908
1081
|
let result = results.find((item) => item.filePath === filePath);
|
|
909
1082
|
const effectiveCode = typeof result?.output === "string" ? result.output : code;
|
|
910
|
-
const sourceCode = createLinterSourceCode(effectiveCode);
|
|
1083
|
+
const sourceCode = createLinterSourceCode(effectiveCode, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
|
|
911
1084
|
const messages = runCustomLinterRules(customRules, sourceCode, {
|
|
912
1085
|
cwd: options.cwd,
|
|
913
1086
|
filename: filePath,
|
|
@@ -922,13 +1095,74 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
|
|
|
922
1095
|
result = emptyESLintResult(filePath, effectiveCode);
|
|
923
1096
|
results.push(result);
|
|
924
1097
|
}
|
|
925
|
-
result
|
|
1098
|
+
if (relintAfterCustomFixes(result, effectiveCode, filePath, customRules, config, rules, options)) {
|
|
1099
|
+
return results;
|
|
1100
|
+
}
|
|
1101
|
+
result.messages = sortLintMessages([...result.messages, ...filtered.messages]);
|
|
926
1102
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
927
1103
|
applyResultFixes(result, effectiveCode, options);
|
|
928
1104
|
finalizeESLintResult(result);
|
|
929
1105
|
return results;
|
|
930
1106
|
}
|
|
931
1107
|
|
|
1108
|
+
// When custom fixes change a file, the native messages computed earlier are
|
|
1109
|
+
// stale. Re-lint the fixed text with native rules and custom rules together
|
|
1110
|
+
// until neither changes it, then rebuild the result from that final pass.
|
|
1111
|
+
function relintAfterCustomFixes(result, code, filePath, customRules, config, rules, options) {
|
|
1112
|
+
if (!options.fix) {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
const nativeOptions = {
|
|
1116
|
+
...lintOptionsWithoutCustomRules(options, rules),
|
|
1117
|
+
filePath,
|
|
1118
|
+
filename: undefined,
|
|
1119
|
+
fix: true,
|
|
1120
|
+
useDefaultConfigTargets: undefined,
|
|
1121
|
+
pluginTextFilePath: undefined
|
|
1122
|
+
};
|
|
1123
|
+
const parserOptions = { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true };
|
|
1124
|
+
const customLintOptions = {
|
|
1125
|
+
cwd: options.cwd,
|
|
1126
|
+
filename: filePath,
|
|
1127
|
+
config: calculatedConfig({ cwd: options.cwd, noConfig: true, baseConfig: options.baseConfig, overrideConfig: options.overrideConfig }, filePath)
|
|
1128
|
+
};
|
|
1129
|
+
const lintCustom = (text) => {
|
|
1130
|
+
const sourceCode = createLinterSourceCode(text, null, parserOptions);
|
|
1131
|
+
return applyDisableDirectives(runCustomLinterRules(customRules, sourceCode, customLintOptions), sourceCode);
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
let output = code;
|
|
1135
|
+
let custom = lintCustom(output);
|
|
1136
|
+
let fixedOutput = applyMessageFixes(output, custom.messages);
|
|
1137
|
+
if (fixedOutput === output) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
let nativeReport;
|
|
1141
|
+
for (let pass = 0; pass < MAX_AUTOFIX_PASSES && fixedOutput !== output; pass += 1) {
|
|
1142
|
+
nativeReport = lintText(fixedOutput, nativeOptions);
|
|
1143
|
+
output = nativeReport.outputs?.find((fixed) => fixed.filePath === filePath)?.output ?? fixedOutput;
|
|
1144
|
+
custom = lintCustom(output);
|
|
1145
|
+
fixedOutput = applyMessageFixes(output, custom.messages);
|
|
1146
|
+
}
|
|
1147
|
+
if (output !== fixedOutput) {
|
|
1148
|
+
output = fixedOutput;
|
|
1149
|
+
nativeReport = lintText(output, nativeOptions);
|
|
1150
|
+
custom = lintCustom(output);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const nativeResult = reportToESLintResults(nativeReport, {
|
|
1154
|
+
source: result.source,
|
|
1155
|
+
filePath,
|
|
1156
|
+
includeEmptyTextResult: true,
|
|
1157
|
+
ruleSeverityForFile: (path) => ruleSeverityMapForOptions(nativeOptions, path)
|
|
1158
|
+
})[0];
|
|
1159
|
+
result.messages = sortLintMessages([...(nativeResult?.messages ?? []), ...custom.messages]);
|
|
1160
|
+
result.suppressedMessages = [...(nativeResult?.suppressedMessages ?? []), ...custom.suppressedMessages];
|
|
1161
|
+
result.output = output;
|
|
1162
|
+
finalizeESLintResult(result);
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
932
1166
|
function appendCustomLintFileMessages(results, config, options) {
|
|
933
1167
|
for (const result of results) {
|
|
934
1168
|
const filePath = result.filePath;
|
|
@@ -945,7 +1179,7 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
945
1179
|
continue;
|
|
946
1180
|
}
|
|
947
1181
|
}
|
|
948
|
-
const sourceCode = createLinterSourceCode(code);
|
|
1182
|
+
const sourceCode = createLinterSourceCode(code, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
|
|
949
1183
|
const messages = runCustomLinterRules(customRules, sourceCode, {
|
|
950
1184
|
cwd: options.cwd,
|
|
951
1185
|
filename: filePath,
|
|
@@ -955,7 +1189,10 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
955
1189
|
if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
|
|
956
1190
|
continue;
|
|
957
1191
|
}
|
|
958
|
-
result
|
|
1192
|
+
if (relintAfterCustomFixes(result, code, filePath, customRules, config, rules, options)) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
result.messages = sortLintMessages([...result.messages, ...filtered.messages]);
|
|
959
1196
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
960
1197
|
applyResultFixes(result, code, options);
|
|
961
1198
|
finalizeESLintResult(result);
|
|
@@ -963,11 +1200,22 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
963
1200
|
return results;
|
|
964
1201
|
}
|
|
965
1202
|
|
|
1203
|
+
// ESLint orders a file's messages by position; native and plugin messages
|
|
1204
|
+
// arrive as two separate batches.
|
|
1205
|
+
function sortLintMessages(messages) {
|
|
1206
|
+
return messages
|
|
1207
|
+
.map((message, index) => ({ message, index }))
|
|
1208
|
+
.sort((left, right) => (left.message.line ?? 0) - (right.message.line ?? 0)
|
|
1209
|
+
|| (left.message.column ?? 0) - (right.message.column ?? 0)
|
|
1210
|
+
|| left.index - right.index)
|
|
1211
|
+
.map(({ message }) => message);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
966
1214
|
function applyResultFixes(result, code, options) {
|
|
967
1215
|
if (!options.fix) {
|
|
968
1216
|
return;
|
|
969
1217
|
}
|
|
970
|
-
const output =
|
|
1218
|
+
const output = applyMessageFixes(code, result.messages);
|
|
971
1219
|
if (output !== code) {
|
|
972
1220
|
result.output = output;
|
|
973
1221
|
}
|
|
@@ -980,14 +1228,39 @@ function runCustomLinterRules(ruleEntries, sourceCode, options) {
|
|
|
980
1228
|
|
|
981
1229
|
const messages = [];
|
|
982
1230
|
const program = sourceCode.ast ?? { type: "Program", range: [0, sourceCode.text.length] };
|
|
1231
|
+
const hasAst = Array.isArray(program.body);
|
|
1232
|
+
const listenersBySelector = new Map();
|
|
983
1233
|
for (const ruleEntry of ruleEntries) {
|
|
1234
|
+
if (typeof ruleEntry.rule !== "function" && typeof ruleEntry.rule?.create !== "function") {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`utoo-lint: rule "${ruleEntry.ruleId}" has no create() function. ` +
|
|
1237
|
+
"ESLint plugins must be mounted from a utlint.config.ts (or .js) module so their rule objects are available at lint time."
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
984
1240
|
const context = createCustomRuleContext(ruleEntry, sourceCode, options, messages);
|
|
985
1241
|
const listeners = customRuleListeners(ruleEntry.rule, context);
|
|
986
|
-
if (
|
|
987
|
-
traverseCustomRuleAst(program, listeners, context, sourceCode.visitorKeys);
|
|
988
|
-
} else {
|
|
1242
|
+
if (!hasAst) {
|
|
989
1243
|
runCustomRuleTokenListeners(program, sourceCode.tokens, listeners, context);
|
|
1244
|
+
continue;
|
|
990
1245
|
}
|
|
1246
|
+
for (const [selector, listener] of Object.entries(listeners)) {
|
|
1247
|
+
if (typeof listener !== "function") {
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (!listenersBySelector.has(selector)) {
|
|
1251
|
+
listenersBySelector.set(selector, []);
|
|
1252
|
+
}
|
|
1253
|
+
listenersBySelector.get(selector).push({
|
|
1254
|
+
ruleId: ruleEntry.ruleId,
|
|
1255
|
+
listener(node) {
|
|
1256
|
+
context.setCurrentNode(node);
|
|
1257
|
+
listener(node);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (hasAst && listenersBySelector.size > 0) {
|
|
1263
|
+
pluginRunner.runRuleListeners(program, listenersBySelector, { visitorKeys: sourceCode.visitorKeys });
|
|
991
1264
|
}
|
|
992
1265
|
return messages;
|
|
993
1266
|
}
|
|
@@ -1144,231 +1417,6 @@ function disableDirectiveMatchesRule(directive, ruleId) {
|
|
|
1144
1417
|
return directive.ruleId == null || directive.ruleId === ruleId;
|
|
1145
1418
|
}
|
|
1146
1419
|
|
|
1147
|
-
function traverseCustomRuleAst(node, listeners, context, visitorKeys, seen = new Set(), ancestors = [], siblings = {}) {
|
|
1148
|
-
if (!node || typeof node !== "object" || typeof node.type !== "string" || seen.has(node)) {
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
seen.add(node);
|
|
1152
|
-
for (const listener of customRuleMatchingListeners(listeners, node, false, ancestors, siblings, visitorKeys)) {
|
|
1153
|
-
context.setCurrentNode(node);
|
|
1154
|
-
listener(node);
|
|
1155
|
-
}
|
|
1156
|
-
const children = customRuleChildNodes(node, visitorKeys);
|
|
1157
|
-
for (const [index, child] of children.entries()) {
|
|
1158
|
-
traverseCustomRuleAst(child, listeners, context, visitorKeys, seen, [...ancestors, node], {
|
|
1159
|
-
previous: children[index - 1] ?? null,
|
|
1160
|
-
previousAll: children.slice(0, index)
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
for (const listener of customRuleMatchingListeners(listeners, node, true, ancestors, siblings, visitorKeys)) {
|
|
1164
|
-
context.setCurrentNode(node);
|
|
1165
|
-
listener(node);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
function customRuleMatchingListeners(listeners, node, exit, ancestors = [], siblings = {}, visitorKeys = null) {
|
|
1170
|
-
const matches = [];
|
|
1171
|
-
for (const [selector, listener] of Object.entries(listeners)) {
|
|
1172
|
-
if (typeof listener !== "function" || !customRuleSelectorMatches(selector, node, exit, ancestors, siblings, visitorKeys)) {
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
matches.push(listener);
|
|
1176
|
-
}
|
|
1177
|
-
return matches;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
function customRuleSelectorMatches(selector, node, exit, ancestors = [], siblings = {}, visitorKeys = null) {
|
|
1181
|
-
const suffix = ":exit";
|
|
1182
|
-
const isExit = selector.endsWith(suffix);
|
|
1183
|
-
if (isExit !== exit) {
|
|
1184
|
-
return false;
|
|
1185
|
-
}
|
|
1186
|
-
const expression = isExit ? selector.slice(0, -suffix.length) : selector;
|
|
1187
|
-
const childSelector = customRuleSplitTopLevelSelector(expression, ">");
|
|
1188
|
-
if (childSelector) {
|
|
1189
|
-
const parent = ancestors.at(-1);
|
|
1190
|
-
return Boolean(parent)
|
|
1191
|
-
&& customRuleSimpleSelectorMatches(childSelector[0].trim(), parent, visitorKeys)
|
|
1192
|
-
&& customRuleSimpleSelectorMatches(childSelector[1].trim(), node, visitorKeys);
|
|
1193
|
-
}
|
|
1194
|
-
const adjacentSelector = customRuleSplitTopLevelSelector(expression, "+");
|
|
1195
|
-
if (adjacentSelector) {
|
|
1196
|
-
return Boolean(siblings.previous)
|
|
1197
|
-
&& customRuleSimpleSelectorMatches(adjacentSelector[0].trim(), siblings.previous, visitorKeys)
|
|
1198
|
-
&& customRuleSimpleSelectorMatches(adjacentSelector[1].trim(), node, visitorKeys);
|
|
1199
|
-
}
|
|
1200
|
-
const siblingSelector = customRuleSplitTopLevelSelector(expression, "~");
|
|
1201
|
-
if (siblingSelector) {
|
|
1202
|
-
return (siblings.previousAll ?? []).some((sibling) => customRuleSimpleSelectorMatches(siblingSelector[0].trim(), sibling, visitorKeys))
|
|
1203
|
-
&& customRuleSimpleSelectorMatches(siblingSelector[1].trim(), node, visitorKeys);
|
|
1204
|
-
}
|
|
1205
|
-
const descendantSelector = customRuleSplitTopLevelDescendantSelector(expression);
|
|
1206
|
-
if (descendantSelector) {
|
|
1207
|
-
return ancestors.some((ancestor) => customRuleSimpleSelectorMatches(descendantSelector[0].trim(), ancestor, visitorKeys))
|
|
1208
|
-
&& customRuleSimpleSelectorMatches(descendantSelector[1].trim(), node, visitorKeys);
|
|
1209
|
-
}
|
|
1210
|
-
return customRuleSimpleSelectorMatches(expression, node, visitorKeys);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function customRuleSimpleSelectorMatches(expression, node, visitorKeys = null) {
|
|
1214
|
-
const hasSelector = expression.match(/^(.+?):has\((.+)\)$/u);
|
|
1215
|
-
if (hasSelector) {
|
|
1216
|
-
return customRuleSimpleSelectorMatches(hasSelector[1].trim(), node, visitorKeys)
|
|
1217
|
-
&& customRuleDescendants(node, visitorKeys).some((descendant) => (
|
|
1218
|
-
customRuleSplitSelectorList(hasSelector[2]).some((selector) => customRuleSimpleSelectorMatches(selector.trim(), descendant, visitorKeys))
|
|
1219
|
-
));
|
|
1220
|
-
}
|
|
1221
|
-
const notSelector = expression.match(/^(.+?):not\((.+)\)$/u);
|
|
1222
|
-
if (notSelector) {
|
|
1223
|
-
return customRuleSimpleSelectorMatches(notSelector[1].trim(), node, visitorKeys)
|
|
1224
|
-
&& !customRuleSimpleSelectorMatches(notSelector[2].trim(), node, visitorKeys);
|
|
1225
|
-
}
|
|
1226
|
-
const matchesSelector = expression.match(/^(.*?):matches\((.+)\)$/u);
|
|
1227
|
-
if (matchesSelector) {
|
|
1228
|
-
const prefix = matchesSelector[1].trim();
|
|
1229
|
-
if (prefix && !customRuleSimpleSelectorMatches(prefix, node, visitorKeys)) {
|
|
1230
|
-
return false;
|
|
1231
|
-
}
|
|
1232
|
-
return customRuleSplitSelectorList(matchesSelector[2]).some((selector) => (
|
|
1233
|
-
customRuleSimpleSelectorMatches(selector.trim(), node, visitorKeys)
|
|
1234
|
-
));
|
|
1235
|
-
}
|
|
1236
|
-
if (expression === node.type) {
|
|
1237
|
-
return true;
|
|
1238
|
-
}
|
|
1239
|
-
const match = expression.match(/^([A-Za-z_$][\w$-]*|\*)?(?:\[([^\]]+)\])?$/u);
|
|
1240
|
-
if (!match) {
|
|
1241
|
-
return false;
|
|
1242
|
-
}
|
|
1243
|
-
const [, type = "*", attribute] = match;
|
|
1244
|
-
if (type !== "*" && type !== node.type) {
|
|
1245
|
-
return false;
|
|
1246
|
-
}
|
|
1247
|
-
return attribute ? customRuleAttributeSelectorMatches(node, attribute.trim()) : type === "*" || type === node.type;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
function customRuleDescendants(node, visitorKeys, seen = new Set()) {
|
|
1251
|
-
const descendants = [];
|
|
1252
|
-
for (const child of customRuleChildNodes(node, visitorKeys)) {
|
|
1253
|
-
if (seen.has(child)) {
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1256
|
-
seen.add(child);
|
|
1257
|
-
descendants.push(child, ...customRuleDescendants(child, visitorKeys, seen));
|
|
1258
|
-
}
|
|
1259
|
-
return descendants;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function customRuleSplitTopLevelSelector(expression, separator) {
|
|
1263
|
-
let state = customRuleSelectorScanState();
|
|
1264
|
-
for (let index = 0; index < expression.length; index += 1) {
|
|
1265
|
-
state = customRuleUpdateSelectorScanState(state, expression[index]);
|
|
1266
|
-
if (state.depth === 0 && !state.quote && expression[index] === separator) {
|
|
1267
|
-
return [expression.slice(0, index), expression.slice(index + 1)];
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
return null;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function customRuleSplitTopLevelDescendantSelector(expression) {
|
|
1274
|
-
let state = customRuleSelectorScanState();
|
|
1275
|
-
for (let index = 0; index < expression.length; index += 1) {
|
|
1276
|
-
state = customRuleUpdateSelectorScanState(state, expression[index]);
|
|
1277
|
-
if (state.depth === 0 && !state.quote && /\s/u.test(expression[index])) {
|
|
1278
|
-
const left = expression.slice(0, index).trim();
|
|
1279
|
-
const right = expression.slice(index).trim();
|
|
1280
|
-
return left && right ? [left, right] : null;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
return null;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function customRuleSplitSelectorList(value) {
|
|
1287
|
-
const selectors = [];
|
|
1288
|
-
let state = customRuleSelectorScanState();
|
|
1289
|
-
let start = 0;
|
|
1290
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1291
|
-
state = customRuleUpdateSelectorScanState(state, value[index]);
|
|
1292
|
-
if (state.depth === 0 && !state.quote && value[index] === ",") {
|
|
1293
|
-
selectors.push(value.slice(start, index));
|
|
1294
|
-
start = index + 1;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
selectors.push(value.slice(start));
|
|
1298
|
-
return selectors.filter((selector) => selector.trim());
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function customRuleSelectorScanState() {
|
|
1302
|
-
return { depth: 0, quote: null, escaped: false };
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function customRuleUpdateSelectorScanState(state, char) {
|
|
1306
|
-
if (state.escaped) {
|
|
1307
|
-
return { ...state, escaped: false };
|
|
1308
|
-
}
|
|
1309
|
-
if (char === "\\") {
|
|
1310
|
-
return { ...state, escaped: true };
|
|
1311
|
-
}
|
|
1312
|
-
if (state.quote) {
|
|
1313
|
-
return char === state.quote ? { ...state, quote: null } : state;
|
|
1314
|
-
}
|
|
1315
|
-
if (char === "\"" || char === "'") {
|
|
1316
|
-
return { ...state, quote: char };
|
|
1317
|
-
}
|
|
1318
|
-
if (char === "[" || char === "(") {
|
|
1319
|
-
return { ...state, depth: state.depth + 1 };
|
|
1320
|
-
}
|
|
1321
|
-
if ((char === "]" || char === ")") && state.depth > 0) {
|
|
1322
|
-
return { ...state, depth: state.depth - 1 };
|
|
1323
|
-
}
|
|
1324
|
-
return state;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
function customRuleAttributeSelectorMatches(node, attribute) {
|
|
1328
|
-
const match = attribute.match(/^([\w$.-]+)\s*(!=|=)\s*(.+)$/u);
|
|
1329
|
-
if (!match) {
|
|
1330
|
-
return customRuleValueByPath(node, attribute) != null;
|
|
1331
|
-
}
|
|
1332
|
-
const [, path, operator, rawExpected] = match;
|
|
1333
|
-
const actual = customRuleValueByPath(node, path);
|
|
1334
|
-
const expected = customRuleSelectorValue(rawExpected);
|
|
1335
|
-
return operator === "="
|
|
1336
|
-
? String(actual) === expected
|
|
1337
|
-
: String(actual) !== expected;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
function customRuleSelectorValue(raw) {
|
|
1341
|
-
const value = raw.trim();
|
|
1342
|
-
const quote = value[0];
|
|
1343
|
-
if ((quote === "\"" || quote === "'") && value.at(-1) === quote) {
|
|
1344
|
-
return value.slice(1, -1);
|
|
1345
|
-
}
|
|
1346
|
-
return value;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
function customRuleValueByPath(node, path) {
|
|
1350
|
-
return path.split(".").reduce((value, key) => (
|
|
1351
|
-
value && typeof value === "object" ? value[key] : undefined
|
|
1352
|
-
), node);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function customRuleChildNodes(node, visitorKeys) {
|
|
1356
|
-
const keys = Array.isArray(visitorKeys?.[node.type])
|
|
1357
|
-
? visitorKeys[node.type]
|
|
1358
|
-
: Object.keys(node).filter((key) => !AST_TRAVERSAL_SKIP_KEYS.has(key));
|
|
1359
|
-
const children = [];
|
|
1360
|
-
for (const key of keys) {
|
|
1361
|
-
const value = node[key];
|
|
1362
|
-
const values = Array.isArray(value) ? value : [value];
|
|
1363
|
-
for (const child of values) {
|
|
1364
|
-
if (child && typeof child === "object" && typeof child.type === "string") {
|
|
1365
|
-
children.push(child);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
return children;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
1420
|
function runCustomRuleTokenListeners(program, tokens, listeners, context) {
|
|
1373
1421
|
if (typeof listeners.Program === "function") {
|
|
1374
1422
|
context.setCurrentNode(program);
|
|
@@ -1403,7 +1451,7 @@ function createCustomRuleContext(ruleEntry, sourceCode, options, messages) {
|
|
|
1403
1451
|
let currentNode = sourceCode.ast ?? null;
|
|
1404
1452
|
const context = {
|
|
1405
1453
|
id: ruleEntry.ruleId,
|
|
1406
|
-
options: ruleEntry.options,
|
|
1454
|
+
options: pluginRunner.applyRuleOptionDefaults(ruleEntry.rule, ruleEntry.options),
|
|
1407
1455
|
filename,
|
|
1408
1456
|
physicalFilename: filename,
|
|
1409
1457
|
cwd,
|
|
@@ -1621,7 +1669,7 @@ export class SourceCode {
|
|
|
1621
1669
|
this.text = textOrConfig.text;
|
|
1622
1670
|
this.ast = textOrConfig.ast ?? null;
|
|
1623
1671
|
this.parserServices = textOrConfig.parserServices ?? {};
|
|
1624
|
-
this
|
|
1672
|
+
defineScopeManager(this, textOrConfig.scopeManager ?? null);
|
|
1625
1673
|
this.visitorKeys = textOrConfig.visitorKeys ?? null;
|
|
1626
1674
|
this.hasBOM = Boolean(textOrConfig.hasBOM);
|
|
1627
1675
|
} else {
|
|
@@ -1813,7 +1861,17 @@ export class SourceCode {
|
|
|
1813
1861
|
}
|
|
1814
1862
|
|
|
1815
1863
|
getAncestors(node) {
|
|
1816
|
-
|
|
1864
|
+
if (!node) {
|
|
1865
|
+
return [];
|
|
1866
|
+
}
|
|
1867
|
+
if (node.parent !== undefined) {
|
|
1868
|
+
const ancestors = [];
|
|
1869
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1870
|
+
ancestors.unshift(current);
|
|
1871
|
+
}
|
|
1872
|
+
return ancestors;
|
|
1873
|
+
}
|
|
1874
|
+
return sourceAncestorsForNode(this.ast, node) ?? [];
|
|
1817
1875
|
}
|
|
1818
1876
|
|
|
1819
1877
|
getDeclaredVariables(node) {
|
|
@@ -1821,10 +1879,20 @@ export class SourceCode {
|
|
|
1821
1879
|
}
|
|
1822
1880
|
|
|
1823
1881
|
getScope(node) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1882
|
+
const scopeManager = this.scopeManager;
|
|
1883
|
+
if (!scopeManager || typeof scopeManager.acquire !== "function") {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
if (node) {
|
|
1887
|
+
const inner = node.type !== "Program";
|
|
1888
|
+
for (let current = node; current; current = current.parent) {
|
|
1889
|
+
const scope = scopeManager.acquire(current, inner);
|
|
1890
|
+
if (scope) {
|
|
1891
|
+
return scope.type === "function-expression-name" ? scope.childScopes[0] : scope;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1826
1894
|
}
|
|
1827
|
-
return
|
|
1895
|
+
return scopeManager.scopes?.[0] ?? scopeManager.globalScope ?? null;
|
|
1828
1896
|
}
|
|
1829
1897
|
|
|
1830
1898
|
markVariableAsUsed(name, node) {
|
|
@@ -1860,6 +1928,31 @@ export class SourceCode {
|
|
|
1860
1928
|
}
|
|
1861
1929
|
}
|
|
1862
1930
|
|
|
1931
|
+
function defineScopeManager(sourceCode, scopeManager) {
|
|
1932
|
+
if (typeof scopeManager !== "function") {
|
|
1933
|
+
sourceCode.scopeManager = scopeManager;
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
// Scope analysis is comparatively expensive; run it only when a rule asks.
|
|
1937
|
+
let computed = false;
|
|
1938
|
+
let value = null;
|
|
1939
|
+
Object.defineProperty(sourceCode, "scopeManager", {
|
|
1940
|
+
configurable: true,
|
|
1941
|
+
enumerable: true,
|
|
1942
|
+
get() {
|
|
1943
|
+
if (!computed) {
|
|
1944
|
+
computed = true;
|
|
1945
|
+
value = scopeManager() ?? null;
|
|
1946
|
+
}
|
|
1947
|
+
return value;
|
|
1948
|
+
},
|
|
1949
|
+
set(next) {
|
|
1950
|
+
computed = true;
|
|
1951
|
+
value = next;
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1863
1956
|
function sourceLineStartIndices(text) {
|
|
1864
1957
|
const indices = [0];
|
|
1865
1958
|
for (let index = 0; index < text.length; index += 1) {
|
|
@@ -2149,12 +2242,10 @@ export class RuleTester {
|
|
|
2149
2242
|
RuleTester[testCase.only ? "itOnly" : "it"](testCase.name ?? testCase.code, () => {
|
|
2150
2243
|
const config = ruleTesterConfig(ruleName, this.config, testCase, "error");
|
|
2151
2244
|
const options = ruleTesterOptions(testCase);
|
|
2152
|
-
const
|
|
2153
|
-
? linter.verifyAndFix(testCase.code, config, options)
|
|
2154
|
-
: { messages: linter.verify(testCase.code, config, options), output: testCase.code };
|
|
2155
|
-
const messages = result.messages;
|
|
2245
|
+
const messages = linter.verify(testCase.code, config, options);
|
|
2156
2246
|
assertRuleTesterErrors(testCase, messages, rule);
|
|
2157
|
-
|
|
2247
|
+
// Like ESLint's RuleTester, `output` is the result of one autofix pass.
|
|
2248
|
+
assertRuleTesterOutput(testCase, Object.hasOwn(testCase, "output") ? applyMessageFixes(testCase.code, messages) : testCase.code);
|
|
2158
2249
|
});
|
|
2159
2250
|
}
|
|
2160
2251
|
});
|
|
@@ -2301,6 +2392,47 @@ function applyRuleFixes(code, fix) {
|
|
|
2301
2392
|
.reduce((output, item) => output.slice(0, item.range[0]) + item.text + output.slice(item.range[1]), code);
|
|
2302
2393
|
}
|
|
2303
2394
|
|
|
2395
|
+
// Applies one non-overlapping fix per message, like ESLint's SourceCodeFixer.
|
|
2396
|
+
// Fixes that overlap an earlier one are left for a later pass.
|
|
2397
|
+
function applyMessageFixes(code, messages) {
|
|
2398
|
+
const fixes = [];
|
|
2399
|
+
for (const message of messages) {
|
|
2400
|
+
const items = ruleFixItems(message?.fix)
|
|
2401
|
+
.slice()
|
|
2402
|
+
.sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
2403
|
+
if (items.length === 0) {
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
let text = "";
|
|
2407
|
+
let cursor = items[0].range[0];
|
|
2408
|
+
let valid = true;
|
|
2409
|
+
for (const item of items) {
|
|
2410
|
+
if (item.range[0] < cursor) {
|
|
2411
|
+
valid = false;
|
|
2412
|
+
break;
|
|
2413
|
+
}
|
|
2414
|
+
text += code.slice(cursor, item.range[0]) + item.text;
|
|
2415
|
+
cursor = item.range[1];
|
|
2416
|
+
}
|
|
2417
|
+
if (valid) {
|
|
2418
|
+
fixes.push({ range: [items[0].range[0], cursor], text });
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
fixes.sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
2422
|
+
let output = "";
|
|
2423
|
+
let position = 0;
|
|
2424
|
+
let lastEnd = -Infinity;
|
|
2425
|
+
for (const fix of fixes) {
|
|
2426
|
+
if (lastEnd >= fix.range[0]) {
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
output += code.slice(position, fix.range[0]) + fix.text;
|
|
2430
|
+
position = fix.range[1];
|
|
2431
|
+
lastEnd = fix.range[1];
|
|
2432
|
+
}
|
|
2433
|
+
return output + code.slice(position);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2304
2436
|
function ruleFixItems(fix) {
|
|
2305
2437
|
if (!fix) {
|
|
2306
2438
|
return [];
|
|
@@ -2353,10 +2485,29 @@ function createLinterSourceCode(text, parser = null, parserOptions = {}) {
|
|
|
2353
2485
|
});
|
|
2354
2486
|
}
|
|
2355
2487
|
|
|
2356
|
-
function
|
|
2357
|
-
|
|
2488
|
+
function parseSourceCodeWithDefaultParser(text, parserOptions) {
|
|
2489
|
+
let parsed;
|
|
2490
|
+
try {
|
|
2491
|
+
parsed = estreeParser.parseForESLint(text, parserOptions);
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
if (parserOptions.requireParser) {
|
|
2494
|
+
throw error;
|
|
2495
|
+
}
|
|
2358
2496
|
return null;
|
|
2359
2497
|
}
|
|
2498
|
+
return new SourceCode({
|
|
2499
|
+
text,
|
|
2500
|
+
ast: parsed.ast,
|
|
2501
|
+
parserServices: parsed.services ?? {},
|
|
2502
|
+
scopeManager: parsed.scopeManager ?? null,
|
|
2503
|
+
visitorKeys: parsed.visitorKeys ?? null
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function parseSourceCode(text, parser, parserOptions = {}) {
|
|
2508
|
+
if (!parser) {
|
|
2509
|
+
return parserOptions.tokensOnly ? null : parseSourceCodeWithDefaultParser(text, parserOptions);
|
|
2510
|
+
}
|
|
2360
2511
|
if (typeof parser.parseForESLint === "function") {
|
|
2361
2512
|
const result = parser.parseForESLint(text, parserOptions) ?? {};
|
|
2362
2513
|
return new SourceCode({
|
|
@@ -3128,21 +3279,210 @@ export function lintFiles(paths, options = {}) {
|
|
|
3128
3279
|
}
|
|
3129
3280
|
|
|
3130
3281
|
const nativeFlatOptions = nativeFlatConfigOptions(options);
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3282
|
+
const report = nativeFlatOptions
|
|
3283
|
+
? runNativeLintReport(lintPaths, nativeFlatOptions)
|
|
3284
|
+
: runNativeConfigRunReports(lintPaths, options);
|
|
3285
|
+
// The flat-config path resolves one config for the whole run; only the
|
|
3286
|
+
// per-file path has already discovered (and cached) nested configs.
|
|
3287
|
+
appendPluginRuleDiagnostics(report, options, { perFileConfig: !nativeFlatOptions });
|
|
3288
|
+
return finalizeLintReport(report, ignoredDiagnostics, options);
|
|
3289
|
+
}
|
|
3135
3290
|
|
|
3291
|
+
function runNativeConfigRunReports(lintPaths, options) {
|
|
3136
3292
|
const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
|
|
3137
3293
|
if (configRuns) {
|
|
3138
|
-
|
|
3294
|
+
return mergeNativeReports(
|
|
3139
3295
|
configRuns.map(({ paths: runPaths, options: runOptions }) => runNativeLintReport(runPaths, runOptions))
|
|
3140
3296
|
);
|
|
3141
|
-
return finalizeLintReport(report, ignoredDiagnostics, options);
|
|
3142
3297
|
}
|
|
3143
3298
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3299
|
+
return runNativeLintReport(lintPaths, options);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
function pluginFixModeForOptions(options) {
|
|
3303
|
+
const extraArgs = Array.isArray(options.extraArgs) ? options.extraArgs : [];
|
|
3304
|
+
if (options.fix || extraArgs.includes("--fix-dry-run")) {
|
|
3305
|
+
return "dry-run";
|
|
3306
|
+
}
|
|
3307
|
+
return extraArgs.includes("--fix") ? "write" : null;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function pluginMessageToDiagnostic(message, filePath) {
|
|
3311
|
+
const toFix = (fix) => ({ range: [fix.range[0], fix.range[1]], text: fix.text });
|
|
3312
|
+
const diagnostic = {
|
|
3313
|
+
filePath,
|
|
3314
|
+
line: message.line,
|
|
3315
|
+
column: message.column,
|
|
3316
|
+
severity: message.severity === 2 ? "error" : "warning",
|
|
3317
|
+
message: message.message,
|
|
3318
|
+
ruleId: message.ruleId,
|
|
3319
|
+
fixes: ruleFixItems(message.fix).map(toFix),
|
|
3320
|
+
suggestions: (message.suggestions ?? []).map((suggestion) => ({
|
|
3321
|
+
desc: suggestion.desc,
|
|
3322
|
+
fix: ruleFixItems(suggestion.fix).map(toFix)
|
|
3323
|
+
}))
|
|
3324
|
+
};
|
|
3325
|
+
if (typeof message.endLine === "number") {
|
|
3326
|
+
diagnostic.endLine = message.endLine;
|
|
3327
|
+
}
|
|
3328
|
+
if (typeof message.endColumn === "number") {
|
|
3329
|
+
diagnostic.endColumn = message.endColumn;
|
|
3330
|
+
}
|
|
3331
|
+
return diagnostic;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
function sortReportDiagnostics(report) {
|
|
3335
|
+
const order = new Map((report.filePaths ?? []).map((filePath, index) => [filePath, index]));
|
|
3336
|
+
const rank = (diagnostic) => order.get(diagnostic.filePath) ?? Number.MAX_SAFE_INTEGER;
|
|
3337
|
+
const compare = (left, right) => rank(left) - rank(right)
|
|
3338
|
+
|| (left.line ?? 0) - (right.line ?? 0)
|
|
3339
|
+
|| (left.column ?? 0) - (right.column ?? 0);
|
|
3340
|
+
report.diagnostics = [...(report.diagnostics ?? [])].sort(compare);
|
|
3341
|
+
report.suppressedDiagnostics = [...(report.suppressedDiagnostics ?? [])].sort(compare);
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
// Runs the ESLint plugin rules a config mounts and merges their diagnostics
|
|
3345
|
+
// into the native report. Plugin rules never reach the native binary; see
|
|
3346
|
+
// `withoutJsPluginRules`.
|
|
3347
|
+
function appendPluginRuleDiagnostics(report, options, { perFileConfig = true } = {}) {
|
|
3348
|
+
if (options.noConfig && !options.baseConfig && !options.overrideConfig) {
|
|
3349
|
+
return report;
|
|
3350
|
+
}
|
|
3351
|
+
const filePaths = report.filePaths ?? [];
|
|
3352
|
+
if (filePaths.length === 0 || !anyConfigDeclaresPlugins(options)) {
|
|
3353
|
+
return report;
|
|
3354
|
+
}
|
|
3355
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3356
|
+
const singleMatchPath = filePaths.length === 1
|
|
3357
|
+
? options.filePath ?? options.filename ?? options.pluginTextFilePath
|
|
3358
|
+
: undefined;
|
|
3359
|
+
const fixMode = pluginFixModeForOptions(options);
|
|
3360
|
+
let appended = false;
|
|
3361
|
+
|
|
3362
|
+
for (const filePath of filePaths) {
|
|
3363
|
+
const matchPath = singleMatchPath ?? filePath;
|
|
3364
|
+
const ruleMap = jsPluginRuleMapForFile(options, matchPath, perFileConfig);
|
|
3365
|
+
if (ruleMap.size === 0) {
|
|
3366
|
+
continue;
|
|
3367
|
+
}
|
|
3368
|
+
const ruleEntries = pluginRuleEntriesForFile(options, matchPath, ruleMap, perFileConfig);
|
|
3369
|
+
if (ruleEntries.length === 0) {
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// The native report echoes targets as given, so relative paths resolve
|
|
3374
|
+
// against the lint cwd rather than the process cwd.
|
|
3375
|
+
const absolutePath = normalizeESLintFilePath(filePath, cwd);
|
|
3376
|
+
const outputIndex = (report.outputs ?? []).findIndex((fixed) => fixed.filePath === filePath);
|
|
3377
|
+
let code = outputIndex === -1 ? undefined : report.outputs[outputIndex].output;
|
|
3378
|
+
if (typeof code !== "string") {
|
|
3379
|
+
try {
|
|
3380
|
+
code = readFileSync(absolutePath, "utf8");
|
|
3381
|
+
} catch {
|
|
3382
|
+
continue;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
const config = pluginConfigSourcesForFile(options, matchPath, perFileConfig).map((source) => source.config);
|
|
3387
|
+
const parserOptions = { ...parserOptionsForConfig(config, matchPath, cwd), filePath: matchPath, requireParser: true };
|
|
3388
|
+
const lintOptions = {
|
|
3389
|
+
cwd,
|
|
3390
|
+
filename: matchPath,
|
|
3391
|
+
config: calculatedConfig({ cwd, noConfig: true, baseConfig: config }, matchPath)
|
|
3392
|
+
};
|
|
3393
|
+
const lintOnce = (text) => {
|
|
3394
|
+
const sourceCode = createLinterSourceCode(text, null, parserOptions);
|
|
3395
|
+
return applyDisableDirectives(runCustomLinterRules(ruleEntries, sourceCode, lintOptions), sourceCode);
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3398
|
+
let filtered = lintOnce(code);
|
|
3399
|
+
let relintedFromReport = false;
|
|
3400
|
+
if (fixMode) {
|
|
3401
|
+
let changed = false;
|
|
3402
|
+
for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
|
|
3403
|
+
const output = applyMessageFixes(code, filtered.messages);
|
|
3404
|
+
if (output === code) {
|
|
3405
|
+
break;
|
|
3406
|
+
}
|
|
3407
|
+
changed = true;
|
|
3408
|
+
code = output;
|
|
3409
|
+
filtered = lintOnce(code);
|
|
3410
|
+
}
|
|
3411
|
+
if (changed) {
|
|
3412
|
+
// Plugin fixes changed the text, so the native diagnostics for this
|
|
3413
|
+
// file describe stale source. Re-lint the fixed text through the whole
|
|
3414
|
+
// pipeline (native rules, plugin rules, further fixes) and replace this
|
|
3415
|
+
// file's diagnostics with the result.
|
|
3416
|
+
const depth = options.pluginFixDepth ?? 0;
|
|
3417
|
+
if (depth < MAX_AUTOFIX_PASSES) {
|
|
3418
|
+
const relinted = lintText(code, {
|
|
3419
|
+
...options,
|
|
3420
|
+
filePath: matchPath,
|
|
3421
|
+
filename: undefined,
|
|
3422
|
+
config: pluginConfigPathForFile(options, matchPath, perFileConfig) ?? options.config,
|
|
3423
|
+
fix: true,
|
|
3424
|
+
extraArgs: (options.extraArgs ?? []).filter((arg) => arg !== "--fix" && arg !== "--fix-dry-run"),
|
|
3425
|
+
useDefaultConfigTargets: undefined,
|
|
3426
|
+
pluginTextFilePath: undefined,
|
|
3427
|
+
pluginFixDepth: depth + 1
|
|
3428
|
+
});
|
|
3429
|
+
code = relinted.outputs?.find((fixed) => fixed.filePath === matchPath)?.output ?? code;
|
|
3430
|
+
filtered = {
|
|
3431
|
+
messages: relinted.diagnostics ?? [],
|
|
3432
|
+
suppressedMessages: relinted.suppressedDiagnostics ?? []
|
|
3433
|
+
};
|
|
3434
|
+
report.diagnostics = (report.diagnostics ?? []).filter((diagnostic) => diagnostic.filePath !== filePath);
|
|
3435
|
+
report.suppressedDiagnostics = (report.suppressedDiagnostics ?? []).filter((diagnostic) => diagnostic.filePath !== filePath);
|
|
3436
|
+
relintedFromReport = true;
|
|
3437
|
+
}
|
|
3438
|
+
if (fixMode === "write") {
|
|
3439
|
+
writeFileSync(absolutePath, code);
|
|
3440
|
+
} else if (outputIndex === -1) {
|
|
3441
|
+
report.outputs = [...(report.outputs ?? []), { filePath, output: code }];
|
|
3442
|
+
} else {
|
|
3443
|
+
report.outputs[outputIndex] = { ...report.outputs[outputIndex], output: code };
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
if (relintedFromReport) {
|
|
3449
|
+
// Already native-shaped diagnostics from the nested lint; only the
|
|
3450
|
+
// file path needs to match this report.
|
|
3451
|
+
report.diagnostics = [
|
|
3452
|
+
...(report.diagnostics ?? []),
|
|
3453
|
+
...filtered.messages.map((diagnostic) => ({ ...diagnostic, filePath }))
|
|
3454
|
+
];
|
|
3455
|
+
report.suppressedDiagnostics = [
|
|
3456
|
+
...(report.suppressedDiagnostics ?? []),
|
|
3457
|
+
...filtered.suppressedMessages.map((diagnostic) => ({ ...diagnostic, filePath }))
|
|
3458
|
+
];
|
|
3459
|
+
appended = true;
|
|
3460
|
+
continue;
|
|
3461
|
+
}
|
|
3462
|
+
if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
report.diagnostics = [
|
|
3466
|
+
...(report.diagnostics ?? []),
|
|
3467
|
+
...filtered.messages.map((message) => pluginMessageToDiagnostic(message, filePath))
|
|
3468
|
+
];
|
|
3469
|
+
report.suppressedDiagnostics = [
|
|
3470
|
+
...(report.suppressedDiagnostics ?? []),
|
|
3471
|
+
...filtered.suppressedMessages.map((message) => ({
|
|
3472
|
+
...pluginMessageToDiagnostic(message, filePath),
|
|
3473
|
+
suppression: {
|
|
3474
|
+
kind: message.suppressions?.[0]?.kind ?? "directive",
|
|
3475
|
+
justification: message.suppressions?.[0]?.justification ?? ""
|
|
3476
|
+
}
|
|
3477
|
+
}))
|
|
3478
|
+
];
|
|
3479
|
+
appended = true;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
if (appended) {
|
|
3483
|
+
sortReportDiagnostics(report);
|
|
3484
|
+
}
|
|
3485
|
+
return report;
|
|
3146
3486
|
}
|
|
3147
3487
|
|
|
3148
3488
|
function nativeFlatConfigOptions(options) {
|
|
@@ -3172,6 +3512,10 @@ function nativeFlatConfigOptions(options) {
|
|
|
3172
3512
|
return undefined;
|
|
3173
3513
|
}
|
|
3174
3514
|
|
|
3515
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(config);
|
|
3516
|
+
if (jsPluginRuleIds.size > 0) {
|
|
3517
|
+
config = configWithoutCustomRules(config, jsPluginRuleIds);
|
|
3518
|
+
}
|
|
3175
3519
|
if (configCanUseNativeRulesFastPath(config)) {
|
|
3176
3520
|
return undefined;
|
|
3177
3521
|
}
|
|
@@ -3185,11 +3529,28 @@ function nativeFlatConfigOptions(options) {
|
|
|
3185
3529
|
};
|
|
3186
3530
|
}
|
|
3187
3531
|
|
|
3532
|
+
// Globals declared by a config entry, merged the way ESLint does: flat
|
|
3533
|
+
// `languageOptions.globals` wins over eslintrc-style `globals`.
|
|
3534
|
+
function nativeConfigGlobals(config) {
|
|
3535
|
+
if (!config || typeof config !== "object") {
|
|
3536
|
+
return undefined;
|
|
3537
|
+
}
|
|
3538
|
+
const globals = {
|
|
3539
|
+
...(isGlobalsObject(config.globals) ? config.globals : {}),
|
|
3540
|
+
...(isGlobalsObject(config.languageOptions?.globals) ? config.languageOptions.globals : {})
|
|
3541
|
+
};
|
|
3542
|
+
return Object.keys(globals).length > 0 ? globals : undefined;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
function isGlobalsObject(value) {
|
|
3546
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3188
3549
|
function configCanUseNativeRulesFastPath(config) {
|
|
3189
3550
|
const entries = Array.isArray(config) ? config : [config];
|
|
3190
3551
|
let hasEnabledRule = false;
|
|
3191
3552
|
for (const entry of entries) {
|
|
3192
|
-
if (!entry || typeof entry !== "object" || Object.keys(entry.settings ?? {}).length > 0) {
|
|
3553
|
+
if (!entry || typeof entry !== "object" || Object.keys(entry.settings ?? {}).length > 0 || nativeConfigGlobals(entry)) {
|
|
3193
3554
|
return false;
|
|
3194
3555
|
}
|
|
3195
3556
|
for (const ruleConfig of Object.values(entry.rules ?? {})) {
|
|
@@ -3212,11 +3573,16 @@ function nativeSerializableConfig(config) {
|
|
|
3212
3573
|
if (!config || typeof config !== "object") {
|
|
3213
3574
|
return config;
|
|
3214
3575
|
}
|
|
3215
|
-
|
|
3576
|
+
const serialized = Object.fromEntries(
|
|
3216
3577
|
["$schema", "name", "files", "ignores", "rules", "settings"]
|
|
3217
3578
|
.filter((key) => config[key] !== undefined)
|
|
3218
3579
|
.map((key) => [key, config[key]])
|
|
3219
3580
|
);
|
|
3581
|
+
const globals = nativeConfigGlobals(config);
|
|
3582
|
+
if (globals) {
|
|
3583
|
+
serialized.languageOptions = { globals };
|
|
3584
|
+
}
|
|
3585
|
+
return serialized;
|
|
3220
3586
|
}
|
|
3221
3587
|
|
|
3222
3588
|
function expandNativeConfigRunPaths(paths, options) {
|
|
@@ -3295,12 +3661,16 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
3295
3661
|
const calculated = configured ? calculatedConfig({ ...options, rules: undefined }, matchPath) : {};
|
|
3296
3662
|
let rules = calculated.rules;
|
|
3297
3663
|
const settings = calculated.settings;
|
|
3664
|
+
const globals = configured ? nativeConfigGlobals(calculated) : undefined;
|
|
3298
3665
|
if (configured && options.rules) {
|
|
3299
3666
|
rules = selectedRulesWithConfigOptions(rules, options.rules);
|
|
3300
3667
|
}
|
|
3301
|
-
|
|
3668
|
+
if (configured) {
|
|
3669
|
+
rules = withoutJsPluginRules(rules, jsPluginRuleIdsForOptions(options, matchPath));
|
|
3670
|
+
}
|
|
3671
|
+
const signature = configured ? stableConfigSignature({ rules, settings, globals }) : "<native-defaults>";
|
|
3302
3672
|
if (!groups.has(signature)) {
|
|
3303
|
-
groups.set(signature, { paths: [], rules, settings, configured });
|
|
3673
|
+
groups.set(signature, { paths: [], rules, settings, globals, configured });
|
|
3304
3674
|
}
|
|
3305
3675
|
groups.get(signature).paths.push(filePath);
|
|
3306
3676
|
}
|
|
@@ -3323,7 +3693,7 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
3323
3693
|
};
|
|
3324
3694
|
}
|
|
3325
3695
|
|
|
3326
|
-
const selectedRules = severityOnlyNativeRuleNames(group.rules, group.settings);
|
|
3696
|
+
const selectedRules = severityOnlyNativeRuleNames(group.rules, group.settings, group.globals);
|
|
3327
3697
|
if (selectedRules) {
|
|
3328
3698
|
return {
|
|
3329
3699
|
paths: group.paths,
|
|
@@ -3348,7 +3718,8 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
3348
3718
|
baseConfig: undefined,
|
|
3349
3719
|
overrideConfig: {
|
|
3350
3720
|
rules: allDisabledNativeRules(group.rules),
|
|
3351
|
-
...(group.settings ? { settings: group.settings } : {})
|
|
3721
|
+
...(group.settings ? { settings: group.settings } : {}),
|
|
3722
|
+
...(group.globals ? { languageOptions: { globals: group.globals } } : {})
|
|
3352
3723
|
},
|
|
3353
3724
|
forceMaterializedConfig: true
|
|
3354
3725
|
}
|
|
@@ -3356,8 +3727,8 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
3356
3727
|
});
|
|
3357
3728
|
}
|
|
3358
3729
|
|
|
3359
|
-
function severityOnlyNativeRuleNames(rules, settings) {
|
|
3360
|
-
if (Object.keys(settings ?? {}).length > 0) {
|
|
3730
|
+
function severityOnlyNativeRuleNames(rules, settings, globals) {
|
|
3731
|
+
if (globals || Object.keys(settings ?? {}).length > 0) {
|
|
3361
3732
|
return undefined;
|
|
3362
3733
|
}
|
|
3363
3734
|
|
|
@@ -3470,7 +3841,8 @@ export function lintText(code, options = {}) {
|
|
|
3470
3841
|
cwd: options.cwd,
|
|
3471
3842
|
config: options.config ?? discoveredConfig,
|
|
3472
3843
|
noConfig: options.noConfig,
|
|
3473
|
-
deferDiagnosticConfigFiltering: true
|
|
3844
|
+
deferDiagnosticConfigFiltering: true,
|
|
3845
|
+
pluginTextFilePath: requestedPath
|
|
3474
3846
|
});
|
|
3475
3847
|
report.filePaths = (report.filePaths ?? []).map((filePath) => (filePath === tempFile ? requestedPath : filePath));
|
|
3476
3848
|
report.outputs = (report.outputs ?? []).map((fixed) => ({
|
|
@@ -3512,11 +3884,12 @@ function buildNativeLintArgs(paths, options) {
|
|
|
3512
3884
|
const configForRuleSelection = options.noConfig
|
|
3513
3885
|
? undefined
|
|
3514
3886
|
: options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
|
|
3887
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(configForRuleSelection, options.baseConfig, options.overrideConfig);
|
|
3515
3888
|
const enabledRules = enabledRuleNamesFromConfigs(
|
|
3516
3889
|
configForRuleSelection,
|
|
3517
3890
|
options.baseConfig,
|
|
3518
3891
|
options.overrideConfig
|
|
3519
|
-
);
|
|
3892
|
+
).filter((rule) => !jsPluginRuleIds.has(rule));
|
|
3520
3893
|
if (enabledRules.length > 0) {
|
|
3521
3894
|
cliArgs.push(`--rules=${enabledRules.join(",")}`);
|
|
3522
3895
|
}
|
|
@@ -3679,6 +4052,10 @@ function mergeConfigData(base, override) {
|
|
|
3679
4052
|
}
|
|
3680
4053
|
if (base.languageOptions || override.languageOptions) {
|
|
3681
4054
|
result.languageOptions = { ...(base.languageOptions ?? {}), ...(override.languageOptions ?? {}) };
|
|
4055
|
+
// ESLint merges `languageOptions.globals` per name across config objects.
|
|
4056
|
+
if (isGlobalsObject(base.languageOptions?.globals) && isGlobalsObject(override.languageOptions?.globals)) {
|
|
4057
|
+
result.languageOptions.globals = { ...base.languageOptions.globals, ...override.languageOptions.globals };
|
|
4058
|
+
}
|
|
3682
4059
|
}
|
|
3683
4060
|
if (base.parserOptions || override.parserOptions) {
|
|
3684
4061
|
result.parserOptions = { ...(base.parserOptions ?? {}), ...(override.parserOptions ?? {}) };
|
|
@@ -3974,7 +4351,7 @@ function withTemporaryConfig(options, callback) {
|
|
|
3974
4351
|
const tmp = mkdtempSync(join(tmpdir(), "utoo-lint-flat-config-"));
|
|
3975
4352
|
const configPath = join(tmp, "utlint.config.json");
|
|
3976
4353
|
try {
|
|
3977
|
-
writeFileSync(configPath, JSON.stringify(options.nativeFlatConfigData));
|
|
4354
|
+
writeFileSync(configPath, JSON.stringify(nativeFlatConfigEntries(options.nativeFlatConfigData)));
|
|
3978
4355
|
return callback({
|
|
3979
4356
|
...options,
|
|
3980
4357
|
config: configPath,
|
|
@@ -3994,9 +4371,11 @@ function withTemporaryConfig(options, callback) {
|
|
|
3994
4371
|
? readConfig(fileConfigPath, options.cwd, options.configCache)
|
|
3995
4372
|
: undefined;
|
|
3996
4373
|
const configs = [options.baseConfig, fileConfig, options.overrideConfig];
|
|
3997
|
-
const
|
|
3998
|
-
|
|
3999
|
-
: runtimeRulesFromConfigs(...configs)
|
|
4374
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(...configs);
|
|
4375
|
+
const rules = withoutJsPluginRules(
|
|
4376
|
+
shouldMaterializeFileConfig ? materializedRulesFromConfigs(...configs) : runtimeRulesFromConfigs(...configs),
|
|
4377
|
+
jsPluginRuleIds
|
|
4378
|
+
);
|
|
4000
4379
|
const settings = configs.reduce(
|
|
4001
4380
|
(result, config) => ({
|
|
4002
4381
|
...result,
|
|
@@ -4005,7 +4384,15 @@ function withTemporaryConfig(options, callback) {
|
|
|
4005
4384
|
{}
|
|
4006
4385
|
);
|
|
4007
4386
|
const hasSettings = Object.keys(settings).length > 0;
|
|
4008
|
-
|
|
4387
|
+
const globals = configs.reduce(
|
|
4388
|
+
(result, config) => ({
|
|
4389
|
+
...result,
|
|
4390
|
+
...(nativeConfigGlobals(configDataFromConfig(config, options.filePath ?? options.filename, options.cwd)) ?? {})
|
|
4391
|
+
}),
|
|
4392
|
+
{}
|
|
4393
|
+
);
|
|
4394
|
+
const hasGlobals = Object.keys(globals).length > 0;
|
|
4395
|
+
if (Object.keys(rules).length === 0 && !hasSettings && !hasGlobals) {
|
|
4009
4396
|
if (shouldMaterializeFileConfig) {
|
|
4010
4397
|
return callback({
|
|
4011
4398
|
...options,
|
|
@@ -4015,9 +4402,9 @@ function withTemporaryConfig(options, callback) {
|
|
|
4015
4402
|
}
|
|
4016
4403
|
return callback(options);
|
|
4017
4404
|
}
|
|
4018
|
-
const enabledRules = enabledRuleNamesFromConfigs(...configs);
|
|
4405
|
+
const enabledRules = enabledRuleNamesFromConfigs(...configs).filter((rule) => !jsPluginRuleIds.has(rule));
|
|
4019
4406
|
const hasExplicitOffRules = Object.values(rules).some((value) => ruleConfigSeverity(value) === 0);
|
|
4020
|
-
if (!shouldMaterializeFileConfig && !hasSettings && !hasRuleOptions(rules) && !hasExplicitOffRules && !options.forceMaterializedConfig) {
|
|
4407
|
+
if (!shouldMaterializeFileConfig && !hasSettings && !hasGlobals && !hasRuleOptions(rules) && !hasExplicitOffRules && !options.forceMaterializedConfig) {
|
|
4021
4408
|
return callback({
|
|
4022
4409
|
...options,
|
|
4023
4410
|
config: shouldMaterializeFileConfig ? undefined : options.config,
|
|
@@ -4029,7 +4416,11 @@ function withTemporaryConfig(options, callback) {
|
|
|
4029
4416
|
const tmp = mkdtempSync(join(tmpdir(), "utoo-lint-config-"));
|
|
4030
4417
|
const configPath = join(tmp, "utlint.config.json");
|
|
4031
4418
|
try {
|
|
4032
|
-
writeFileSync(configPath, JSON.stringify({
|
|
4419
|
+
writeFileSync(configPath, JSON.stringify({
|
|
4420
|
+
rules: nativeRules(rules),
|
|
4421
|
+
...(hasSettings ? { settings } : {}),
|
|
4422
|
+
...(hasGlobals ? { languageOptions: { globals } } : {})
|
|
4423
|
+
}));
|
|
4033
4424
|
return callback({
|
|
4034
4425
|
...options,
|
|
4035
4426
|
config: shouldMaterializeFileConfig ? configPath : options.config ?? configPath,
|
|
@@ -4201,7 +4592,7 @@ function applyCompatibilityDisableDirectives(result, textOptions) {
|
|
|
4201
4592
|
return;
|
|
4202
4593
|
}
|
|
4203
4594
|
|
|
4204
|
-
const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source));
|
|
4595
|
+
const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source, null, { tokensOnly: true }));
|
|
4205
4596
|
result.messages = filtered.messages;
|
|
4206
4597
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
4207
4598
|
}
|
|
@@ -4935,6 +5326,12 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
|
|
|
4935
5326
|
column: diagnostic.column,
|
|
4936
5327
|
nodeType: null
|
|
4937
5328
|
};
|
|
5329
|
+
if (typeof diagnostic.endLine === "number") {
|
|
5330
|
+
message.endLine = diagnostic.endLine;
|
|
5331
|
+
}
|
|
5332
|
+
if (typeof diagnostic.endColumn === "number") {
|
|
5333
|
+
message.endColumn = diagnostic.endColumn;
|
|
5334
|
+
}
|
|
4938
5335
|
if (diagnostic.endLine != null) {
|
|
4939
5336
|
message.endLine = diagnostic.endLine;
|
|
4940
5337
|
}
|