@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/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 (customRuleConfig.length === 0 || typeof result.filePath !== "string") {
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, customRuleMapForConfig(customRuleConfig, new Map(), filePath, options.cwd));
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 = applyRuleFixes(output, messages.flatMap((message) => ruleFixItems(message.fix)));
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
- return {
805
+ const languageOptions = configData.languageOptions ?? {};
806
+ const parserOptions = {
805
807
  ...(configData.parserOptions ?? {}),
806
- ...(configData.languageOptions?.parserOptions ?? {}),
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
- rules.set(`${pluginName}/${ruleName}`, rule);
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.messages.push(...filtered.messages);
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.messages.push(...filtered.messages);
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 = applyRuleFixes(code, result.messages.flatMap((message) => ruleFixItems(message.fix)));
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 (customRuleChildNodes(program, sourceCode.visitorKeys).length > 0) {
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.scopeManager = textOrConfig.scopeManager ?? null;
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
- return node ? sourceAncestorsForNode(this.ast, node) ?? [] : [];
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
- if (node && typeof this.scopeManager?.acquire === "function") {
1825
- return this.scopeManager.acquire(node, true) ?? this.scopeManager.acquire(node, false) ?? this.scopeManager.globalScope ?? null;
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 this.scopeManager?.globalScope ?? null;
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 result = Object.hasOwn(testCase, "output")
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
- assertRuleTesterOutput(testCase, result.output);
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 parseSourceCode(text, parser, parserOptions) {
2357
- if (!parser) {
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
- if (nativeFlatOptions) {
3132
- const report = runNativeLintReport(lintPaths, nativeFlatOptions);
3133
- return finalizeLintReport(report, ignoredDiagnostics, options);
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
- const report = mergeNativeReports(
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
- const report = runNativeLintReport(lintPaths, options);
3145
- return finalizeLintReport(report, ignoredDiagnostics, options);
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 rules = shouldMaterializeFileConfig
3998
- ? materializedRulesFromConfigs(...configs)
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
  }