@utoo/lint 0.3.8 → 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;
@@ -400,6 +400,10 @@ const BUILTIN_RULE_IDS = [
400
400
  "react/void-dom-elements-no-children",
401
401
  "react-hooks/exhaustive-deps",
402
402
  "react-hooks/rules-of-hooks",
403
+ "react-hooks/use-memo",
404
+ "react-hooks/void-use-memo",
405
+ "react-hooks/set-state-in-render",
406
+ "react-hooks/purity",
403
407
  "unused-imports/no-unused-imports",
404
408
  "@typescript-eslint/adjacent-overload-signatures",
405
409
  "@typescript-eslint/array-type",
@@ -479,6 +483,7 @@ const NATIVE_ONLY_RULE_IDS = [
479
483
  "semantic-errors"
480
484
  ];
481
485
  const NATIVE_RULE_IDS = [...BUILTIN_RULE_IDS, ...NATIVE_ONLY_RULE_IDS];
486
+ const NATIVE_RULE_ID_SET = new Set(NATIVE_RULE_IDS);
482
487
  const FIXABLE_BUILTIN_RULE_IDS = new Set([
483
488
  "@typescript-eslint/no-explicit-any",
484
489
  "jest/no-deprecated-functions",
@@ -606,15 +611,15 @@ export class UtooLint {
606
611
  throw new Error("'results' must be an array");
607
612
  }
608
613
  const options = eslintConstructorOptions(this.options);
609
- const customRuleConfig = [options.baseConfig, options.overrideConfig].filter(Boolean);
610
614
  const rulesByFilePath = new Map();
615
+ const cachedOptions = withConfigCache(options);
611
616
  return rulesMetaForResults(results, (ruleId, result) => {
612
- if (customRuleConfig.length === 0 || typeof result.filePath !== "string") {
617
+ if (typeof result.filePath !== "string") {
613
618
  return undefined;
614
619
  }
615
620
  const filePath = normalizeESLintFilePath(result.filePath, options.cwd);
616
621
  if (!rulesByFilePath.has(filePath)) {
617
- rulesByFilePath.set(filePath, customRuleMapForConfig(customRuleConfig, new Map(), filePath, options.cwd));
622
+ rulesByFilePath.set(filePath, jsPluginRuleMapForFile(cachedOptions, filePath));
618
623
  }
619
624
  return rulesByFilePath.get(filePath).get(ruleId)?.meta;
620
625
  });
@@ -665,14 +670,14 @@ export class Linter {
665
670
  const verifyOptions = typeof options === "string" ? { filename: options } : { ...options };
666
671
  const filePath = verifyOptions.filename ?? verifyOptions.filePath ?? "input.js";
667
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);
668
676
  const sourceCode = createLinterSourceCode(
669
677
  code,
670
678
  parserForConfig(config, this.parsers, normalizedFilePath, verifyOptions.cwd),
671
- parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd)
679
+ { ...parserOptionsForConfig(config, normalizedFilePath, verifyOptions.cwd), requireParser: customRules.length > 0 }
672
680
  );
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);
676
681
  const nativeConfig = configWithoutCustomRules(config, customRuleFilterMap);
677
682
  const lintOptions = {
678
683
  cwd: verifyOptions.cwd,
@@ -702,10 +707,10 @@ export class Linter {
702
707
  ];
703
708
  this.times = { passes: [] };
704
709
  this.fixPassCount = 0;
705
- return [
710
+ return sortLintMessages([
706
711
  ...nativeRuleFilter.messages,
707
712
  ...customRuleFilter.messages
708
- ];
713
+ ]);
709
714
  }
710
715
 
711
716
  verifyAndFix(code, config = {}, options = {}) {
@@ -714,7 +719,7 @@ export class Linter {
714
719
  let fixPassCount = 0;
715
720
  for (let pass = 0; pass < MAX_AUTOFIX_PASSES; pass += 1) {
716
721
  const messages = this.verify(output, config, options);
717
- const fixedOutput = applyRuleFixes(output, messages.flatMap((message) => ruleFixItems(message.fix)));
722
+ const fixedOutput = applyMessageFixes(output, messages);
718
723
  if (fixedOutput === output) {
719
724
  this.fixPassCount = fixPassCount;
720
725
  return {
@@ -797,9 +802,17 @@ function parserForConfig(config, parsers, filePath, cwd) {
797
802
 
798
803
  function parserOptionsForConfig(config, filePath, cwd) {
799
804
  const configData = configDataFromConfig(config, filePath, cwd);
800
- return {
805
+ const languageOptions = configData.languageOptions ?? {};
806
+ const parserOptions = {
801
807
  ...(configData.parserOptions ?? {}),
802
- ...(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 ?? {}) },
803
816
  filename: filePath,
804
817
  filePath,
805
818
  cwd
@@ -845,13 +858,176 @@ function pluginRulesForConfig(config, filePath, cwd) {
845
858
  continue;
846
859
  }
847
860
  for (const [ruleName, rule] of Object.entries(plugin.rules)) {
848
- 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;
849
936
  }
850
937
  }
851
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
+ }
852
998
  return rules;
853
999
  }
854
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
+
855
1031
  function matchingConfigEntries(config, filePath = undefined, cwd = undefined) {
856
1032
  if (!config) {
857
1033
  return [];
@@ -903,7 +1079,7 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
903
1079
  }
904
1080
  let result = results.find((item) => item.filePath === filePath);
905
1081
  const effectiveCode = typeof result?.output === "string" ? result.output : code;
906
- const sourceCode = createLinterSourceCode(effectiveCode);
1082
+ const sourceCode = createLinterSourceCode(effectiveCode, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
907
1083
  const messages = runCustomLinterRules(customRules, sourceCode, {
908
1084
  cwd: options.cwd,
909
1085
  filename: filePath,
@@ -918,13 +1094,74 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
918
1094
  result = emptyESLintResult(filePath, effectiveCode);
919
1095
  results.push(result);
920
1096
  }
921
- 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]);
922
1101
  result.suppressedMessages.push(...filtered.suppressedMessages);
923
1102
  applyResultFixes(result, effectiveCode, options);
924
1103
  finalizeESLintResult(result);
925
1104
  return results;
926
1105
  }
927
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
+
928
1165
  function appendCustomLintFileMessages(results, config, options) {
929
1166
  for (const result of results) {
930
1167
  const filePath = result.filePath;
@@ -941,7 +1178,7 @@ function appendCustomLintFileMessages(results, config, options) {
941
1178
  continue;
942
1179
  }
943
1180
  }
944
- const sourceCode = createLinterSourceCode(code);
1181
+ const sourceCode = createLinterSourceCode(code, null, { ...parserOptionsForConfig(config, filePath, options.cwd), requireParser: true });
945
1182
  const messages = runCustomLinterRules(customRules, sourceCode, {
946
1183
  cwd: options.cwd,
947
1184
  filename: filePath,
@@ -951,7 +1188,10 @@ function appendCustomLintFileMessages(results, config, options) {
951
1188
  if (filtered.messages.length === 0 && filtered.suppressedMessages.length === 0) {
952
1189
  continue;
953
1190
  }
954
- 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]);
955
1195
  result.suppressedMessages.push(...filtered.suppressedMessages);
956
1196
  applyResultFixes(result, code, options);
957
1197
  finalizeESLintResult(result);
@@ -959,11 +1199,22 @@ function appendCustomLintFileMessages(results, config, options) {
959
1199
  return results;
960
1200
  }
961
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
+
962
1213
  function applyResultFixes(result, code, options) {
963
1214
  if (!options.fix) {
964
1215
  return;
965
1216
  }
966
- const output = applyRuleFixes(code, result.messages.flatMap((message) => ruleFixItems(message.fix)));
1217
+ const output = applyMessageFixes(code, result.messages);
967
1218
  if (output !== code) {
968
1219
  result.output = output;
969
1220
  }
@@ -976,15 +1227,40 @@ function runCustomLinterRules(ruleEntries, sourceCode, options) {
976
1227
 
977
1228
  const messages = [];
978
1229
  const program = sourceCode.ast ?? { type: "Program", range: [0, sourceCode.text.length] };
1230
+ const hasAst = Array.isArray(program.body);
1231
+ const listenersBySelector = new Map();
979
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
+ }
980
1239
  const context = createCustomRuleContext(ruleEntry, sourceCode, options, messages);
981
1240
  const listeners = customRuleListeners(ruleEntry.rule, context);
982
- if (customRuleChildNodes(program, sourceCode.visitorKeys).length > 0) {
983
- traverseCustomRuleAst(program, listeners, context, sourceCode.visitorKeys);
984
- } else {
1241
+ if (!hasAst) {
985
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
+ });
986
1259
  }
987
1260
  }
1261
+ if (hasAst && listenersBySelector.size > 0) {
1262
+ pluginRunner.runRuleListeners(program, listenersBySelector, { visitorKeys: sourceCode.visitorKeys });
1263
+ }
988
1264
  return messages;
989
1265
  }
990
1266
 
@@ -1140,231 +1416,6 @@ function disableDirectiveMatchesRule(directive, ruleId) {
1140
1416
  return directive.ruleId == null || directive.ruleId === ruleId;
1141
1417
  }
1142
1418
 
1143
- function traverseCustomRuleAst(node, listeners, context, visitorKeys, seen = new Set(), ancestors = [], siblings = {}) {
1144
- if (!node || typeof node !== "object" || typeof node.type !== "string" || seen.has(node)) {
1145
- return;
1146
- }
1147
- seen.add(node);
1148
- for (const listener of customRuleMatchingListeners(listeners, node, false, ancestors, siblings, visitorKeys)) {
1149
- context.setCurrentNode(node);
1150
- listener(node);
1151
- }
1152
- const children = customRuleChildNodes(node, visitorKeys);
1153
- for (const [index, child] of children.entries()) {
1154
- traverseCustomRuleAst(child, listeners, context, visitorKeys, seen, [...ancestors, node], {
1155
- previous: children[index - 1] ?? null,
1156
- previousAll: children.slice(0, index)
1157
- });
1158
- }
1159
- for (const listener of customRuleMatchingListeners(listeners, node, true, ancestors, siblings, visitorKeys)) {
1160
- context.setCurrentNode(node);
1161
- listener(node);
1162
- }
1163
- }
1164
-
1165
- function customRuleMatchingListeners(listeners, node, exit, ancestors = [], siblings = {}, visitorKeys = null) {
1166
- const matches = [];
1167
- for (const [selector, listener] of Object.entries(listeners)) {
1168
- if (typeof listener !== "function" || !customRuleSelectorMatches(selector, node, exit, ancestors, siblings, visitorKeys)) {
1169
- continue;
1170
- }
1171
- matches.push(listener);
1172
- }
1173
- return matches;
1174
- }
1175
-
1176
- function customRuleSelectorMatches(selector, node, exit, ancestors = [], siblings = {}, visitorKeys = null) {
1177
- const suffix = ":exit";
1178
- const isExit = selector.endsWith(suffix);
1179
- if (isExit !== exit) {
1180
- return false;
1181
- }
1182
- const expression = isExit ? selector.slice(0, -suffix.length) : selector;
1183
- const childSelector = customRuleSplitTopLevelSelector(expression, ">");
1184
- if (childSelector) {
1185
- const parent = ancestors.at(-1);
1186
- return Boolean(parent)
1187
- && customRuleSimpleSelectorMatches(childSelector[0].trim(), parent, visitorKeys)
1188
- && customRuleSimpleSelectorMatches(childSelector[1].trim(), node, visitorKeys);
1189
- }
1190
- const adjacentSelector = customRuleSplitTopLevelSelector(expression, "+");
1191
- if (adjacentSelector) {
1192
- return Boolean(siblings.previous)
1193
- && customRuleSimpleSelectorMatches(adjacentSelector[0].trim(), siblings.previous, visitorKeys)
1194
- && customRuleSimpleSelectorMatches(adjacentSelector[1].trim(), node, visitorKeys);
1195
- }
1196
- const siblingSelector = customRuleSplitTopLevelSelector(expression, "~");
1197
- if (siblingSelector) {
1198
- return (siblings.previousAll ?? []).some((sibling) => customRuleSimpleSelectorMatches(siblingSelector[0].trim(), sibling, visitorKeys))
1199
- && customRuleSimpleSelectorMatches(siblingSelector[1].trim(), node, visitorKeys);
1200
- }
1201
- const descendantSelector = customRuleSplitTopLevelDescendantSelector(expression);
1202
- if (descendantSelector) {
1203
- return ancestors.some((ancestor) => customRuleSimpleSelectorMatches(descendantSelector[0].trim(), ancestor, visitorKeys))
1204
- && customRuleSimpleSelectorMatches(descendantSelector[1].trim(), node, visitorKeys);
1205
- }
1206
- return customRuleSimpleSelectorMatches(expression, node, visitorKeys);
1207
- }
1208
-
1209
- function customRuleSimpleSelectorMatches(expression, node, visitorKeys = null) {
1210
- const hasSelector = expression.match(/^(.+?):has\((.+)\)$/u);
1211
- if (hasSelector) {
1212
- return customRuleSimpleSelectorMatches(hasSelector[1].trim(), node, visitorKeys)
1213
- && customRuleDescendants(node, visitorKeys).some((descendant) => (
1214
- customRuleSplitSelectorList(hasSelector[2]).some((selector) => customRuleSimpleSelectorMatches(selector.trim(), descendant, visitorKeys))
1215
- ));
1216
- }
1217
- const notSelector = expression.match(/^(.+?):not\((.+)\)$/u);
1218
- if (notSelector) {
1219
- return customRuleSimpleSelectorMatches(notSelector[1].trim(), node, visitorKeys)
1220
- && !customRuleSimpleSelectorMatches(notSelector[2].trim(), node, visitorKeys);
1221
- }
1222
- const matchesSelector = expression.match(/^(.*?):matches\((.+)\)$/u);
1223
- if (matchesSelector) {
1224
- const prefix = matchesSelector[1].trim();
1225
- if (prefix && !customRuleSimpleSelectorMatches(prefix, node, visitorKeys)) {
1226
- return false;
1227
- }
1228
- return customRuleSplitSelectorList(matchesSelector[2]).some((selector) => (
1229
- customRuleSimpleSelectorMatches(selector.trim(), node, visitorKeys)
1230
- ));
1231
- }
1232
- if (expression === node.type) {
1233
- return true;
1234
- }
1235
- const match = expression.match(/^([A-Za-z_$][\w$-]*|\*)?(?:\[([^\]]+)\])?$/u);
1236
- if (!match) {
1237
- return false;
1238
- }
1239
- const [, type = "*", attribute] = match;
1240
- if (type !== "*" && type !== node.type) {
1241
- return false;
1242
- }
1243
- return attribute ? customRuleAttributeSelectorMatches(node, attribute.trim()) : type === "*" || type === node.type;
1244
- }
1245
-
1246
- function customRuleDescendants(node, visitorKeys, seen = new Set()) {
1247
- const descendants = [];
1248
- for (const child of customRuleChildNodes(node, visitorKeys)) {
1249
- if (seen.has(child)) {
1250
- continue;
1251
- }
1252
- seen.add(child);
1253
- descendants.push(child, ...customRuleDescendants(child, visitorKeys, seen));
1254
- }
1255
- return descendants;
1256
- }
1257
-
1258
- function customRuleSplitTopLevelSelector(expression, separator) {
1259
- let state = customRuleSelectorScanState();
1260
- for (let index = 0; index < expression.length; index += 1) {
1261
- state = customRuleUpdateSelectorScanState(state, expression[index]);
1262
- if (state.depth === 0 && !state.quote && expression[index] === separator) {
1263
- return [expression.slice(0, index), expression.slice(index + 1)];
1264
- }
1265
- }
1266
- return null;
1267
- }
1268
-
1269
- function customRuleSplitTopLevelDescendantSelector(expression) {
1270
- let state = customRuleSelectorScanState();
1271
- for (let index = 0; index < expression.length; index += 1) {
1272
- state = customRuleUpdateSelectorScanState(state, expression[index]);
1273
- if (state.depth === 0 && !state.quote && /\s/u.test(expression[index])) {
1274
- const left = expression.slice(0, index).trim();
1275
- const right = expression.slice(index).trim();
1276
- return left && right ? [left, right] : null;
1277
- }
1278
- }
1279
- return null;
1280
- }
1281
-
1282
- function customRuleSplitSelectorList(value) {
1283
- const selectors = [];
1284
- let state = customRuleSelectorScanState();
1285
- let start = 0;
1286
- for (let index = 0; index < value.length; index += 1) {
1287
- state = customRuleUpdateSelectorScanState(state, value[index]);
1288
- if (state.depth === 0 && !state.quote && value[index] === ",") {
1289
- selectors.push(value.slice(start, index));
1290
- start = index + 1;
1291
- }
1292
- }
1293
- selectors.push(value.slice(start));
1294
- return selectors.filter((selector) => selector.trim());
1295
- }
1296
-
1297
- function customRuleSelectorScanState() {
1298
- return { depth: 0, quote: null, escaped: false };
1299
- }
1300
-
1301
- function customRuleUpdateSelectorScanState(state, char) {
1302
- if (state.escaped) {
1303
- return { ...state, escaped: false };
1304
- }
1305
- if (char === "\\") {
1306
- return { ...state, escaped: true };
1307
- }
1308
- if (state.quote) {
1309
- return char === state.quote ? { ...state, quote: null } : state;
1310
- }
1311
- if (char === "\"" || char === "'") {
1312
- return { ...state, quote: char };
1313
- }
1314
- if (char === "[" || char === "(") {
1315
- return { ...state, depth: state.depth + 1 };
1316
- }
1317
- if ((char === "]" || char === ")") && state.depth > 0) {
1318
- return { ...state, depth: state.depth - 1 };
1319
- }
1320
- return state;
1321
- }
1322
-
1323
- function customRuleAttributeSelectorMatches(node, attribute) {
1324
- const match = attribute.match(/^([\w$.-]+)\s*(!=|=)\s*(.+)$/u);
1325
- if (!match) {
1326
- return customRuleValueByPath(node, attribute) != null;
1327
- }
1328
- const [, path, operator, rawExpected] = match;
1329
- const actual = customRuleValueByPath(node, path);
1330
- const expected = customRuleSelectorValue(rawExpected);
1331
- return operator === "="
1332
- ? String(actual) === expected
1333
- : String(actual) !== expected;
1334
- }
1335
-
1336
- function customRuleSelectorValue(raw) {
1337
- const value = raw.trim();
1338
- const quote = value[0];
1339
- if ((quote === "\"" || quote === "'") && value.at(-1) === quote) {
1340
- return value.slice(1, -1);
1341
- }
1342
- return value;
1343
- }
1344
-
1345
- function customRuleValueByPath(node, path) {
1346
- return path.split(".").reduce((value, key) => (
1347
- value && typeof value === "object" ? value[key] : undefined
1348
- ), node);
1349
- }
1350
-
1351
- function customRuleChildNodes(node, visitorKeys) {
1352
- const keys = Array.isArray(visitorKeys?.[node.type])
1353
- ? visitorKeys[node.type]
1354
- : Object.keys(node).filter((key) => !AST_TRAVERSAL_SKIP_KEYS.has(key));
1355
- const children = [];
1356
- for (const key of keys) {
1357
- const value = node[key];
1358
- const values = Array.isArray(value) ? value : [value];
1359
- for (const child of values) {
1360
- if (child && typeof child === "object" && typeof child.type === "string") {
1361
- children.push(child);
1362
- }
1363
- }
1364
- }
1365
- return children;
1366
- }
1367
-
1368
1419
  function runCustomRuleTokenListeners(program, tokens, listeners, context) {
1369
1420
  if (typeof listeners.Program === "function") {
1370
1421
  context.setCurrentNode(program);
@@ -1399,7 +1450,7 @@ function createCustomRuleContext(ruleEntry, sourceCode, options, messages) {
1399
1450
  let currentNode = sourceCode.ast ?? null;
1400
1451
  const context = {
1401
1452
  id: ruleEntry.ruleId,
1402
- options: ruleEntry.options,
1453
+ options: pluginRunner.applyRuleOptionDefaults(ruleEntry.rule, ruleEntry.options),
1403
1454
  filename,
1404
1455
  physicalFilename: filename,
1405
1456
  cwd,
@@ -1617,7 +1668,7 @@ export class SourceCode {
1617
1668
  this.text = textOrConfig.text;
1618
1669
  this.ast = textOrConfig.ast ?? null;
1619
1670
  this.parserServices = textOrConfig.parserServices ?? {};
1620
- this.scopeManager = textOrConfig.scopeManager ?? null;
1671
+ defineScopeManager(this, textOrConfig.scopeManager ?? null);
1621
1672
  this.visitorKeys = textOrConfig.visitorKeys ?? null;
1622
1673
  this.hasBOM = Boolean(textOrConfig.hasBOM);
1623
1674
  } else {
@@ -1809,7 +1860,17 @@ export class SourceCode {
1809
1860
  }
1810
1861
 
1811
1862
  getAncestors(node) {
1812
- 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) ?? [];
1813
1874
  }
1814
1875
 
1815
1876
  getDeclaredVariables(node) {
@@ -1817,10 +1878,20 @@ export class SourceCode {
1817
1878
  }
1818
1879
 
1819
1880
  getScope(node) {
1820
- if (node && typeof this.scopeManager?.acquire === "function") {
1821
- 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
+ }
1822
1893
  }
1823
- return this.scopeManager?.globalScope ?? null;
1894
+ return scopeManager.scopes?.[0] ?? scopeManager.globalScope ?? null;
1824
1895
  }
1825
1896
 
1826
1897
  markVariableAsUsed(name, node) {
@@ -1856,6 +1927,31 @@ export class SourceCode {
1856
1927
  }
1857
1928
  }
1858
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
+
1859
1955
  function sourceLineStartIndices(text) {
1860
1956
  const indices = [0];
1861
1957
  for (let index = 0; index < text.length; index += 1) {
@@ -2145,12 +2241,10 @@ export class RuleTester {
2145
2241
  RuleTester[testCase.only ? "itOnly" : "it"](testCase.name ?? testCase.code, () => {
2146
2242
  const config = ruleTesterConfig(ruleName, this.config, testCase, "error");
2147
2243
  const options = ruleTesterOptions(testCase);
2148
- const result = Object.hasOwn(testCase, "output")
2149
- ? linter.verifyAndFix(testCase.code, config, options)
2150
- : { messages: linter.verify(testCase.code, config, options), output: testCase.code };
2151
- const messages = result.messages;
2244
+ const messages = linter.verify(testCase.code, config, options);
2152
2245
  assertRuleTesterErrors(testCase, messages, rule);
2153
- 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);
2154
2248
  });
2155
2249
  }
2156
2250
  });
@@ -2297,6 +2391,47 @@ function applyRuleFixes(code, fix) {
2297
2391
  .reduce((output, item) => output.slice(0, item.range[0]) + item.text + output.slice(item.range[1]), code);
2298
2392
  }
2299
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
+
2300
2435
  function ruleFixItems(fix) {
2301
2436
  if (!fix) {
2302
2437
  return [];
@@ -2349,10 +2484,29 @@ function createLinterSourceCode(text, parser = null, parserOptions = {}) {
2349
2484
  });
2350
2485
  }
2351
2486
 
2352
- function parseSourceCode(text, parser, parserOptions) {
2353
- 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
+ }
2354
2495
  return null;
2355
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
+ }
2356
2510
  if (typeof parser.parseForESLint === "function") {
2357
2511
  const result = parser.parseForESLint(text, parserOptions) ?? {};
2358
2512
  return new SourceCode({
@@ -3124,21 +3278,210 @@ export function lintFiles(paths, options = {}) {
3124
3278
  }
3125
3279
 
3126
3280
  const nativeFlatOptions = nativeFlatConfigOptions(options);
3127
- if (nativeFlatOptions) {
3128
- const report = runNativeLintReport(lintPaths, nativeFlatOptions);
3129
- return finalizeLintReport(report, ignoredDiagnostics, options);
3130
- }
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
+ }
3131
3289
 
3290
+ function runNativeConfigRunReports(lintPaths, options) {
3132
3291
  const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
3133
3292
  if (configRuns) {
3134
- const report = mergeNativeReports(
3293
+ return mergeNativeReports(
3135
3294
  configRuns.map(({ paths: runPaths, options: runOptions }) => runNativeLintReport(runPaths, runOptions))
3136
3295
  );
3137
- return finalizeLintReport(report, ignoredDiagnostics, options);
3138
3296
  }
3139
3297
 
3140
- const report = runNativeLintReport(lintPaths, options);
3141
- 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;
3142
3485
  }
3143
3486
 
3144
3487
  function nativeFlatConfigOptions(options) {
@@ -3168,6 +3511,10 @@ function nativeFlatConfigOptions(options) {
3168
3511
  return undefined;
3169
3512
  }
3170
3513
 
3514
+ const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(config);
3515
+ if (jsPluginRuleIds.size > 0) {
3516
+ config = configWithoutCustomRules(config, jsPluginRuleIds);
3517
+ }
3171
3518
  if (configCanUseNativeRulesFastPath(config)) {
3172
3519
  return undefined;
3173
3520
  }
@@ -3294,6 +3641,9 @@ function nativeConfigRunsForFiles(paths, options) {
3294
3641
  if (configured && options.rules) {
3295
3642
  rules = selectedRulesWithConfigOptions(rules, options.rules);
3296
3643
  }
3644
+ if (configured) {
3645
+ rules = withoutJsPluginRules(rules, jsPluginRuleIdsForOptions(options, matchPath));
3646
+ }
3297
3647
  const signature = configured ? stableConfigSignature({ rules, settings }) : "<native-defaults>";
3298
3648
  if (!groups.has(signature)) {
3299
3649
  groups.set(signature, { paths: [], rules, settings, configured });
@@ -3466,7 +3816,8 @@ export function lintText(code, options = {}) {
3466
3816
  cwd: options.cwd,
3467
3817
  config: options.config ?? discoveredConfig,
3468
3818
  noConfig: options.noConfig,
3469
- deferDiagnosticConfigFiltering: true
3819
+ deferDiagnosticConfigFiltering: true,
3820
+ pluginTextFilePath: requestedPath
3470
3821
  });
3471
3822
  report.filePaths = (report.filePaths ?? []).map((filePath) => (filePath === tempFile ? requestedPath : filePath));
3472
3823
  report.outputs = (report.outputs ?? []).map((fixed) => ({
@@ -3508,11 +3859,12 @@ function buildNativeLintArgs(paths, options) {
3508
3859
  const configForRuleSelection = options.noConfig
3509
3860
  ? undefined
3510
3861
  : options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
3862
+ const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(configForRuleSelection, options.baseConfig, options.overrideConfig);
3511
3863
  const enabledRules = enabledRuleNamesFromConfigs(
3512
3864
  configForRuleSelection,
3513
3865
  options.baseConfig,
3514
3866
  options.overrideConfig
3515
- );
3867
+ ).filter((rule) => !jsPluginRuleIds.has(rule));
3516
3868
  if (enabledRules.length > 0) {
3517
3869
  cliArgs.push(`--rules=${enabledRules.join(",")}`);
3518
3870
  }
@@ -3990,9 +4342,11 @@ function withTemporaryConfig(options, callback) {
3990
4342
  ? readConfig(fileConfigPath, options.cwd, options.configCache)
3991
4343
  : undefined;
3992
4344
  const configs = [options.baseConfig, fileConfig, options.overrideConfig];
3993
- const rules = shouldMaterializeFileConfig
3994
- ? materializedRulesFromConfigs(...configs)
3995
- : runtimeRulesFromConfigs(...configs);
4345
+ const jsPluginRuleIds = jsPluginRuleIdsFromConfigs(...configs);
4346
+ const rules = withoutJsPluginRules(
4347
+ shouldMaterializeFileConfig ? materializedRulesFromConfigs(...configs) : runtimeRulesFromConfigs(...configs),
4348
+ jsPluginRuleIds
4349
+ );
3996
4350
  const settings = configs.reduce(
3997
4351
  (result, config) => ({
3998
4352
  ...result,
@@ -4011,7 +4365,7 @@ function withTemporaryConfig(options, callback) {
4011
4365
  }
4012
4366
  return callback(options);
4013
4367
  }
4014
- const enabledRules = enabledRuleNamesFromConfigs(...configs);
4368
+ const enabledRules = enabledRuleNamesFromConfigs(...configs).filter((rule) => !jsPluginRuleIds.has(rule));
4015
4369
  const hasExplicitOffRules = Object.values(rules).some((value) => ruleConfigSeverity(value) === 0);
4016
4370
  if (!shouldMaterializeFileConfig && !hasSettings && !hasRuleOptions(rules) && !hasExplicitOffRules && !options.forceMaterializedConfig) {
4017
4371
  return callback({
@@ -4197,7 +4551,7 @@ function applyCompatibilityDisableDirectives(result, textOptions) {
4197
4551
  return;
4198
4552
  }
4199
4553
 
4200
- const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source));
4554
+ const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source, null, { tokensOnly: true }));
4201
4555
  result.messages = filtered.messages;
4202
4556
  result.suppressedMessages.push(...filtered.suppressedMessages);
4203
4557
  }
@@ -4931,6 +5285,12 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4931
5285
  column: diagnostic.column,
4932
5286
  nodeType: null
4933
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
+ }
4934
5294
  if (diagnostic.endLine != null) {
4935
5295
  message.endLine = diagnostic.endLine;
4936
5296
  }