@utoo/lint 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/index.cjs +634 -278
- package/index.d.ts +11 -0
- package/index.js +634 -278
- package/lib/config-module.cjs +54 -0
- package/lib/estree-parser.cjs +662 -0
- package/lib/load-config.js +67 -1
- package/lib/plugin-runner.cjs +342 -0
- package/package.json +12 -7
package/index.js
CHANGED
|
@@ -10,8 +10,12 @@ import { createLegacyConfigResolver } from "./lib/legacy-config.cjs";
|
|
|
10
10
|
import { formatESLintResults } from "./lib/stylish-formatter.cjs";
|
|
11
11
|
import {
|
|
12
12
|
findConfigPath as findConfigPathFromDirectory,
|
|
13
|
+
isExecutableConfigPath,
|
|
13
14
|
readConfig
|
|
14
15
|
} from "./lib/config-loader.js";
|
|
16
|
+
import configModule from "./lib/config-module.cjs";
|
|
17
|
+
import estreeParser from "./lib/estree-parser.cjs";
|
|
18
|
+
import pluginRunner from "./lib/plugin-runner.cjs";
|
|
15
19
|
|
|
16
20
|
export { platformPackageName, resolveBinary } from "./lib/binary.js";
|
|
17
21
|
|
|
@@ -28,10 +32,6 @@ const JS_KEYWORDS = new Set([
|
|
|
28
32
|
"instanceof", "let", "new", "return", "super", "switch", "this", "throw", "try", "typeof",
|
|
29
33
|
"var", "void", "while", "with", "yield"
|
|
30
34
|
]);
|
|
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
35
|
const RULE_TESTER_INITIAL_CONFIG = { rules: {} };
|
|
36
36
|
let ruleTesterDefaultConfig = { rules: {} };
|
|
37
37
|
let ruleTesterDescribe = null;
|
|
@@ -483,6 +483,7 @@ const NATIVE_ONLY_RULE_IDS = [
|
|
|
483
483
|
"semantic-errors"
|
|
484
484
|
];
|
|
485
485
|
const NATIVE_RULE_IDS = [...BUILTIN_RULE_IDS, ...NATIVE_ONLY_RULE_IDS];
|
|
486
|
+
const NATIVE_RULE_ID_SET = new Set(NATIVE_RULE_IDS);
|
|
486
487
|
const FIXABLE_BUILTIN_RULE_IDS = new Set([
|
|
487
488
|
"@typescript-eslint/no-explicit-any",
|
|
488
489
|
"jest/no-deprecated-functions",
|
|
@@ -610,15 +611,15 @@ export class UtooLint {
|
|
|
610
611
|
throw new Error("'results' must be an array");
|
|
611
612
|
}
|
|
612
613
|
const options = eslintConstructorOptions(this.options);
|
|
613
|
-
const customRuleConfig = [options.baseConfig, options.overrideConfig].filter(Boolean);
|
|
614
614
|
const rulesByFilePath = new Map();
|
|
615
|
+
const cachedOptions = withConfigCache(options);
|
|
615
616
|
return rulesMetaForResults(results, (ruleId, result) => {
|
|
616
|
-
if (
|
|
617
|
+
if (typeof result.filePath !== "string") {
|
|
617
618
|
return undefined;
|
|
618
619
|
}
|
|
619
620
|
const filePath = normalizeESLintFilePath(result.filePath, options.cwd);
|
|
620
621
|
if (!rulesByFilePath.has(filePath)) {
|
|
621
|
-
rulesByFilePath.set(filePath,
|
|
622
|
+
rulesByFilePath.set(filePath, jsPluginRuleMapForFile(cachedOptions, filePath));
|
|
622
623
|
}
|
|
623
624
|
return rulesByFilePath.get(filePath).get(ruleId)?.meta;
|
|
624
625
|
});
|
|
@@ -669,14 +670,14 @@ export class Linter {
|
|
|
669
670
|
const verifyOptions = typeof options === "string" ? { filename: options } : { ...options };
|
|
670
671
|
const filePath = verifyOptions.filename ?? verifyOptions.filePath ?? "input.js";
|
|
671
672
|
const normalizedFilePath = normalizeESLintFilePath(filePath, verifyOptions.cwd);
|
|
673
|
+
const customRuleMap = customRuleMapForConfig(config, this.rules, normalizedFilePath, verifyOptions.cwd);
|
|
674
|
+
const customRuleFilterMap = customRuleMapForConfig(config, this.rules);
|
|
675
|
+
const customRules = customRuleEntriesForConfig(config, customRuleMap, normalizedFilePath, verifyOptions.cwd);
|
|
672
676
|
const sourceCode = createLinterSourceCode(
|
|
673
677
|
code,
|
|
674
678
|
parserForConfig(config, this.parsers, normalizedFilePath, verifyOptions.cwd),
|
|
675
|
-
parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd)
|
|
679
|
+
{ ...parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd), requireParser: customRules.length > 0 }
|
|
676
680
|
);
|
|
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
681
|
const nativeConfig = configWithoutCustomRules(config, customRuleFilterMap);
|
|
681
682
|
const lintOptions = {
|
|
682
683
|
cwd: verifyOptions.cwd,
|
|
@@ -706,10 +707,10 @@ export class Linter {
|
|
|
706
707
|
];
|
|
707
708
|
this.times = { passes: [] };
|
|
708
709
|
this.fixPassCount = 0;
|
|
709
|
-
return [
|
|
710
|
+
return sortLintMessages([
|
|
710
711
|
...nativeRuleFilter.messages,
|
|
711
712
|
...customRuleFilter.messages
|
|
712
|
-
];
|
|
713
|
+
]);
|
|
713
714
|
}
|
|
714
715
|
|
|
715
716
|
verifyAndFix(code, config = {}, options = {}) {
|
|
@@ -718,7 +719,7 @@ export class Linter {
|
|
|
718
719
|
let fixPassCount = 0;
|
|
719
720
|
for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
|
|
720
721
|
const messages = this.verify(output, config, options);
|
|
721
|
-
const fixedOutput =
|
|
722
|
+
const fixedOutput = applyMessageFixes(output, messages);
|
|
722
723
|
if (fixedOutput === output) {
|
|
723
724
|
this.fixPassCount = fixPassCount;
|
|
724
725
|
return {
|
|
@@ -801,9 +802,17 @@ function parserForConfig(config, parsers, filePath, cwd) {
|
|
|
801
802
|
|
|
802
803
|
function parserOptionsForConfig(config, filePath, cwd) {
|
|
803
804
|
const configData = configDataFromConfig(config, filePath, cwd);
|
|
804
|
-
|
|
805
|
+
const languageOptions = configData.languageOptions ?? {};
|
|
806
|
+
const parserOptions = {
|
|
805
807
|
...(configData.parserOptions ?? {}),
|
|
806
|
-
...(
|
|
808
|
+
...(languageOptions.parserOptions ?? {})
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
...parserOptions,
|
|
812
|
+
sourceType: languageOptions.sourceType ?? parserOptions.sourceType,
|
|
813
|
+
ecmaVersion: languageOptions.ecmaVersion ?? parserOptions.ecmaVersion,
|
|
814
|
+
ecmaFeatures: parserOptions.ecmaFeatures ?? {},
|
|
815
|
+
globals: { ...(configData.globals ?? {}), ...(languageOptions.globals ?? {}) },
|
|
807
816
|
filename: filePath,
|
|
808
817
|
filePath,
|
|
809
818
|
cwd
|
|
@@ -849,13 +858,176 @@ function pluginRulesForConfig(config, filePath, cwd) {
|
|
|
849
858
|
continue;
|
|
850
859
|
}
|
|
851
860
|
for (const [ruleName, rule] of Object.entries(plugin.rules)) {
|
|
852
|
-
|
|
861
|
+
const ruleId = `${pluginName}/${ruleName}`;
|
|
862
|
+
// A native rule takes precedence over a plugin rule mounted under the
|
|
863
|
+
// same id, so a config copied from ESLint keeps its native speed.
|
|
864
|
+
if (NATIVE_RULE_ID_SET.has(ruleId)) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
rules.set(ruleId, rule);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return rules;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const pluginRuleIdsByConfig = new WeakMap();
|
|
875
|
+
|
|
876
|
+
function jsPluginRuleIdsFromConfigs(...configs) {
|
|
877
|
+
const ids = new Set();
|
|
878
|
+
for (const config of configs) {
|
|
879
|
+
if (!config || typeof config !== "object") {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
let ruleIds = pluginRuleIdsByConfig.get(config);
|
|
883
|
+
if (!ruleIds) {
|
|
884
|
+
ruleIds = [...pluginRulesForConfig(config).keys()];
|
|
885
|
+
pluginRuleIdsByConfig.set(config, ruleIds);
|
|
886
|
+
}
|
|
887
|
+
for (const ruleId of ruleIds) {
|
|
888
|
+
ids.add(ruleId);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return ids;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function withoutJsPluginRules(rules, jsPluginRuleIds) {
|
|
895
|
+
if (!rules || typeof rules !== "object" || jsPluginRuleIds.size === 0) {
|
|
896
|
+
return rules;
|
|
897
|
+
}
|
|
898
|
+
const filtered = {};
|
|
899
|
+
for (const [ruleId, ruleConfig] of Object.entries(rules)) {
|
|
900
|
+
if (!jsPluginRuleIds.has(ruleId)) {
|
|
901
|
+
filtered[ruleId] = ruleConfig;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return filtered;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function configDeclaresPlugins(config) {
|
|
908
|
+
if (Array.isArray(config)) {
|
|
909
|
+
return config.some((entry) => configDeclaresPlugins(entry));
|
|
910
|
+
}
|
|
911
|
+
return Boolean(
|
|
912
|
+
config
|
|
913
|
+
&& typeof config === "object"
|
|
914
|
+
&& config.plugins
|
|
915
|
+
&& typeof config.plugins === "object"
|
|
916
|
+
&& !Array.isArray(config.plugins)
|
|
917
|
+
&& Object.keys(config.plugins).length > 0
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Cheap pre-check so runs without plugins skip the per-file plugin work. Every
|
|
922
|
+
// config the native run consulted was read through `options.configCache`.
|
|
923
|
+
function anyConfigDeclaresPlugins(options) {
|
|
924
|
+
if (configDeclaresPlugins(options.baseConfig) || configDeclaresPlugins(options.overrideConfig)) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
if (options.noConfig) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
let sawConfig = false;
|
|
931
|
+
for (const config of options.configCache?.values() ?? []) {
|
|
932
|
+
if (config && typeof config === "object") {
|
|
933
|
+
sawConfig = true;
|
|
934
|
+
if (configDeclaresPlugins(config)) {
|
|
935
|
+
return true;
|
|
853
936
|
}
|
|
854
937
|
}
|
|
855
938
|
}
|
|
939
|
+
if (sawConfig) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
const configPath = configPathForOptions(options);
|
|
943
|
+
return Boolean(configPath) && configDeclaresPlugins(readConfig(configPath, options.cwd, options.configCache));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// The native engine receives the JSON form of a config module. Plugin objects
|
|
947
|
+
// (and their `create()` functions) only survive an in-process import, so a
|
|
948
|
+
// config module that mounts plugins is loaded again in this process.
|
|
949
|
+
function liveConfigForPath(configPath, options) {
|
|
950
|
+
const config = readConfig(configPath, options.cwd, options.configCache);
|
|
951
|
+
if (!isExecutableConfigPath(configPath) || !configDeclaresPlugins(config)) {
|
|
952
|
+
return config;
|
|
953
|
+
}
|
|
954
|
+
return configModule.readConfigModule(configPath, options.cwd);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function pluginConfigPathForFile(options, matchPath, perFileConfig = true) {
|
|
958
|
+
if (options.noConfig) {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
return perFileConfig && matchPath ? configPathForFile(options, matchPath) : configPathForOptions(options);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function pluginConfigSourcesForFile(options, matchPath, perFileConfig = true) {
|
|
965
|
+
const cwd = options.cwd ?? process.cwd();
|
|
966
|
+
const sources = [];
|
|
967
|
+
if (options.baseConfig) {
|
|
968
|
+
sources.push({ config: options.baseConfig, cwd });
|
|
969
|
+
}
|
|
970
|
+
const configPath = pluginConfigPathForFile(options, matchPath, perFileConfig);
|
|
971
|
+
if (configPath) {
|
|
972
|
+
sources.push({ config: liveConfigForPath(configPath, options), cwd: dirname(configPath) });
|
|
973
|
+
}
|
|
974
|
+
if (options.overrideConfig) {
|
|
975
|
+
sources.push({ config: options.overrideConfig, cwd });
|
|
976
|
+
}
|
|
977
|
+
return sources;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function jsPluginRuleIdsForOptions(options, matchPath) {
|
|
981
|
+
const configs = [options.baseConfig, options.overrideConfig];
|
|
982
|
+
if (!options.noConfig) {
|
|
983
|
+
const configPath = matchPath ? configPathForFile(options, matchPath) : configPathForOptions(options);
|
|
984
|
+
if (configPath) {
|
|
985
|
+
configs.push(readConfig(configPath, options.cwd, options.configCache));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return jsPluginRuleIdsFromConfigs(...configs);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function jsPluginRuleMapForFile(options, matchPath, perFileConfig = true) {
|
|
992
|
+
const rules = new Map();
|
|
993
|
+
for (const { config, cwd } of pluginConfigSourcesForFile(options, matchPath, perFileConfig)) {
|
|
994
|
+
for (const [ruleId, rule] of pluginRulesForConfig(config, matchPath, cwd)) {
|
|
995
|
+
rules.set(ruleId, rule);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
856
998
|
return rules;
|
|
857
999
|
}
|
|
858
1000
|
|
|
1001
|
+
function pluginRuleEntriesForFile(options, matchPath, ruleMap, perFileConfig = true) {
|
|
1002
|
+
const entries = new Map();
|
|
1003
|
+
for (const { config, cwd } of pluginConfigSourcesForFile(options, matchPath, perFileConfig)) {
|
|
1004
|
+
for (const [ruleId, ruleConfig] of Object.entries(rulesFromConfig(config, matchPath, cwd))) {
|
|
1005
|
+
if (!ruleMap.has(ruleId)) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const severity = ruleConfigSeverity(ruleConfig);
|
|
1009
|
+
if (severity === 0) {
|
|
1010
|
+
entries.delete(ruleId);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
entries.set(ruleId, {
|
|
1014
|
+
ruleId,
|
|
1015
|
+
rule: ruleMap.get(ruleId),
|
|
1016
|
+
severity,
|
|
1017
|
+
options: Array.isArray(ruleConfig) ? ruleConfig.slice(1) : []
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (options.rules) {
|
|
1022
|
+
// `--rules` disables everything first and then enables only the listed
|
|
1023
|
+
// rules, keeping configured severities and options like the native path.
|
|
1024
|
+
return Object.keys(rulesFromNativeRuleList(options.rules))
|
|
1025
|
+
.filter((ruleId) => ruleMap.has(ruleId))
|
|
1026
|
+
.map((ruleId) => entries.get(ruleId) ?? { ruleId, rule: ruleMap.get(ruleId), severity: 1, options: [] });
|
|
1027
|
+
}
|
|
1028
|
+
return [...entries.values()];
|
|
1029
|
+
}
|
|
1030
|
+
|
|
859
1031
|
function matchingConfigEntries(config, filePath = undefined, cwd = undefined) {
|
|
860
1032
|
if (!config) {
|
|
861
1033
|
return [];
|
|
@@ -907,7 +1079,7 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
|
|
|
907
1079
|
}
|
|
908
1080
|
let result = results.find((item) => item.filePath === filePath);
|
|
909
1081
|
const effectiveCode = typeof result?.output === "string" ? result.output : code;
|
|
910
|
-
const sourceCode = createLinterSourceCode(effectiveCode);
|
|
1082
|
+
const sourceCode = createLinterSourceCode(effectiveCode, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
|
|
911
1083
|
const messages = runCustomLinterRules(customRules, sourceCode, {
|
|
912
1084
|
cwd: options.cwd,
|
|
913
1085
|
filename: filePath,
|
|
@@ -922,13 +1094,74 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
|
|
|
922
1094
|
result = emptyESLintResult(filePath, effectiveCode);
|
|
923
1095
|
results.push(result);
|
|
924
1096
|
}
|
|
925
|
-
result
|
|
1097
|
+
if (relintAfterCustomFixes(result, effectiveCode, filePath, customRules, config, rules, options)) {
|
|
1098
|
+
return results;
|
|
1099
|
+
}
|
|
1100
|
+
result.messages = sortLintMessages([...result.messages, ...filtered.messages]);
|
|
926
1101
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
927
1102
|
applyResultFixes(result, effectiveCode, options);
|
|
928
1103
|
finalizeESLintResult(result);
|
|
929
1104
|
return results;
|
|
930
1105
|
}
|
|
931
1106
|
|
|
1107
|
+
// When custom fixes change a file, the native messages computed earlier are
|
|
1108
|
+
// stale. Re-lint the fixed text with native rules and custom rules together
|
|
1109
|
+
// until neither changes it, then rebuild the result from that final pass.
|
|
1110
|
+
function relintAfterCustomFixes(result, code, filePath, customRules, config, rules, options) {
|
|
1111
|
+
if (!options.fix) {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
const nativeOptions = {
|
|
1115
|
+
...lintOptionsWithoutCustomRules(options, rules),
|
|
1116
|
+
filePath,
|
|
1117
|
+
filename: undefined,
|
|
1118
|
+
fix: true,
|
|
1119
|
+
useDefaultConfigTargets: undefined,
|
|
1120
|
+
pluginTextFilePath: undefined
|
|
1121
|
+
};
|
|
1122
|
+
const parserOptions = { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true };
|
|
1123
|
+
const customLintOptions = {
|
|
1124
|
+
cwd: options.cwd,
|
|
1125
|
+
filename: filePath,
|
|
1126
|
+
config: calculatedConfig({ cwd: options.cwd, noConfig: true, baseConfig: options.baseConfig, overrideConfig: options.overrideConfig }, filePath)
|
|
1127
|
+
};
|
|
1128
|
+
const lintCustom = (text) => {
|
|
1129
|
+
const sourceCode = createLinterSourceCode(text, null, parserOptions);
|
|
1130
|
+
return applyDisableDirectives(runCustomLinterRules(customRules, sourceCode, customLintOptions), sourceCode);
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
let output = code;
|
|
1134
|
+
let custom = lintCustom(output);
|
|
1135
|
+
let fixedOutput = applyMessageFixes(output, custom.messages);
|
|
1136
|
+
if (fixedOutput === output) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
let nativeReport;
|
|
1140
|
+
for (let pass = 0; pass < MAX_AUTOFIX_PASSES && fixedOutput !== output; pass += 1) {
|
|
1141
|
+
nativeReport = lintText(fixedOutput, nativeOptions);
|
|
1142
|
+
output = nativeReport.outputs?.find((fixed) => fixed.filePath === filePath)?.output ?? fixedOutput;
|
|
1143
|
+
custom = lintCustom(output);
|
|
1144
|
+
fixedOutput = applyMessageFixes(output, custom.messages);
|
|
1145
|
+
}
|
|
1146
|
+
if (output !== fixedOutput) {
|
|
1147
|
+
output = fixedOutput;
|
|
1148
|
+
nativeReport = lintText(output, nativeOptions);
|
|
1149
|
+
custom = lintCustom(output);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const nativeResult = reportToESLintResults(nativeReport, {
|
|
1153
|
+
source: result.source,
|
|
1154
|
+
filePath,
|
|
1155
|
+
includeEmptyTextResult: true,
|
|
1156
|
+
ruleSeverityForFile: (path) => ruleSeverityMapForOptions(nativeOptions, path)
|
|
1157
|
+
})[0];
|
|
1158
|
+
result.messages = sortLintMessages([...(nativeResult?.messages ?? []), ...custom.messages]);
|
|
1159
|
+
result.suppressedMessages = [...(nativeResult?.suppressedMessages ?? []), ...custom.suppressedMessages];
|
|
1160
|
+
result.output = output;
|
|
1161
|
+
finalizeESLintResult(result);
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
932
1165
|
function appendCustomLintFileMessages(results, config, options) {
|
|
933
1166
|
for (const result of results) {
|
|
934
1167
|
const filePath = result.filePath;
|
|
@@ -945,7 +1178,7 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
945
1178
|
continue;
|
|
946
1179
|
}
|
|
947
1180
|
}
|
|
948
|
-
const sourceCode = createLinterSourceCode(code);
|
|
1181
|
+
const sourceCode = createLinterSourceCode(code, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
|
|
949
1182
|
const messages = runCustomLinterRules(customRules, sourceCode, {
|
|
950
1183
|
cwd: options.cwd,
|
|
951
1184
|
filename: filePath,
|
|
@@ -955,7 +1188,10 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
955
1188
|
if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
|
|
956
1189
|
continue;
|
|
957
1190
|
}
|
|
958
|
-
result
|
|
1191
|
+
if (relintAfterCustomFixes(result, code, filePath, customRules, config, rules, options)) {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
result.messages = sortLintMessages([...result.messages, ...filtered.messages]);
|
|
959
1195
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
960
1196
|
applyResultFixes(result, code, options);
|
|
961
1197
|
finalizeESLintResult(result);
|
|
@@ -963,11 +1199,22 @@ function appendCustomLintFileMessages(results, config, options) {
|
|
|
963
1199
|
return results;
|
|
964
1200
|
}
|
|
965
1201
|
|
|
1202
|
+
// ESLint orders a file's messages by position; native and plugin messages
|
|
1203
|
+
// arrive as two separate batches.
|
|
1204
|
+
function sortLintMessages(messages) {
|
|
1205
|
+
return messages
|
|
1206
|
+
.map((message, index) => ({ message, index }))
|
|
1207
|
+
.sort((left, right) => (left.message.line ?? 0) - (right.message.line ?? 0)
|
|
1208
|
+
|| (left.message.column ?? 0) - (right.message.column ?? 0)
|
|
1209
|
+
|| left.index - right.index)
|
|
1210
|
+
.map(({ message }) => message);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
966
1213
|
function applyResultFixes(result, code, options) {
|
|
967
1214
|
if (!options.fix) {
|
|
968
1215
|
return;
|
|
969
1216
|
}
|
|
970
|
-
const output =
|
|
1217
|
+
const output = applyMessageFixes(code, result.messages);
|
|
971
1218
|
if (output !== code) {
|
|
972
1219
|
result.output = output;
|
|
973
1220
|
}
|
|
@@ -980,15 +1227,40 @@ function runCustomLinterRules(ruleEntries, sourceCode, options) {
|
|
|
980
1227
|
|
|
981
1228
|
const messages = [];
|
|
982
1229
|
const program = sourceCode.ast ?? { type: "Program", range: [0, sourceCode.text.length] };
|
|
1230
|
+
const hasAst = Array.isArray(program.body);
|
|
1231
|
+
const listenersBySelector = new Map();
|
|
983
1232
|
for (const ruleEntry of ruleEntries) {
|
|
1233
|
+
if (typeof ruleEntry.rule !== "function" && typeof ruleEntry.rule?.create !== "function") {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`utoo-lint: rule "${ruleEntry.ruleId}" has no create() function. ` +
|
|
1236
|
+
"ESLint plugins must be mounted from a utlint.config.ts (or .js) module so their rule objects are available at lint time."
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
984
1239
|
const context = createCustomRuleContext(ruleEntry, sourceCode, options, messages);
|
|
985
1240
|
const listeners = customRuleListeners(ruleEntry.rule, context);
|
|
986
|
-
if (
|
|
987
|
-
traverseCustomRuleAst(program, listeners, context, sourceCode.visitorKeys);
|
|
988
|
-
} else {
|
|
1241
|
+
if (!hasAst) {
|
|
989
1242
|
runCustomRuleTokenListeners(program, sourceCode.tokens, listeners, context);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
for (const [selector, listener] of Object.entries(listeners)) {
|
|
1246
|
+
if (typeof listener !== "function") {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (!listenersBySelector.has(selector)) {
|
|
1250
|
+
listenersBySelector.set(selector, []);
|
|
1251
|
+
}
|
|
1252
|
+
listenersBySelector.get(selector).push({
|
|
1253
|
+
ruleId: ruleEntry.ruleId,
|
|
1254
|
+
listener(node) {
|
|
1255
|
+
context.setCurrentNode(node);
|
|
1256
|
+
listener(node);
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
990
1259
|
}
|
|
991
1260
|
}
|
|
1261
|
+
if (hasAst && listenersBySelector.size > 0) {
|
|
1262
|
+
pluginRunner.runRuleListeners(program, listenersBySelector, { visitorKeys: sourceCode.visitorKeys });
|
|
1263
|
+
}
|
|
992
1264
|
return messages;
|
|
993
1265
|
}
|
|
994
1266
|
|
|
@@ -1144,231 +1416,6 @@ function disableDirectiveMatchesRule(directive, ruleId) {
|
|
|
1144
1416
|
return directive.ruleId == null || directive.ruleId === ruleId;
|
|
1145
1417
|
}
|
|
1146
1418
|
|
|
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
1419
|
function runCustomRuleTokenListeners(program, tokens, listeners, context) {
|
|
1373
1420
|
if (typeof listeners.Program === "function") {
|
|
1374
1421
|
context.setCurrentNode(program);
|
|
@@ -1403,7 +1450,7 @@ function createCustomRuleContext(ruleEntry, sourceCode, options, messages) {
|
|
|
1403
1450
|
let currentNode = sourceCode.ast ?? null;
|
|
1404
1451
|
const context = {
|
|
1405
1452
|
id: ruleEntry.ruleId,
|
|
1406
|
-
options: ruleEntry.options,
|
|
1453
|
+
options: pluginRunner.applyRuleOptionDefaults(ruleEntry.rule, ruleEntry.options),
|
|
1407
1454
|
filename,
|
|
1408
1455
|
physicalFilename: filename,
|
|
1409
1456
|
cwd,
|
|
@@ -1621,7 +1668,7 @@ export class SourceCode {
|
|
|
1621
1668
|
this.text = textOrConfig.text;
|
|
1622
1669
|
this.ast = textOrConfig.ast ?? null;
|
|
1623
1670
|
this.parserServices = textOrConfig.parserServices ?? {};
|
|
1624
|
-
this
|
|
1671
|
+
defineScopeManager(this, textOrConfig.scopeManager ?? null);
|
|
1625
1672
|
this.visitorKeys = textOrConfig.visitorKeys ?? null;
|
|
1626
1673
|
this.hasBOM = Boolean(textOrConfig.hasBOM);
|
|
1627
1674
|
} else {
|
|
@@ -1813,7 +1860,17 @@ export class SourceCode {
|
|
|
1813
1860
|
}
|
|
1814
1861
|
|
|
1815
1862
|
getAncestors(node) {
|
|
1816
|
-
|
|
1863
|
+
if (!node) {
|
|
1864
|
+
return [];
|
|
1865
|
+
}
|
|
1866
|
+
if (node.parent !== undefined) {
|
|
1867
|
+
const ancestors = [];
|
|
1868
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1869
|
+
ancestors.unshift(current);
|
|
1870
|
+
}
|
|
1871
|
+
return ancestors;
|
|
1872
|
+
}
|
|
1873
|
+
return sourceAncestorsForNode(this.ast, node) ?? [];
|
|
1817
1874
|
}
|
|
1818
1875
|
|
|
1819
1876
|
getDeclaredVariables(node) {
|
|
@@ -1821,10 +1878,20 @@ export class SourceCode {
|
|
|
1821
1878
|
}
|
|
1822
1879
|
|
|
1823
1880
|
getScope(node) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1881
|
+
const scopeManager = this.scopeManager;
|
|
1882
|
+
if (!scopeManager || typeof scopeManager.acquire !== "function") {
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
if (node) {
|
|
1886
|
+
const inner = node.type !== "Program";
|
|
1887
|
+
for (let current = node; current; current = current.parent) {
|
|
1888
|
+
const scope = scopeManager.acquire(current, inner);
|
|
1889
|
+
if (scope) {
|
|
1890
|
+
return scope.type === "function-expression-name" ? scope.childScopes[0] : scope;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1826
1893
|
}
|
|
1827
|
-
return
|
|
1894
|
+
return scopeManager.scopes?.[0] ?? scopeManager.globalScope ?? null;
|
|
1828
1895
|
}
|
|
1829
1896
|
|
|
1830
1897
|
markVariableAsUsed(name, node) {
|
|
@@ -1860,6 +1927,31 @@ export class SourceCode {
|
|
|
1860
1927
|
}
|
|
1861
1928
|
}
|
|
1862
1929
|
|
|
1930
|
+
function defineScopeManager(sourceCode, scopeManager) {
|
|
1931
|
+
if (typeof scopeManager !== "function") {
|
|
1932
|
+
sourceCode.scopeManager = scopeManager;
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
// Scope analysis is comparatively expensive; run it only when a rule asks.
|
|
1936
|
+
let computed = false;
|
|
1937
|
+
let value = null;
|
|
1938
|
+
Object.defineProperty(sourceCode, "scopeManager", {
|
|
1939
|
+
configurable: true,
|
|
1940
|
+
enumerable: true,
|
|
1941
|
+
get() {
|
|
1942
|
+
if (!computed) {
|
|
1943
|
+
computed = true;
|
|
1944
|
+
value = scopeManager() ?? null;
|
|
1945
|
+
}
|
|
1946
|
+
return value;
|
|
1947
|
+
},
|
|
1948
|
+
set(next) {
|
|
1949
|
+
computed = true;
|
|
1950
|
+
value = next;
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1863
1955
|
function sourceLineStartIndices(text) {
|
|
1864
1956
|
const indices = [0];
|
|
1865
1957
|
for (let index = 0; index < text.length; index += 1) {
|
|
@@ -2149,12 +2241,10 @@ export class RuleTester {
|
|
|
2149
2241
|
RuleTester[testCase.only ? "itOnly" : "it"](testCase.name ?? testCase.code, () => {
|
|
2150
2242
|
const config = ruleTesterConfig(ruleName, this.config, testCase, "error");
|
|
2151
2243
|
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;
|
|
2244
|
+
const messages = linter.verify(testCase.code, config, options);
|
|
2156
2245
|
assertRuleTesterErrors(testCase, messages, rule);
|
|
2157
|
-
|
|
2246
|
+
// Like ESLint's RuleTester, `output` is the result of one autofix pass.
|
|
2247
|
+
assertRuleTesterOutput(testCase, Object.hasOwn(testCase, "output") ? applyMessageFixes(testCase.code, messages) : testCase.code);
|
|
2158
2248
|
});
|
|
2159
2249
|
}
|
|
2160
2250
|
});
|
|
@@ -2301,6 +2391,47 @@ function applyRuleFixes(code, fix) {
|
|
|
2301
2391
|
.reduce((output, item) => output.slice(0, item.range[0]) + item.text + output.slice(item.range[1]), code);
|
|
2302
2392
|
}
|
|
2303
2393
|
|
|
2394
|
+
// Applies one non-overlapping fix per message, like ESLint's SourceCodeFixer.
|
|
2395
|
+
// Fixes that overlap an earlier one are left for a later pass.
|
|
2396
|
+
function applyMessageFixes(code, messages) {
|
|
2397
|
+
const fixes = [];
|
|
2398
|
+
for (const message of messages) {
|
|
2399
|
+
const items = ruleFixItems(message?.fix)
|
|
2400
|
+
.slice()
|
|
2401
|
+
.sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
2402
|
+
if (items.length === 0) {
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
let text = "";
|
|
2406
|
+
let cursor = items[0].range[0];
|
|
2407
|
+
let valid = true;
|
|
2408
|
+
for (const item of items) {
|
|
2409
|
+
if (item.range[0] < cursor) {
|
|
2410
|
+
valid = false;
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
text += code.slice(cursor, item.range[0]) + item.text;
|
|
2414
|
+
cursor = item.range[1];
|
|
2415
|
+
}
|
|
2416
|
+
if (valid) {
|
|
2417
|
+
fixes.push({ range: [items[0].range[0], cursor], text });
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
fixes.sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
2421
|
+
let output = "";
|
|
2422
|
+
let position = 0;
|
|
2423
|
+
let lastEnd = -Infinity;
|
|
2424
|
+
for (const fix of fixes) {
|
|
2425
|
+
if (lastEnd >= fix.range[0]) {
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
output += code.slice(position, fix.range[0]) + fix.text;
|
|
2429
|
+
position = fix.range[1];
|
|
2430
|
+
lastEnd = fix.range[1];
|
|
2431
|
+
}
|
|
2432
|
+
return output + code.slice(position);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2304
2435
|
function ruleFixItems(fix) {
|
|
2305
2436
|
if (!fix) {
|
|
2306
2437
|
return [];
|
|
@@ -2353,10 +2484,29 @@ function createLinterSourceCode(text, parser = null, parserOptions = {}) {
|
|
|
2353
2484
|
});
|
|
2354
2485
|
}
|
|
2355
2486
|
|
|
2356
|
-
function
|
|
2357
|
-
|
|
2487
|
+
function parseSourceCodeWithDefaultParser(text, parserOptions) {
|
|
2488
|
+
let parsed;
|
|
2489
|
+
try {
|
|
2490
|
+
parsed = estreeParser.parseForESLint(text, parserOptions);
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
if (parserOptions.requireParser) {
|
|
2493
|
+
throw error;
|
|
2494
|
+
}
|
|
2358
2495
|
return null;
|
|
2359
2496
|
}
|
|
2497
|
+
return new SourceCode({
|
|
2498
|
+
text,
|
|
2499
|
+
ast: parsed.ast,
|
|
2500
|
+
parserServices: parsed.services ?? {},
|
|
2501
|
+
scopeManager: parsed.scopeManager ?? null,
|
|
2502
|
+
visitorKeys: parsed.visitorKeys ?? null
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
function parseSourceCode(text, parser, parserOptions = {}) {
|
|
2507
|
+
if (!parser) {
|
|
2508
|
+
return parserOptions.tokensOnly ? null : parseSourceCodeWithDefaultParser(text, parserOptions);
|
|
2509
|
+
}
|
|
2360
2510
|
if (typeof parser.parseForESLint === "function") {
|
|
2361
2511
|
const result = parser.parseForESLint(text, parserOptions) ?? {};
|
|
2362
2512
|
return new SourceCode({
|
|
@@ -3128,21 +3278,210 @@ export function lintFiles(paths, options = {}) {
|
|
|
3128
3278
|
}
|
|
3129
3279
|
|
|
3130
3280
|
const nativeFlatOptions = nativeFlatConfigOptions(options);
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3281
|
+
const report = nativeFlatOptions
|
|
3282
|
+
? runNativeLintReport(lintPaths, nativeFlatOptions)
|
|
3283
|
+
: runNativeConfigRunReports(lintPaths, options);
|
|
3284
|
+
// The flat-config path resolves one config for the whole run; only the
|
|
3285
|
+
// per-file path has already discovered (and cached) nested configs.
|
|
3286
|
+
appendPluginRuleDiagnostics(report, options, { perFileConfig: !nativeFlatOptions });
|
|
3287
|
+
return finalizeLintReport(report, ignoredDiagnostics, options);
|
|
3288
|
+
}
|
|
3135
3289
|
|
|
3290
|
+
function runNativeConfigRunReports(lintPaths, options) {
|
|
3136
3291
|
const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
|
|
3137
3292
|
if (configRuns) {
|
|
3138
|
-
|
|
3293
|
+
return mergeNativeReports(
|
|
3139
3294
|
configRuns.map(({ paths: runPaths, options: runOptions }) => runNativeLintReport(runPaths, runOptions))
|
|
3140
3295
|
);
|
|
3141
|
-
return finalizeLintReport(report, ignoredDiagnostics, options);
|
|
3142
3296
|
}
|
|
3143
3297
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3298
|
+
return runNativeLintReport(lintPaths, options);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function pluginFixModeForOptions(options) {
|
|
3302
|
+
const extraArgs = Array.isArray(options.extraArgs) ? options.extraArgs : [];
|
|
3303
|
+
if (options.fix || extraArgs.includes("--fix-dry-run")) {
|
|
3304
|
+
return "dry-run";
|
|
3305
|
+
}
|
|
3306
|
+
return extraArgs.includes("--fix") ? "write" : null;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function pluginMessageToDiagnostic(message, filePath) {
|
|
3310
|
+
const toFix = (fix) => ({ range: [fix.range[0], fix.range[1]], text: fix.text });
|
|
3311
|
+
const diagnostic = {
|
|
3312
|
+
filePath,
|
|
3313
|
+
line: message.line,
|
|
3314
|
+
column: message.column,
|
|
3315
|
+
severity: message.severity === 2 ? "error" : "warning",
|
|
3316
|
+
message: message.message,
|
|
3317
|
+
ruleId: message.ruleId,
|
|
3318
|
+
fixes: ruleFixItems(message.fix).map(toFix),
|
|
3319
|
+
suggestions: (message.suggestions ?? []).map((suggestion) => ({
|
|
3320
|
+
desc: suggestion.desc,
|
|
3321
|
+
fix: ruleFixItems(suggestion.fix).map(toFix)
|
|
3322
|
+
}))
|
|
3323
|
+
};
|
|
3324
|
+
if (typeof message.endLine === "number") {
|
|
3325
|
+
diagnostic.endLine = message.endLine;
|
|
3326
|
+
}
|
|
3327
|
+
if (typeof message.endColumn === "number") {
|
|
3328
|
+
diagnostic.endColumn = message.endColumn;
|
|
3329
|
+
}
|
|
3330
|
+
return diagnostic;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
function sortReportDiagnostics(report) {
|
|
3334
|
+
const order = new Map((report.filePaths ?? []).map((filePath, index) => [filePath, index]));
|
|
3335
|
+
const rank = (diagnostic) => order.get(diagnostic.filePath) ?? Number.MAX_SAFE_INTEGER;
|
|
3336
|
+
const compare = (left, right) => rank(left) - rank(right)
|
|
3337
|
+
|| (left.line ?? 0) - (right.line ?? 0)
|
|
3338
|
+
|| (left.column ?? 0) - (right.column ?? 0);
|
|
3339
|
+
report.diagnostics = [...(report.diagnostics ?? [])].sort(compare);
|
|
3340
|
+
report.suppressedDiagnostics = [...(report.suppressedDiagnostics ?? [])].sort(compare);
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// Runs the ESLint plugin rules a config mounts and merges their diagnostics
|
|
3344
|
+
// into the native report. Plugin rules never reach the native binary; see
|
|
3345
|
+
// `withoutJsPluginRules`.
|
|
3346
|
+
function appendPluginRuleDiagnostics(report, options, { perFileConfig = true } = {}) {
|
|
3347
|
+
if (options.noConfig && !options.baseConfig && !options.overrideConfig) {
|
|
3348
|
+
return report;
|
|
3349
|
+
}
|
|
3350
|
+
const filePaths = report.filePaths ?? [];
|
|
3351
|
+
if (filePaths.length === 0 || !anyConfigDeclaresPlugins(options)) {
|
|
3352
|
+
return report;
|
|
3353
|
+
}
|
|
3354
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3355
|
+
const singleMatchPath = filePaths.length === 1
|
|
3356
|
+
? options.filePath ?? options.filename ?? options.pluginTextFilePath
|
|
3357
|
+
: undefined;
|
|
3358
|
+
const fixMode = pluginFixModeForOptions(options);
|
|
3359
|
+
let appended = false;
|
|
3360
|
+
|
|
3361
|
+
for (const filePath of filePaths) {
|
|
3362
|
+
const matchPath = singleMatchPath ?? filePath;
|
|
3363
|
+
const ruleMap = jsPluginRuleMapForFile(options, matchPath, perFileConfig);
|
|
3364
|
+
if (ruleMap.size === 0) {
|
|
3365
|
+
continue;
|
|
3366
|
+
}
|
|
3367
|
+
const ruleEntries = pluginRuleEntriesForFile(options, matchPath, ruleMap, perFileConfig);
|
|
3368
|
+
if (ruleEntries.length === 0) {
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// The native report echoes targets as given, so relative paths resolve
|
|
3373
|
+
// against the lint cwd rather than the process cwd.
|
|
3374
|
+
const absolutePath = normalizeESLintFilePath(filePath, cwd);
|
|
3375
|
+
const outputIndex = (report.outputs ?? []).findIndex((fixed) => fixed.filePath === filePath);
|
|
3376
|
+
let code = outputIndex === -1 ? undefined : report.outputs[outputIndex].output;
|
|
3377
|
+
if (typeof code !== "string") {
|
|
3378
|
+
try {
|
|
3379
|
+
code = readFileSync(absolutePath, "utf8");
|
|
3380
|
+
} catch {
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
const config = pluginConfigSourcesForFile(options, matchPath, perFileConfig).map((source) => source.config);
|
|
3386
|
+
const parserOptions = { ...parserOptionsForConfig(config, matchPath, cwd), filePath: matchPath, requireParser: true };
|
|
3387
|
+
const lintOptions = {
|
|
3388
|
+
cwd,
|
|
3389
|
+
filename: matchPath,
|
|
3390
|
+
config: calculatedConfig({ cwd, noConfig: true, baseConfig: config }, matchPath)
|
|
3391
|
+
};
|
|
3392
|
+
const lintOnce = (text) => {
|
|
3393
|
+
const sourceCode = createLinterSourceCode(text, null, parserOptions);
|
|
3394
|
+
return applyDisableDirectives(runCustomLinterRules(ruleEntries, sourceCode, lintOptions), sourceCode);
|
|
3395
|
+
};
|
|
3396
|
+
|
|
3397
|
+
let filtered = lintOnce(code);
|
|
3398
|
+
let relintedFromReport = false;
|
|
3399
|
+
if (fixMode) {
|
|
3400
|
+
let changed = false;
|
|
3401
|
+
for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
|
|
3402
|
+
const output = applyMessageFixes(code, filtered.messages);
|
|
3403
|
+
if (output === code) {
|
|
3404
|
+
break;
|
|
3405
|
+
}
|
|
3406
|
+
changed = true;
|
|
3407
|
+
code = output;
|
|
3408
|
+
filtered = lintOnce(code);
|
|
3409
|
+
}
|
|
3410
|
+
if (changed) {
|
|
3411
|
+
// Plugin fixes changed the text, so the native diagnostics for this
|
|
3412
|
+
// file describe stale source. Re-lint the fixed text through the whole
|
|
3413
|
+
// pipeline (native rules, plugin rules, further fixes) and replace this
|
|
3414
|
+
// file's diagnostics with the result.
|
|
3415
|
+
const depth = options.pluginFixDepth ?? 0;
|
|
3416
|
+
if (depth < MAX_AUTOFIX_PASSES) {
|
|
3417
|
+
const relinted = lintText(code, {
|
|
3418
|
+
...options,
|
|
3419
|
+
filePath: matchPath,
|
|
3420
|
+
filename: undefined,
|
|
3421
|
+
config: pluginConfigPathForFile(options, matchPath, perFileConfig) ?? options.config,
|
|
3422
|
+
fix: true,
|
|
3423
|
+
extraArgs: (options.extraArgs ?? []).filter((arg) => arg !== "--fix" && arg !== "--fix-dry-run"),
|
|
3424
|
+
useDefaultConfigTargets: undefined,
|
|
3425
|
+
pluginTextFilePath: undefined,
|
|
3426
|
+
pluginFixDepth: depth + 1
|
|
3427
|
+
});
|
|
3428
|
+
code = relinted.outputs?.find((fixed) => fixed.filePath === matchPath)?.output ?? code;
|
|
3429
|
+
filtered = {
|
|
3430
|
+
messages: relinted.diagnostics ?? [],
|
|
3431
|
+
suppressedMessages: relinted.suppressedDiagnostics ?? []
|
|
3432
|
+
};
|
|
3433
|
+
report.diagnostics = (report.diagnostics ?? []).filter((diagnostic) => diagnostic.filePath !== filePath);
|
|
3434
|
+
report.suppressedDiagnostics = (report.suppressedDiagnostics ?? []).filter((diagnostic) => diagnostic.filePath !== filePath);
|
|
3435
|
+
relintedFromReport = true;
|
|
3436
|
+
}
|
|
3437
|
+
if (fixMode === "write") {
|
|
3438
|
+
writeFileSync(absolutePath, code);
|
|
3439
|
+
} else if (outputIndex === -1) {
|
|
3440
|
+
report.outputs = [...(report.outputs ?? []), { filePath, output: code }];
|
|
3441
|
+
} else {
|
|
3442
|
+
report.outputs[outputIndex] = { ...report.outputs[outputIndex], output: code };
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
if (relintedFromReport) {
|
|
3448
|
+
// Already native-shaped diagnostics from the nested lint; only the
|
|
3449
|
+
// file path needs to match this report.
|
|
3450
|
+
report.diagnostics = [
|
|
3451
|
+
...(report.diagnostics ?? []),
|
|
3452
|
+
...filtered.messages.map((diagnostic) => ({ ...diagnostic, filePath }))
|
|
3453
|
+
];
|
|
3454
|
+
report.suppressedDiagnostics = [
|
|
3455
|
+
...(report.suppressedDiagnostics ?? []),
|
|
3456
|
+
...filtered.suppressedMessages.map((diagnostic) => ({ ...diagnostic, filePath }))
|
|
3457
|
+
];
|
|
3458
|
+
appended = true;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
|
|
3462
|
+
continue;
|
|
3463
|
+
}
|
|
3464
|
+
report.diagnostics = [
|
|
3465
|
+
...(report.diagnostics ?? []),
|
|
3466
|
+
...filtered.messages.map((message) => pluginMessageToDiagnostic(message, filePath))
|
|
3467
|
+
];
|
|
3468
|
+
report.suppressedDiagnostics = [
|
|
3469
|
+
...(report.suppressedDiagnostics ?? []),
|
|
3470
|
+
...filtered.suppressedMessages.map((message) => ({
|
|
3471
|
+
...pluginMessageToDiagnostic(message, filePath),
|
|
3472
|
+
suppression: {
|
|
3473
|
+
kind: message.suppressions?.[0]?.kind ?? "directive",
|
|
3474
|
+
justification: message.suppressions?.[0]?.justification ?? ""
|
|
3475
|
+
}
|
|
3476
|
+
}))
|
|
3477
|
+
];
|
|
3478
|
+
appended = true;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
if (appended) {
|
|
3482
|
+
sortReportDiagnostics(report);
|
|
3483
|
+
}
|
|
3484
|
+
return report;
|
|
3146
3485
|
}
|
|
3147
3486
|
|
|
3148
3487
|
function nativeFlatConfigOptions(options) {
|
|
@@ -3172,6 +3511,10 @@ function nativeFlatConfigOptions(options) {
|
|
|
3172
3511
|
return undefined;
|
|
3173
3512
|
}
|
|
3174
3513
|
|
|
3514
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(config);
|
|
3515
|
+
if (jsPluginRuleIds.size > 0) {
|
|
3516
|
+
config = configWithoutCustomRules(config, jsPluginRuleIds);
|
|
3517
|
+
}
|
|
3175
3518
|
if (configCanUseNativeRulesFastPath(config)) {
|
|
3176
3519
|
return undefined;
|
|
3177
3520
|
}
|
|
@@ -3298,6 +3641,9 @@ function nativeConfigRunsForFiles(paths, options) {
|
|
|
3298
3641
|
if (configured && options.rules) {
|
|
3299
3642
|
rules = selectedRulesWithConfigOptions(rules, options.rules);
|
|
3300
3643
|
}
|
|
3644
|
+
if (configured) {
|
|
3645
|
+
rules = withoutJsPluginRules(rules, jsPluginRuleIdsForOptions(options, matchPath));
|
|
3646
|
+
}
|
|
3301
3647
|
const signature = configured ? stableConfigSignature({ rules, settings }) : "<native-defaults>";
|
|
3302
3648
|
if (!groups.has(signature)) {
|
|
3303
3649
|
groups.set(signature, { paths: [], rules, settings, configured });
|
|
@@ -3470,7 +3816,8 @@ export function lintText(code, options = {}) {
|
|
|
3470
3816
|
cwd: options.cwd,
|
|
3471
3817
|
config: options.config ?? discoveredConfig,
|
|
3472
3818
|
noConfig: options.noConfig,
|
|
3473
|
-
deferDiagnosticConfigFiltering: true
|
|
3819
|
+
deferDiagnosticConfigFiltering: true,
|
|
3820
|
+
pluginTextFilePath: requestedPath
|
|
3474
3821
|
});
|
|
3475
3822
|
report.filePaths = (report.filePaths ?? []).map((filePath) => (filePath === tempFile ? requestedPath : filePath));
|
|
3476
3823
|
report.outputs = (report.outputs ?? []).map((fixed) => ({
|
|
@@ -3512,11 +3859,12 @@ function buildNativeLintArgs(paths, options) {
|
|
|
3512
3859
|
const configForRuleSelection = options.noConfig
|
|
3513
3860
|
? undefined
|
|
3514
3861
|
: options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
|
|
3862
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(configForRuleSelection, options.baseConfig, options.overrideConfig);
|
|
3515
3863
|
const enabledRules = enabledRuleNamesFromConfigs(
|
|
3516
3864
|
configForRuleSelection,
|
|
3517
3865
|
options.baseConfig,
|
|
3518
3866
|
options.overrideConfig
|
|
3519
|
-
);
|
|
3867
|
+
).filter((rule) => !jsPluginRuleIds.has(rule));
|
|
3520
3868
|
if (enabledRules.length > 0) {
|
|
3521
3869
|
cliArgs.push(`--rules=${enabledRules.join(",")}`);
|
|
3522
3870
|
}
|
|
@@ -3994,9 +4342,11 @@ function withTemporaryConfig(options, callback) {
|
|
|
3994
4342
|
? readConfig(fileConfigPath, options.cwd, options.configCache)
|
|
3995
4343
|
: undefined;
|
|
3996
4344
|
const configs = [options.baseConfig, fileConfig, options.overrideConfig];
|
|
3997
|
-
const
|
|
3998
|
-
|
|
3999
|
-
: runtimeRulesFromConfigs(...configs)
|
|
4345
|
+
const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(...configs);
|
|
4346
|
+
const rules = withoutJsPluginRules(
|
|
4347
|
+
shouldMaterializeFileConfig ? materializedRulesFromConfigs(...configs) : runtimeRulesFromConfigs(...configs),
|
|
4348
|
+
jsPluginRuleIds
|
|
4349
|
+
);
|
|
4000
4350
|
const settings = configs.reduce(
|
|
4001
4351
|
(result, config) => ({
|
|
4002
4352
|
...result,
|
|
@@ -4015,7 +4365,7 @@ function withTemporaryConfig(options, callback) {
|
|
|
4015
4365
|
}
|
|
4016
4366
|
return callback(options);
|
|
4017
4367
|
}
|
|
4018
|
-
const enabledRules = enabledRuleNamesFromConfigs(...configs);
|
|
4368
|
+
const enabledRules = enabledRuleNamesFromConfigs(...configs).filter((rule) => !jsPluginRuleIds.has(rule));
|
|
4019
4369
|
const hasExplicitOffRules = Object.values(rules).some((value) => ruleConfigSeverity(value) === 0);
|
|
4020
4370
|
if (!shouldMaterializeFileConfig && !hasSettings && !hasRuleOptions(rules) && !hasExplicitOffRules && !options.forceMaterializedConfig) {
|
|
4021
4371
|
return callback({
|
|
@@ -4201,7 +4551,7 @@ function applyCompatibilityDisableDirectives(result, textOptions) {
|
|
|
4201
4551
|
return;
|
|
4202
4552
|
}
|
|
4203
4553
|
|
|
4204
|
-
const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source));
|
|
4554
|
+
const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source, null, { tokensOnly: true }));
|
|
4205
4555
|
result.messages = filtered.messages;
|
|
4206
4556
|
result.suppressedMessages.push(...filtered.suppressedMessages);
|
|
4207
4557
|
}
|
|
@@ -4935,6 +5285,12 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
|
|
|
4935
5285
|
column: diagnostic.column,
|
|
4936
5286
|
nodeType: null
|
|
4937
5287
|
};
|
|
5288
|
+
if (typeof diagnostic.endLine === "number") {
|
|
5289
|
+
message.endLine = diagnostic.endLine;
|
|
5290
|
+
}
|
|
5291
|
+
if (typeof diagnostic.endColumn === "number") {
|
|
5292
|
+
message.endColumn = diagnostic.endColumn;
|
|
5293
|
+
}
|
|
4938
5294
|
if (diagnostic.endLine != null) {
|
|
4939
5295
|
message.endLine = diagnostic.endLine;
|
|
4940
5296
|
}
|