@utoo/lint 0.2.1 → 0.2.2

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/bin/fishlint.js CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdtempSync, realpathSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
- import { extname, join } from "node:path";
5
+ import { basename, dirname, extname, join, relative, resolve as resolvePath } from "node:path";
6
6
 
7
7
  import { resolveBinary } from "../lib/binary.js";
8
- import { translateFishlintArgs, version } from "../index.js";
8
+ import {
9
+ findConfigPath as findConfigPathFromDirectory,
10
+ isExecutableConfigPath,
11
+ readConfig as readSharedConfig
12
+ } from "../lib/config-loader.js";
13
+ import { Linter, translateFishlintArgs, version } from "../index.js";
9
14
 
10
15
  const values = process.argv.slice(2);
11
16
  const command = values[0];
@@ -44,20 +49,7 @@ const FISHLINT_PASSTHROUGH_DROP_FLAGS = new Set([
44
49
  "-v"
45
50
  ]);
46
51
  const FORMAT_VALUE_FLAGS = new Set(["--config", "--ignore-path", "--parser", "--plugin"]);
47
- const CONFIG_FILENAMES = ["utoo.json", "utoo-lint.json", "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
48
- const JAVASCRIPT_CONFIG_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
49
- const JAVASCRIPT_CONFIG_LOADER_SCRIPT = `
50
- import { pathToFileURL } from "node:url";
51
-
52
- const configPath = process.argv[1];
53
- const loaded = await import(pathToFileURL(configPath).href);
54
- const value = loaded.default ?? loaded;
55
- const json = JSON.stringify(value);
56
- if (json === undefined) {
57
- throw new TypeError("config did not export a JSON-serializable value");
58
- }
59
- process.stdout.write(json);
60
- `;
52
+ const ESLINT_CONFIG_FILENAMES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
61
53
  const STYLELINT_VALUE_FLAGS = new Set([
62
54
  "--cache-location",
63
55
  "--config",
@@ -80,7 +72,7 @@ if (isHelpRequest(values)) {
80
72
  }
81
73
 
82
74
  if (command === "setup" || command === "setuplint") {
83
- console.warn(`utoo-lint: fishlint ${command} is treated as a no-op; configure utoo-lint with utoo.json.`);
75
+ console.warn(`utoo-lint: fishlint ${command} is treated as a no-op; configure utoo-lint with utlint.config.json.`);
84
76
  process.exit(0);
85
77
  }
86
78
 
@@ -143,14 +135,53 @@ try {
143
135
  process.exit(1);
144
136
  }
145
137
 
146
- const printableConfig = loadPrintableConfig(nativeValues, ruleOverrides.rules);
147
- const ruleSeverities = ruleSeverityMap(printableConfig.rules);
148
- const ruleConfig = materializeRuntimeConfig(nativeValues, args, ruleOverrides.rules);
138
+ const selectedConfig = loadSelectedConfig(nativeValues);
139
+ const ruleResolution = createRuleResolution(selectedConfig, ruleOverrides.rules, selectedRulesFromArgs(args));
140
+ if (Array.isArray(selectedConfig.config)) {
141
+ const grouped = runFlatConfigGroups(
142
+ binary,
143
+ args,
144
+ selectedConfig,
145
+ ruleOverrides.rules,
146
+ ruleResolution.selectedRules,
147
+ input
148
+ );
149
+ if (grouped.error) {
150
+ console.error(`utoo-lint: failed to run native binary: ${grouped.error.message}`);
151
+ cleanupStdinInput(input);
152
+ process.exit(1);
153
+ }
154
+ if (!grouped.report) {
155
+ if (grouped.stderr) {
156
+ process.stderr.write(rewriteStdinPath(grouped.stderr, input));
157
+ }
158
+ if (grouped.stdout) {
159
+ writeOutput(rewriteStdinPath(grouped.stdout, input), output.file);
160
+ }
161
+ cleanupStdinInput(input);
162
+ process.exit(grouped.status ?? 1);
163
+ }
164
+
165
+ const reportWithSeverity = normalizeReportSeverities(grouped.report, ruleResolution, input);
166
+ const reportWithIgnored = appendDiagnostics(reportWithSeverity, ignoredFiltered.diagnostics);
167
+ const diagnosticCounts = diagnosticCountsFromReport(reportWithIgnored);
168
+ const filteredReport = quiet.enabled ? filterQuietReport(reportWithIgnored) : reportWithIgnored;
169
+ const rewrittenReport = rewriteReportStdinPaths(filteredReport, input);
170
+ if (grouped.stderr) {
171
+ process.stderr.write(rewriteStdinPath(grouped.stderr, input));
172
+ }
173
+ writeOutput(formatReportOutput(rewrittenReport, nativeValues), output.file);
174
+ cleanupStdinInput(input);
175
+ process.exit(eslintExitStatusFromCounts(diagnosticCounts, maxWarnings.value));
176
+ }
177
+
178
+ const ruleConfig = materializeRuntimeConfig(nativeValues, args, ruleOverrides.rules, selectedConfig);
149
179
  const needsReportOutput =
150
180
  quiet.enabled ||
181
+ maxWarnings.value != null ||
151
182
  ignoredFiltered.diagnostics.length > 0 ||
152
183
  usesJsonWithMetadataFormat(nativeValues) ||
153
- ruleSeverities.size > 0;
184
+ ruleResolution.configuredRules.size > 0;
154
185
  const result = spawnSync(binary, needsReportOutput ? withJsonFormat(ruleConfig.args) : ruleConfig.args, { encoding: "utf8" });
155
186
 
156
187
  if (result.error) {
@@ -172,12 +203,13 @@ if (needsReportOutput) {
172
203
  writeOutput(rewriteStdinPath(rawStdout, input), output.file);
173
204
  exitStatus = result.status ?? 1;
174
205
  } else {
175
- const reportWithSeverity = normalizeReportSeverities(report, ruleSeverities);
206
+ const reportWithSeverity = normalizeReportSeverities(report, ruleResolution, input);
176
207
  const reportWithIgnored = appendDiagnostics(reportWithSeverity, ignoredFiltered.diagnostics);
208
+ const diagnosticCounts = diagnosticCountsFromReport(reportWithIgnored);
177
209
  const filteredReport = quiet.enabled ? filterQuietReport(reportWithIgnored) : reportWithIgnored;
178
210
  const rewrittenReport = rewriteReportStdinPaths(filteredReport, input);
179
211
  writeOutput(formatReportOutput(rewrittenReport, nativeValues), output.file);
180
- exitStatus = eslintExitStatusFromCounts(diagnosticCountsFromReport(rewrittenReport), maxWarnings.value);
212
+ exitStatus = eslintExitStatusFromCounts(diagnosticCounts, maxWarnings.value);
181
213
  }
182
214
  } else {
183
215
  writeOutput(rewriteStdinPath(rawStdout, input), output.file);
@@ -951,16 +983,29 @@ function loadPrintableConfig(args, ruleOverrides = {}) {
951
983
  };
952
984
  }
953
985
 
954
- function materializeRuntimeConfig(args, translatedArgs, ruleOverrides) {
955
- const configPath = findConfigPath(args);
956
- const shouldMaterialize = isJavaScriptConfigPath(configPath) || Object.keys(ruleOverrides).length > 0;
986
+ function materializeRuntimeConfig(args, translatedArgs, ruleOverrides, selectedConfig = loadSelectedConfig(args)) {
987
+ const { configPath, config } = selectedConfig;
988
+ const shouldMaterialize =
989
+ (config && typeof config === "object" && Object.keys(config.rules ?? {}).length > 0) ||
990
+ (typeof configPath === "string" && isExecutableConfigPath(configPath)) ||
991
+ Array.isArray(config) ||
992
+ Object.keys(ruleOverrides).length > 0;
957
993
  if (!shouldMaterialize) {
958
- return { args: translatedArgs };
994
+ return {
995
+ args: configPath ? withConfigArg(translatedArgs, resolvePath(configPath)) : translatedArgs
996
+ };
959
997
  }
960
998
 
961
999
  const directory = mkdtempSync(join(tmpdir(), "utoo-fishlint-rule-"));
962
- const file = join(directory, "utoo.json");
963
- writeFileSync(file, JSON.stringify(loadRuntimeConfig(configPath, ruleOverrides)));
1000
+ const file = join(directory, "utlint.config.json");
1001
+ const runtimeConfig = loadRuntimeConfig(config, ruleOverrides);
1002
+ writeFileSync(file, JSON.stringify({
1003
+ ...runtimeConfig,
1004
+ rules: {
1005
+ ...disabledNativeRules(),
1006
+ ...(runtimeConfig.rules ?? {})
1007
+ }
1008
+ }));
964
1009
 
965
1010
  return {
966
1011
  args: withConfigArg(translatedArgs, file),
@@ -968,12 +1013,11 @@ function materializeRuntimeConfig(args, translatedArgs, ruleOverrides) {
968
1013
  };
969
1014
  }
970
1015
 
971
- function loadRuntimeConfig(configPath, ruleOverrides = {}) {
972
- const config = configPath ? readConfig(configPath) : {};
1016
+ function loadRuntimeConfig(config, ruleOverrides = {}) {
973
1017
  if (Array.isArray(config)) {
974
1018
  return {
975
1019
  rules: {
976
- ...rulesFromConfig(config),
1020
+ ...runtimeRulesFromConfig(config),
977
1021
  ...ruleOverrides
978
1022
  }
979
1023
  };
@@ -987,43 +1031,270 @@ function loadRuntimeConfig(configPath, ruleOverrides = {}) {
987
1031
  };
988
1032
  }
989
1033
 
990
- function readConfig(path) {
991
- if (isJavaScriptConfigPath(path)) {
992
- return readJavaScriptConfig(path);
1034
+ function loadSelectedConfig(args) {
1035
+ const configPath = findConfigPath(args);
1036
+ return {
1037
+ configPath,
1038
+ config: configPath ? readConfig(configPath) : {}
1039
+ };
1040
+ }
1041
+
1042
+ function runtimeRulesFromConfig(config) {
1043
+ if (!config) {
1044
+ return {};
1045
+ }
1046
+ if (Array.isArray(config)) {
1047
+ return config.reduce((rules, entry) => ({
1048
+ ...rules,
1049
+ ...runtimeRulesFromConfig(entry)
1050
+ }), {});
1051
+ }
1052
+
1053
+ return Object.fromEntries(
1054
+ Object.entries(rulesFromConfig(config)).filter(([, value]) => ruleConfigSeverity(value) > 0)
1055
+ );
1056
+ }
1057
+
1058
+ function createRuleResolution(selectedConfig, ruleOverrides, selectedRules = new Set()) {
1059
+ const configuredRules = new Set(Object.keys(rulesFromConfig(selectedConfig.config)));
1060
+ for (const rule of selectedRules) {
1061
+ configuredRules.add(rule);
1062
+ }
1063
+ for (const rule of Object.keys(ruleOverrides)) {
1064
+ configuredRules.add(rule);
993
1065
  }
1066
+ return {
1067
+ config: selectedConfig.config,
1068
+ configDirectory: selectedConfig.configPath
1069
+ ? dirname(canonicalFilePath(selectedConfig.configPath))
1070
+ : process.cwd(),
1071
+ configuredRules,
1072
+ ruleOverrides,
1073
+ selectedRules
1074
+ };
1075
+ }
1076
+
1077
+ function selectedRulesFromArgs(args) {
1078
+ const rules = new Set();
1079
+ for (let index = 0; index < args.length; index += 1) {
1080
+ const arg = args[index];
1081
+ const value = arg === "--rules" ? args[++index] : arg.startsWith("--rules=") ? arg.slice("--rules=".length) : undefined;
1082
+ if (!value) {
1083
+ continue;
1084
+ }
1085
+ for (const rule of value.split(",")) {
1086
+ if (rule.trim()) {
1087
+ rules.add(rule.trim());
1088
+ }
1089
+ }
1090
+ }
1091
+ return rules;
1092
+ }
994
1093
 
1094
+ function runFlatConfigGroups(binary, translatedArgs, selectedConfig, ruleOverrides, selectedRules, input) {
1095
+ const plan = flatConfigExecutionPlan(translatedArgs, selectedConfig, ruleOverrides, selectedRules, input);
1096
+ const report = { files: 0, filePaths: [], diagnostics: [], outputs: [] };
1097
+ const stderr = [];
1098
+
1099
+ for (const group of plan.groups) {
1100
+ const runtimeConfig = createRuntimeConfig(group.rules);
1101
+ const result = spawnSync(binary, withJsonFormat([
1102
+ ...withoutConfigAndTargets(translatedArgs),
1103
+ `--config=${runtimeConfig.file}`,
1104
+ ...group.files
1105
+ ]), { encoding: "utf8" });
1106
+ cleanupRuleConfig(runtimeConfig);
1107
+ if (result.error) {
1108
+ return { error: result.error };
1109
+ }
1110
+ if (result.stderr) {
1111
+ stderr.push(result.stderr);
1112
+ }
1113
+ const groupReport = parseJsonReport(result.stdout ?? "");
1114
+ if (!groupReport) {
1115
+ return {
1116
+ status: result.status,
1117
+ stdout: result.stdout ?? "",
1118
+ stderr: stderr.join("")
1119
+ };
1120
+ }
1121
+ mergeFlatConfigReport(report, groupReport);
1122
+ }
1123
+
1124
+ report.filePaths = report.filePaths.filter((file) => !plan.globallyIgnored.has(canonicalFilePath(file)));
1125
+ report.diagnostics = report.diagnostics.filter((diagnostic) =>
1126
+ !plan.globallyIgnored.has(canonicalFilePath(diagnostic.filePath))
1127
+ );
1128
+ report.outputs = report.outputs.filter((output) =>
1129
+ !plan.globallyIgnored.has(canonicalFilePath(output.filePath))
1130
+ );
1131
+ report.files = report.filePaths.length;
1132
+ return { report, stderr: stderr.join("") };
1133
+ }
1134
+
1135
+ function flatConfigExecutionPlan(translatedArgs, selectedConfig, ruleOverrides, selectedRules, input) {
1136
+ const configDirectory = dirname(canonicalFilePath(selectedConfig.configPath));
1137
+ const targetFiles = explicitTargetFiles(translatedArgs);
1138
+ const globallyIgnored = new Set();
1139
+ const groups = new Map();
1140
+
1141
+ for (const file of targetFiles) {
1142
+ const canonical = canonicalFilePath(file);
1143
+ const configFilePath = file === input.file && input.displayPath
1144
+ ? canonicalFilePath(input.displayPath)
1145
+ : canonical;
1146
+ const relativePath = normalizePath(relative(configDirectory, configFilePath));
1147
+ if (isGloballyIgnoredByFlatConfig(selectedConfig.config, relativePath)) {
1148
+ globallyIgnored.add(canonical);
1149
+ continue;
1150
+ }
1151
+ const rules = {
1152
+ ...rulesForFile(selectedConfig.config, relativePath),
1153
+ ...ruleOverrides
1154
+ };
1155
+ for (const rule of selectedRules) {
1156
+ if (!Object.hasOwn(ruleOverrides, rule) && ruleConfigSeverity(rules[rule]) === 0) {
1157
+ rules[rule] = "warn";
1158
+ }
1159
+ }
1160
+ const signature = stableStringify(rules);
1161
+ const group = groups.get(signature) ?? { rules, files: [] };
1162
+ group.files.push(file);
1163
+ groups.set(signature, group);
1164
+ }
1165
+ return { globallyIgnored, groups: [...groups.values()] };
1166
+ }
1167
+
1168
+ function explicitTargetFiles(args) {
1169
+ const files = [];
1170
+ for (let index = 0; index < args.length; index += 1) {
1171
+ const arg = args[index];
1172
+ if (arg === "--config" || arg === "-c") {
1173
+ index += 1;
1174
+ continue;
1175
+ }
1176
+ if (arg.startsWith("-")) {
1177
+ continue;
1178
+ }
1179
+ collectExecutionFiles(arg, files);
1180
+ }
1181
+ return files;
1182
+ }
1183
+
1184
+ function collectExecutionFiles(target, files) {
1185
+ let stat;
995
1186
  try {
996
- return JSON.parse(readFileSync(path, "utf8"));
997
- } catch (error) {
998
- console.error(`utoo-lint: unable to read config ${path}: ${error.message}`);
999
- process.exit(2);
1187
+ stat = statSync(target);
1188
+ } catch {
1189
+ files.push(target);
1190
+ return;
1191
+ }
1192
+ if (stat.isFile()) {
1193
+ files.push(target);
1194
+ return;
1195
+ }
1196
+ if (stat.isDirectory()) {
1197
+ collectExecutionDirectoryFiles(target, files);
1000
1198
  }
1001
1199
  }
1002
1200
 
1003
- function readJavaScriptConfig(path) {
1004
- const result = spawnSync(process.execPath, ["--input-type=module", "--eval", JAVASCRIPT_CONFIG_LOADER_SCRIPT, path], {
1005
- encoding: "utf8"
1006
- });
1007
- if (result.error) {
1008
- console.error(`utoo-lint: unable to read config ${path}: ${result.error.message}`);
1009
- process.exit(2);
1201
+ function collectExecutionDirectoryFiles(directory, files) {
1202
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
1203
+ if (entry.isDirectory() && [".git", ".zig-cache", "node_modules", "vendor", "zig-out"].includes(entry.name)) {
1204
+ continue;
1205
+ }
1206
+ const path = join(directory, entry.name);
1207
+ if (entry.isDirectory()) {
1208
+ collectExecutionDirectoryFiles(path, files);
1209
+ } else if (entry.isFile() && isExistingLintFile(path)) {
1210
+ files.push(path);
1211
+ }
1010
1212
  }
1011
- if (result.status !== 0) {
1012
- console.error(`utoo-lint: unable to read config ${path}: ${(result.stderr ?? "").trim() || "JavaScript config loader failed"}`);
1013
- process.exit(2);
1213
+ }
1214
+
1215
+ function withoutConfigAndTargets(args) {
1216
+ const values = [];
1217
+ for (let index = 0; index < args.length; index += 1) {
1218
+ const arg = args[index];
1219
+ if (arg === "--config" || arg === "-c") {
1220
+ index += 1;
1221
+ continue;
1222
+ }
1223
+ if (arg.startsWith("--config=")) {
1224
+ continue;
1225
+ }
1226
+ if (!arg.startsWith("-")) {
1227
+ continue;
1228
+ }
1229
+ values.push(arg);
1230
+ }
1231
+ return values;
1232
+ }
1233
+
1234
+ function createRuntimeConfig(rules) {
1235
+ const directory = mkdtempSync(join(tmpdir(), "utoo-fishlint-flat-"));
1236
+ const file = join(directory, "utlint.config.json");
1237
+ writeFileSync(file, JSON.stringify({ rules: { ...disabledNativeRules(), ...rules } }));
1238
+ return { directory, file };
1239
+ }
1240
+
1241
+ function disabledNativeRules() {
1242
+ return Object.fromEntries([...new Linter().getRules().keys()].map((rule) => [rule, "off"]));
1243
+ }
1244
+
1245
+ function mergeFlatConfigReport(report, groupReport) {
1246
+ for (const file of groupReport.filePaths ?? []) {
1247
+ if (!report.filePaths.includes(file)) {
1248
+ report.filePaths.push(file);
1249
+ }
1250
+ }
1251
+ report.diagnostics.push(...(groupReport.diagnostics ?? []));
1252
+ report.outputs.push(...(groupReport.outputs ?? []));
1253
+ report.files = report.filePaths.length;
1254
+ }
1255
+
1256
+ function stableStringify(value) {
1257
+ if (Array.isArray(value)) {
1258
+ return `[${value.map(stableStringify).join(",")}]`;
1259
+ }
1260
+ if (value && typeof value === "object") {
1261
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
1262
+ }
1263
+ return JSON.stringify(value);
1264
+ }
1265
+
1266
+ function isGloballyIgnoredByFlatConfig(config, filePath) {
1267
+ let ignored = false;
1268
+ for (const entry of config) {
1269
+ if (!isGlobalIgnoreEntry(entry)) {
1270
+ continue;
1271
+ }
1272
+ for (const pattern of normalizeConfigPatterns(entry.ignores)) {
1273
+ const negated = pattern.startsWith("!");
1274
+ if (matchesIgnorePattern(filePath, normalizeIgnoredPattern(pattern))) {
1275
+ ignored = !negated;
1276
+ }
1277
+ }
1278
+ }
1279
+ return ignored;
1280
+ }
1281
+
1282
+ function isGlobalIgnoreEntry(entry) {
1283
+ if (!entry || typeof entry !== "object" || !entry.ignores) {
1284
+ return false;
1014
1285
  }
1286
+ return Object.keys(entry).every((key) => key === "name" || key === "ignores");
1287
+ }
1288
+
1289
+ function readConfig(path) {
1015
1290
  try {
1016
- return JSON.parse(result.stdout);
1291
+ return readSharedConfig(resolvePath(path), process.cwd());
1017
1292
  } catch (error) {
1018
- console.error(`utoo-lint: unable to read config ${path}: ${error.message}`);
1293
+ console.error(error.message);
1019
1294
  process.exit(2);
1020
1295
  }
1021
1296
  }
1022
1297
 
1023
- function isJavaScriptConfigPath(path) {
1024
- return typeof path === "string" && JAVASCRIPT_CONFIG_EXTENSIONS.has(extname(path));
1025
- }
1026
-
1027
1298
  function rulesFromConfig(config) {
1028
1299
  if (!config) {
1029
1300
  return {};
@@ -1041,7 +1312,7 @@ function withConfigArg(args, file) {
1041
1312
  const values = [];
1042
1313
  for (let index = 0; index < args.length; index += 1) {
1043
1314
  const arg = args[index];
1044
- if (arg === "-c") {
1315
+ if (arg === "-c" || arg === "--config") {
1045
1316
  index += 1;
1046
1317
  continue;
1047
1318
  }
@@ -1071,11 +1342,13 @@ function findConfigPath(args) {
1071
1342
  console.error(`utoo-lint: fishlint ${arg} requires a path`);
1072
1343
  process.exit(2);
1073
1344
  }
1345
+ configEnabled = true;
1074
1346
  index += 1;
1075
1347
  continue;
1076
1348
  }
1077
1349
  if (arg.startsWith("--config=")) {
1078
1350
  configPath = arg.slice("--config=".length);
1351
+ configEnabled = true;
1079
1352
  }
1080
1353
  }
1081
1354
 
@@ -1085,12 +1358,26 @@ function findConfigPath(args) {
1085
1358
  if (configPath) {
1086
1359
  return configPath;
1087
1360
  }
1088
- for (const candidate of CONFIG_FILENAMES) {
1089
- if (existsSync(candidate)) {
1090
- return candidate;
1361
+
1362
+ const config = findConfigPathFromDirectory(process.cwd());
1363
+ if (config) {
1364
+ return config;
1365
+ }
1366
+
1367
+ let directory = process.cwd();
1368
+ while (true) {
1369
+ for (const filename of ESLINT_CONFIG_FILENAMES) {
1370
+ const candidate = resolvePath(directory, filename);
1371
+ if (existsSync(candidate)) {
1372
+ return candidate;
1373
+ }
1374
+ }
1375
+ const parent = dirname(directory);
1376
+ if (parent === directory) {
1377
+ return undefined;
1091
1378
  }
1379
+ directory = parent;
1092
1380
  }
1093
- return undefined;
1094
1381
  }
1095
1382
 
1096
1383
  function writeOutput(text, file) {
@@ -1251,8 +1538,8 @@ function filterQuietReport(report) {
1251
1538
  };
1252
1539
  }
1253
1540
 
1254
- function normalizeReportSeverities(report, ruleSeverities) {
1255
- if (ruleSeverities.size === 0) {
1541
+ function normalizeReportSeverities(report, ruleResolution, input) {
1542
+ if (ruleResolution.configuredRules.size === 0) {
1256
1543
  return report;
1257
1544
  }
1258
1545
 
@@ -1263,7 +1550,10 @@ function normalizeReportSeverities(report, ruleSeverities) {
1263
1550
  return [diagnostic];
1264
1551
  }
1265
1552
 
1266
- const severity = ruleSeverities.get(diagnostic.ruleId);
1553
+ const severity = severityForDiagnostic(ruleResolution, diagnostic, input);
1554
+ if (severity === undefined && ruleResolution.configuredRules.has(diagnostic.ruleId)) {
1555
+ return [];
1556
+ }
1267
1557
  if (severity === 0) {
1268
1558
  return [];
1269
1559
  }
@@ -1275,6 +1565,81 @@ function normalizeReportSeverities(report, ruleSeverities) {
1275
1565
  };
1276
1566
  }
1277
1567
 
1568
+ function severityForDiagnostic(ruleResolution, diagnostic, input) {
1569
+ if (Object.hasOwn(ruleResolution.ruleOverrides, diagnostic.ruleId)) {
1570
+ return ruleConfigSeverity(ruleResolution.ruleOverrides[diagnostic.ruleId]);
1571
+ }
1572
+ if (ruleResolution.selectedRules.has(diagnostic.ruleId)) {
1573
+ return 1;
1574
+ }
1575
+
1576
+ const filePath = diagnostic.filePath === input.file && input.displayPath
1577
+ ? input.displayPath
1578
+ : diagnostic.filePath;
1579
+ const relativePath = normalizePath(relative(ruleResolution.configDirectory, canonicalFilePath(filePath)));
1580
+ const rules = rulesForFile(ruleResolution.config, relativePath);
1581
+ return Object.hasOwn(rules, diagnostic.ruleId)
1582
+ ? ruleConfigSeverity(rules[diagnostic.ruleId])
1583
+ : undefined;
1584
+ }
1585
+
1586
+ function rulesForFile(config, filePath) {
1587
+ if (!config) {
1588
+ return {};
1589
+ }
1590
+ if (Array.isArray(config)) {
1591
+ return config.reduce((rules, entry) => ({
1592
+ ...rules,
1593
+ ...rulesForFile(entry, filePath)
1594
+ }), {});
1595
+ }
1596
+ if (!configAppliesToFile(config, filePath)) {
1597
+ return {};
1598
+ }
1599
+ return rulesFromConfig(config);
1600
+ }
1601
+
1602
+ function configAppliesToFile(config, filePath) {
1603
+ const files = normalizeConfigPatterns(config.files);
1604
+ if (files.length > 0 && !files.some((pattern) => matchesConfigFilePattern(filePath, normalizeIgnoredPattern(pattern)))) {
1605
+ return false;
1606
+ }
1607
+ return !isIgnoredTarget(filePath, normalizeConfigPatterns(config.ignores));
1608
+ }
1609
+
1610
+ function normalizeConfigPatterns(patterns) {
1611
+ if (!patterns) {
1612
+ return [];
1613
+ }
1614
+ const values = Array.isArray(patterns) ? patterns : [patterns];
1615
+ return values.flatMap((value) => {
1616
+ if (typeof value === "string") {
1617
+ return [value];
1618
+ }
1619
+ return Array.isArray(value) ? normalizeConfigPatterns(value) : [];
1620
+ });
1621
+ }
1622
+
1623
+ function matchesConfigFilePattern(target, pattern) {
1624
+ if (!hasGlobSyntax(pattern)) {
1625
+ return target === pattern || target.startsWith(`${pattern}/`) || target.endsWith(`/${pattern}`);
1626
+ }
1627
+ return new RegExp(`^${globPatternRegExpSource(pattern)}$`).test(target);
1628
+ }
1629
+
1630
+ function canonicalFilePath(path) {
1631
+ const absolute = resolvePath(path);
1632
+ try {
1633
+ return realpathSync.native(absolute);
1634
+ } catch {
1635
+ try {
1636
+ return join(realpathSync.native(dirname(absolute)), basename(absolute));
1637
+ } catch {
1638
+ return absolute;
1639
+ }
1640
+ }
1641
+ }
1642
+
1278
1643
  function rewriteReportStdinPaths(report, input) {
1279
1644
  if (!input.file || !input.displayPath) {
1280
1645
  return report;
package/bin/migrate.js CHANGED
@@ -18,6 +18,7 @@ const IGNORED_RULES = new Map([
18
18
  ]);
19
19
  const SCHEMA_URL = "https://raw.githubusercontent.com/fireairforce/utoo-lint/main/npm/utoo-lint/schema.json";
20
20
  const JAVASCRIPT_CONFIG_LOADER_SCRIPT = `
21
+ import { writeFileSync } from "node:fs";
21
22
  import { pathToFileURL } from "node:url";
22
23
 
23
24
  const loaded = await import(pathToFileURL(process.argv[1]).href);
@@ -57,7 +58,7 @@ const json = JSON.stringify(strip(loaded.default ?? loaded));
57
58
  if (json === undefined) {
58
59
  throw new TypeError("config did not export a migratable value");
59
60
  }
60
- process.stdout.write(json);
61
+ writeFileSync(3, json);
61
62
  `;
62
63
 
63
64
  export async function runMigrate(args) {
@@ -115,7 +116,7 @@ function parseEslintMigrateArgs(args) {
115
116
  force: false,
116
117
  from: undefined,
117
118
  help: false,
118
- output: "utoo.json",
119
+ output: "utlint.config.json",
119
120
  print: false,
120
121
  report: "text"
121
122
  };
@@ -249,7 +250,8 @@ function loadEslintConfig(path, cwd) {
249
250
  if (JAVASCRIPT_CONFIG_EXTENSIONS.has(extname(path))) {
250
251
  const result = spawnSync(process.execPath, ["--input-type=module", "--eval", JAVASCRIPT_CONFIG_LOADER_SCRIPT, path], {
251
252
  cwd,
252
- encoding: "utf8"
253
+ encoding: "utf8",
254
+ stdio: ["ignore", "pipe", "pipe", "pipe"]
253
255
  });
254
256
  if (result.error) {
255
257
  throw result.error;
@@ -257,7 +259,7 @@ function loadEslintConfig(path, cwd) {
257
259
  if (result.status !== 0) {
258
260
  throw new Error(`utoo-lint migrate eslint: unable to load ${path}: ${(result.stderr ?? "").trim() || "JavaScript config loader failed"}`);
259
261
  }
260
- return parseJson(result.stdout, path);
262
+ return parseJson(result.output?.[3] ?? "", path);
261
263
  }
262
264
  return parseJson(readFileSync(path, "utf8"), path);
263
265
  }
@@ -335,7 +337,7 @@ function printMigrateHelp() {
335
337
  utoo-lint migrate eslint [options]
336
338
 
337
339
  Sources:
338
- eslint Convert an ESLint config into utoo.json
340
+ eslint Convert an ESLint config into utlint.config.json
339
341
 
340
342
  Run "utoo-lint migrate eslint --help" for source-specific options.`);
341
343
  }
@@ -346,7 +348,7 @@ function printEslintMigrateHelp() {
346
348
 
347
349
  Options:
348
350
  --from=PATH Read ESLint config from PATH
349
- --output=PATH, -o PATH Write utoo config to PATH (default: utoo.json)
351
+ --output=PATH, -o PATH Write utoo config to PATH (default: utlint.config.json)
350
352
  --force Overwrite the output file when it exists
351
353
  --print Print the generated config instead of writing it
352
354
  --report=text|json Select migration report format