react-doctor 0.2.14-dev.5f7cc7c → 0.2.14-dev.6448d5b

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/dist/index.js CHANGED
@@ -59,6 +59,7 @@ var Diagnostic = class extends Schema.Class("Diagnostic")({
59
59
  plugin: Schema.String,
60
60
  rule: Schema.String,
61
61
  severity: Severity,
62
+ title: Schema.optional(Schema.String),
62
63
  message: Schema.String,
63
64
  help: Schema.String,
64
65
  url: Schema.optional(Schema.String),
@@ -2094,6 +2095,8 @@ const isFile = (filePath) => {
2094
2095
  }
2095
2096
  };
2096
2097
  const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
2098
+ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.js$/i;
2099
+ const MINIFIED_SNIFF_BYTES = 65536;
2097
2100
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
2098
2101
  const IGNORED_DIRECTORIES = new Set([
2099
2102
  ".git",
@@ -2109,6 +2112,34 @@ const IGNORED_DIRECTORIES = new Set([
2109
2112
  "out",
2110
2113
  "storybook-static"
2111
2114
  ]);
2115
+ const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
2116
+ const isMinifiedSource = (absolutePath) => {
2117
+ let fileDescriptor;
2118
+ try {
2119
+ fileDescriptor = fs.openSync(absolutePath, "r");
2120
+ const buffer = Buffer.alloc(MINIFIED_SNIFF_BYTES);
2121
+ const bytesRead = fs.readSync(fileDescriptor, buffer, 0, MINIFIED_SNIFF_BYTES, 0);
2122
+ const prefix = buffer.toString("utf8", 0, bytesRead);
2123
+ const lines = prefix.split("\n");
2124
+ const longestLineLength = lines.reduce((longest, line) => Math.max(longest, line.length), 0);
2125
+ const averageLineLength = prefix.length / lines.length;
2126
+ return longestLineLength > 1e3 && averageLineLength > 500;
2127
+ } catch {
2128
+ return false;
2129
+ } finally {
2130
+ if (fileDescriptor !== void 0) fs.closeSync(fileDescriptor);
2131
+ }
2132
+ };
2133
+ const isLargeMinifiedFile = (absolutePath) => {
2134
+ let sizeBytes;
2135
+ try {
2136
+ sizeBytes = fs.statSync(absolutePath).size;
2137
+ } catch {
2138
+ return false;
2139
+ }
2140
+ if (sizeBytes < 2e4) return false;
2141
+ return isMinifiedSource(absolutePath);
2142
+ };
2112
2143
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
2113
2144
  "EACCES",
2114
2145
  "EPERM",
@@ -2139,7 +2170,7 @@ const countSourceFilesViaFilesystem = (rootDirectory) => {
2139
2170
  if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(path.join(currentDirectory, entry.name));
2140
2171
  continue;
2141
2172
  }
2142
- if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) count++;
2173
+ if (entry.isFile() && isLintableSourceFile(entry.name) && !isLargeMinifiedFile(path.join(currentDirectory, entry.name))) count++;
2143
2174
  }
2144
2175
  }
2145
2176
  return count;
@@ -2157,7 +2188,7 @@ const countSourceFilesViaGit = (rootDirectory) => {
2157
2188
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
2158
2189
  });
2159
2190
  if (result.error || result.status !== 0) return null;
2160
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
2191
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !isLargeMinifiedFile(path.resolve(rootDirectory, filePath))).length;
2161
2192
  };
2162
2193
  const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
2163
2194
  const cachedPackageJsons = /* @__PURE__ */ new Map();
@@ -3240,7 +3271,15 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
3240
3271
  ];
3241
3272
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
3242
3273
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
3274
+ const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
3243
3275
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
3276
+ const DIAGNOSTIC_CATEGORY_BUCKETS = [
3277
+ "Security",
3278
+ "Bugs",
3279
+ "Performance",
3280
+ "Accessibility",
3281
+ "Maintainability"
3282
+ ];
3244
3283
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
3245
3284
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
3246
3285
  var InvalidGlobPatternError = class extends Error {
@@ -3862,10 +3901,13 @@ const collectStringSet = (values) => {
3862
3901
  * wins over `test-noise`)
3863
3902
  * 2. severity overrides (top-level `rules` / `categories`, with
3864
3903
  * `"off"` dropping)
3865
- * 3. ignore filters (rules / file patterns / per-file overrides)
3866
- * 4. `rn-no-raw-text` suppression via configured `textComponents` and
3904
+ * 3. warning suppression (only when `showWarnings` is false: drops every
3905
+ * `"warning"`-severity diagnostic unless a severity override opts a
3906
+ * specific rule / category back in)
3907
+ * 4. ignore filters (rules / file patterns / per-file overrides)
3908
+ * 5. `rn-no-raw-text` suppression via configured `textComponents` and
3867
3909
  * `rawTextWrapperComponents` (config-driven JSX enclosure checks)
3868
- * 5. inline suppressions (`// react-doctor-disable-next-line ...`)
3910
+ * 6. inline suppressions (`// react-doctor-disable-next-line ...`)
3869
3911
  *
3870
3912
  * Returns `null` when the diagnostic is dropped, the (possibly
3871
3913
  * severity-restamped) diagnostic otherwise.
@@ -3875,7 +3917,7 @@ const collectStringSet = (values) => {
3875
3917
  * `mergeAndFilterDiagnostics` wrapper apply this closure per element.
3876
3918
  */
3877
3919
  const buildDiagnosticPipeline = (input) => {
3878
- const { rootDirectory, userConfig, readFileLinesSync, respectInlineDisables } = input;
3920
+ const { rootDirectory, userConfig, readFileLinesSync, respectInlineDisables, showWarnings } = input;
3879
3921
  const severityControls = buildRuleSeverityControls(userConfig);
3880
3922
  const ignoredRules = new Set(Array.isArray(userConfig?.ignore?.rules) ? userConfig.ignore.rules.filter((rule) => typeof rule === "string") : []);
3881
3923
  const ignoredFilePatterns = compileIgnoredFilePatterns(userConfig);
@@ -3925,15 +3967,17 @@ const buildDiagnosticPipeline = (input) => {
3925
3967
  return { apply: (diagnostic) => {
3926
3968
  if (shouldAutoSuppress(diagnostic)) return null;
3927
3969
  let current = diagnostic;
3970
+ let explicitSeverityOverride;
3928
3971
  if (severityControls) {
3929
3972
  const { ruleKey, category } = getDiagnosticRuleIdentity(current);
3930
- const override = resolveRuleSeverityOverride({
3973
+ explicitSeverityOverride = resolveRuleSeverityOverride({
3931
3974
  ruleKey,
3932
3975
  category
3933
3976
  }, severityControls);
3934
- if (override === "off") return null;
3935
- if (override !== void 0) current = restampSeverity(current, override);
3977
+ if (explicitSeverityOverride === "off") return null;
3978
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
3936
3979
  }
3980
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
3937
3981
  if (userConfig) {
3938
3982
  if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
3939
3983
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
@@ -4165,10 +4209,18 @@ const VALID_RULE_SEVERITIES = [
4165
4209
  "warn",
4166
4210
  "off"
4167
4211
  ];
4212
+ const KNOWN_CATEGORY_LABEL = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
4213
+ const isDiagnosticCategoryBucket = (value) => DIAGNOSTIC_CATEGORY_BUCKETS.includes(value);
4214
+ const filterKnownCategories = (fieldName, categories) => categories.filter((category) => {
4215
+ if (isDiagnosticCategoryBucket(category)) return true;
4216
+ warnConfigIssue(`config field "${fieldName}" lists "${category}", which is not a known category (expected one of: ${KNOWN_CATEGORY_LABEL}); ignoring the entry.`);
4217
+ return false;
4218
+ });
4168
4219
  const BOOLEAN_FIELD_NAMES = [
4169
4220
  "lint",
4170
4221
  "deadCode",
4171
4222
  "verbose",
4223
+ "warnings",
4172
4224
  "customRulesOnly",
4173
4225
  "share",
4174
4226
  "noScore",
@@ -4217,13 +4269,15 @@ const validateSurfaceControls = (surface, rawControls) => {
4217
4269
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
4218
4270
  return;
4219
4271
  }
4220
- const validated = {};
4272
+ const validatedSurfaceControls = {};
4221
4273
  for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
4222
4274
  if (rawControls[fieldName] === void 0) continue;
4223
- const result = validateStringArrayField(`surfaces.${surface}.${fieldName}`, rawControls[fieldName]);
4224
- if (result !== void 0) validated[fieldName] = result;
4275
+ const qualifiedName = `surfaces.${surface}.${fieldName}`;
4276
+ const result = validateStringArrayField(qualifiedName, rawControls[fieldName]);
4277
+ if (result === void 0) continue;
4278
+ validatedSurfaceControls[fieldName] = fieldName === "includeCategories" || fieldName === "excludeCategories" ? filterKnownCategories(qualifiedName, result) : result;
4225
4279
  }
4226
- return validated;
4280
+ return validatedSurfaceControls;
4227
4281
  };
4228
4282
  const validateSurfacesField = (rawSurfaces) => {
4229
4283
  if (!isPlainObject$1(rawSurfaces)) {
@@ -4241,7 +4295,7 @@ const validateSurfacesField = (rawSurfaces) => {
4241
4295
  }
4242
4296
  return validated;
4243
4297
  };
4244
- const validateSeverityMap = (fieldName, rawMap) => {
4298
+ const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
4245
4299
  if (!isPlainObject$1(rawMap)) {
4246
4300
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
4247
4301
  return;
@@ -4252,6 +4306,10 @@ const validateSeverityMap = (fieldName, rawMap) => {
4252
4306
  warnConfigIssue(`config field "${fieldName}" has an empty key; ignoring the entry.`);
4253
4307
  continue;
4254
4308
  }
4309
+ if (keysAreCategories && !isDiagnosticCategoryBucket(key)) {
4310
+ warnConfigIssue(`config field "${fieldName}.${key}" is not a known category (expected one of: ${KNOWN_CATEGORY_LABEL}); ignoring the entry.`);
4311
+ continue;
4312
+ }
4255
4313
  if (!isRuleSeverity(value)) {
4256
4314
  warnConfigIssue(`config field "${fieldName}.${key}" must be one of: ${VALID_RULE_SEVERITIES.join(", ")} (got ${formatType(value)}); ignoring the entry.`);
4257
4315
  continue;
@@ -4272,7 +4330,7 @@ const validateConfigTypes = (config) => {
4272
4330
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
4273
4331
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
4274
4332
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
4275
- for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value));
4333
+ for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
4276
4334
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
4277
4335
  return validated;
4278
4336
  };
@@ -4572,99 +4630,6 @@ const checkReducedMotion = (rootDirectory) => {
4572
4630
  return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
4573
4631
  };
4574
4632
  const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
4575
- const toStringSet = (values) => {
4576
- if (!values || values.length === 0) return /* @__PURE__ */ new Set();
4577
- return new Set(values.filter((value) => typeof value === "string" && value.length > 0));
4578
- };
4579
- const buildResolvedControls = (surface, userControls) => {
4580
- const excludeTags = new Set(DEFAULT_SURFACE_EXCLUDED_TAGS[surface]);
4581
- const includeTags = toStringSet(userControls?.includeTags);
4582
- for (const tag of includeTags) excludeTags.delete(tag);
4583
- for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
4584
- return {
4585
- includeTags,
4586
- excludeTags,
4587
- includeCategories: toStringSet(userControls?.includeCategories),
4588
- excludeCategories: toStringSet(userControls?.excludeCategories),
4589
- includeRuleKeys: toStringSet(userControls?.includeRules),
4590
- excludeRuleKeys: toStringSet(userControls?.excludeRules)
4591
- };
4592
- };
4593
- const intersects = (values, candidates) => values.some((value) => candidates.has(value));
4594
- const isDiagnosticOnSurface = (diagnostic, surface, config) => {
4595
- const resolved = buildResolvedControls(surface, config?.surfaces?.[surface]);
4596
- const { ruleKey, category, tags } = getDiagnosticRuleIdentity(diagnostic);
4597
- if (resolved.includeRuleKeys.has(ruleKey)) return true;
4598
- if (resolved.includeCategories.has(category)) return true;
4599
- if (intersects(tags, resolved.includeTags)) return true;
4600
- if (resolved.excludeRuleKeys.has(ruleKey)) return false;
4601
- if (resolved.excludeCategories.has(category)) return false;
4602
- if (intersects(tags, resolved.excludeTags)) return false;
4603
- return true;
4604
- };
4605
- const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
4606
- const listSourceFilesViaGit = (rootDirectory) => {
4607
- const result = spawnSync("git", [
4608
- "ls-files",
4609
- "-z",
4610
- "--cached",
4611
- "--others",
4612
- "--exclude-standard"
4613
- ], {
4614
- cwd: rootDirectory,
4615
- encoding: "utf-8",
4616
- maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
4617
- });
4618
- if (result.error || result.status !== 0) return null;
4619
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
4620
- };
4621
- const listSourceFilesViaFilesystem = (rootDirectory) => {
4622
- const filePaths = [];
4623
- const stack = [rootDirectory];
4624
- while (stack.length > 0) {
4625
- const currentDirectory = stack.pop();
4626
- const entries = readDirectoryEntries(currentDirectory);
4627
- for (const entry of entries) {
4628
- const absolutePath = path.join(currentDirectory, entry.name);
4629
- if (entry.isDirectory()) {
4630
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
4631
- continue;
4632
- }
4633
- if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
4634
- }
4635
- }
4636
- return filePaths;
4637
- };
4638
- const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
4639
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
4640
- if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
4641
- const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
4642
- return listSourceFiles(rootDirectory).filter((filePath) => {
4643
- if (!JSX_FILE_PATTERN.test(filePath)) return false;
4644
- return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
4645
- });
4646
- };
4647
- var Config = class Config extends Context.Service()("react-doctor/Config") {
4648
- static layerNode = Layer.effect(Config, Effect.gen(function* () {
4649
- const cache = yield* Cache.make({
4650
- capacity: 16,
4651
- timeToLive: CONFIG_CACHE_TTL_MS,
4652
- lookup: (directory) => Effect.sync(() => {
4653
- const loaded = loadConfigWithSource(directory);
4654
- const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
4655
- return {
4656
- config: loaded?.config ?? null,
4657
- resolvedDirectory: redirected ?? directory,
4658
- configSourceDirectory: loaded?.sourceDirectory ?? null
4659
- };
4660
- })
4661
- });
4662
- return Config.of({ resolve: Effect.fn("Config.resolve")(function* (directory) {
4663
- return yield* Cache.get(cache, directory);
4664
- }) });
4665
- }));
4666
- static layerOf = (resolved) => Layer.succeed(Config, Config.of({ resolve: () => Effect.succeed(resolved) }));
4667
- };
4668
4633
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
4669
4634
  const FALSY_VALUES = new Set([
4670
4635
  "false",
@@ -4746,6 +4711,8 @@ const collectIgnorePatterns = (rootDirectory) => {
4746
4711
  cachedPatternsByRoot.set(rootDirectory, patterns);
4747
4712
  return patterns;
4748
4713
  };
4714
+ const DEAD_CODE_PLUGIN = "deslop";
4715
+ const DEAD_CODE_CATEGORY = "Maintainability";
4749
4716
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4750
4717
  const DEAD_CODE_WORKER_SCRIPT = `
4751
4718
  const inputChunks = [];
@@ -4921,7 +4888,11 @@ const buildDeadCodeWorkerError = (workerError) => {
4921
4888
  return error;
4922
4889
  };
4923
4890
  const createDeadCodeWorker = (input) => {
4924
- const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
4891
+ const child = spawn(process.execPath, [
4892
+ `--max-old-space-size=${DEAD_CODE_WORKER_MAX_OLD_SPACE_MB}`,
4893
+ "-e",
4894
+ DEAD_CODE_WORKER_SCRIPT
4895
+ ], {
4925
4896
  stdio: [
4926
4897
  "pipe",
4927
4898
  "pipe",
@@ -5009,59 +4980,162 @@ const checkDeadCode = async (options) => {
5009
4980
  const diagnostics = [];
5010
4981
  for (const unusedFile of result.unusedFiles) diagnostics.push({
5011
4982
  filePath: toRelative(unusedFile.path),
5012
- plugin: "deslop",
4983
+ plugin: DEAD_CODE_PLUGIN,
5013
4984
  rule: "unused-file",
5014
4985
  severity: "warning",
5015
4986
  message: "Unused file — not reachable from any entry point",
5016
4987
  help: "Delete the file if it is truly unreachable, or import it from an entry point.",
5017
4988
  line: 0,
5018
4989
  column: 0,
5019
- category: "Dead Code"
4990
+ category: DEAD_CODE_CATEGORY
5020
4991
  });
5021
4992
  for (const unusedExport of result.unusedExports) {
5022
4993
  const label = unusedExport.isTypeOnly ? "type export" : "export";
5023
4994
  diagnostics.push({
5024
4995
  filePath: toRelative(unusedExport.path),
5025
- plugin: "deslop",
4996
+ plugin: DEAD_CODE_PLUGIN,
5026
4997
  rule: unusedExport.isTypeOnly ? "unused-type" : "unused-export",
5027
4998
  severity: "warning",
5028
4999
  message: `Unused ${label}: \`${unusedExport.name}\``,
5029
5000
  help: "Drop the `export` keyword (or remove the declaration) if no other module uses this symbol.",
5030
5001
  line: unusedExport.line,
5031
5002
  column: unusedExport.column,
5032
- category: "Dead Code"
5003
+ category: DEAD_CODE_CATEGORY
5033
5004
  });
5034
5005
  }
5035
5006
  for (const unusedDependency of result.unusedDependencies) {
5036
5007
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
5037
5008
  diagnostics.push({
5038
5009
  filePath: "package.json",
5039
- plugin: "deslop",
5010
+ plugin: DEAD_CODE_PLUGIN,
5040
5011
  rule: unusedDependency.isDevDependency ? "unused-dev-dependency" : "unused-dependency",
5041
5012
  severity: "warning",
5042
5013
  message: `Unused ${label}: \`${unusedDependency.name}\``,
5043
5014
  help: "Remove the dependency from package.json if it is genuinely unused.",
5044
5015
  line: 0,
5045
5016
  column: 0,
5046
- category: "Dead Code"
5017
+ category: DEAD_CODE_CATEGORY
5047
5018
  });
5048
5019
  }
5049
5020
  for (const cycle of result.circularDependencies) {
5050
5021
  if (cycle.files.length === 0) continue;
5051
5022
  diagnostics.push({
5052
5023
  filePath: toRelative(cycle.files[0]),
5053
- plugin: "deslop",
5024
+ plugin: DEAD_CODE_PLUGIN,
5054
5025
  rule: "circular-dependency",
5055
5026
  severity: "warning",
5056
5027
  message: `Circular import cycle: ${cycle.files.map(toRelative).join(" → ")}`,
5057
5028
  help: "Break the cycle by extracting the shared code into a third module that both files import.",
5058
5029
  line: 0,
5059
5030
  column: 0,
5060
- category: "Dead Code"
5031
+ category: DEAD_CODE_CATEGORY
5061
5032
  });
5062
5033
  }
5063
5034
  return diagnostics;
5064
5035
  };
5036
+ const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
5037
+ const isSurfacingOverride = (override) => override === "warn" || override === "error";
5038
+ const deadCodeMaySurfaceWhenWarningsHidden = (userConfig) => {
5039
+ const severityControls = buildRuleSeverityControls(userConfig);
5040
+ if (!severityControls) return false;
5041
+ if (isSurfacingOverride(severityControls.categories?.["Maintainability"])) return true;
5042
+ for (const [ruleKey, override] of Object.entries(severityControls.rules ?? {})) if (ruleKey.startsWith(DEAD_CODE_RULE_KEY_PREFIX) && isSurfacingOverride(override)) return true;
5043
+ return false;
5044
+ };
5045
+ const toStringSet = (values) => {
5046
+ if (!values || values.length === 0) return /* @__PURE__ */ new Set();
5047
+ return new Set(values.filter((value) => typeof value === "string" && value.length > 0));
5048
+ };
5049
+ const buildResolvedControls = (surface, userControls) => {
5050
+ const excludeTags = new Set(DEFAULT_SURFACE_EXCLUDED_TAGS[surface]);
5051
+ const includeTags = toStringSet(userControls?.includeTags);
5052
+ for (const tag of includeTags) excludeTags.delete(tag);
5053
+ for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
5054
+ return {
5055
+ includeTags,
5056
+ excludeTags,
5057
+ includeCategories: toStringSet(userControls?.includeCategories),
5058
+ excludeCategories: toStringSet(userControls?.excludeCategories),
5059
+ includeRuleKeys: toStringSet(userControls?.includeRules),
5060
+ excludeRuleKeys: toStringSet(userControls?.excludeRules)
5061
+ };
5062
+ };
5063
+ const intersects = (values, candidates) => values.some((value) => candidates.has(value));
5064
+ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
5065
+ const resolved = buildResolvedControls(surface, config?.surfaces?.[surface]);
5066
+ const { ruleKey, category, tags } = getDiagnosticRuleIdentity(diagnostic);
5067
+ if (resolved.includeRuleKeys.has(ruleKey)) return true;
5068
+ if (resolved.includeCategories.has(category)) return true;
5069
+ if (intersects(tags, resolved.includeTags)) return true;
5070
+ if (resolved.excludeRuleKeys.has(ruleKey)) return false;
5071
+ if (resolved.excludeCategories.has(category)) return false;
5072
+ if (intersects(tags, resolved.excludeTags)) return false;
5073
+ return true;
5074
+ };
5075
+ const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
5076
+ const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(path.resolve(rootDirectory, relativePath)));
5077
+ const listSourceFilesViaGit = (rootDirectory) => {
5078
+ const result = spawnSync("git", [
5079
+ "ls-files",
5080
+ "-z",
5081
+ "--cached",
5082
+ "--others",
5083
+ "--exclude-standard"
5084
+ ], {
5085
+ cwd: rootDirectory,
5086
+ encoding: "utf-8",
5087
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
5088
+ });
5089
+ if (result.error || result.status !== 0) return null;
5090
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
5091
+ };
5092
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
5093
+ const filePaths = [];
5094
+ const stack = [rootDirectory];
5095
+ while (stack.length > 0) {
5096
+ const currentDirectory = stack.pop();
5097
+ const entries = readDirectoryEntries(currentDirectory);
5098
+ for (const entry of entries) {
5099
+ const absolutePath = path.join(currentDirectory, entry.name);
5100
+ if (entry.isDirectory()) {
5101
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
5102
+ continue;
5103
+ }
5104
+ if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
5105
+ }
5106
+ }
5107
+ return filePaths;
5108
+ };
5109
+ const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
5110
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
5111
+ if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
5112
+ const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
5113
+ return listSourceFiles(rootDirectory).filter((filePath) => {
5114
+ if (!JSX_FILE_PATTERN.test(filePath)) return false;
5115
+ return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
5116
+ });
5117
+ };
5118
+ var Config = class Config extends Context.Service()("react-doctor/Config") {
5119
+ static layerNode = Layer.effect(Config, Effect.gen(function* () {
5120
+ const cache = yield* Cache.make({
5121
+ capacity: 16,
5122
+ timeToLive: CONFIG_CACHE_TTL_MS,
5123
+ lookup: (directory) => Effect.sync(() => {
5124
+ const loaded = loadConfigWithSource(directory);
5125
+ const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
5126
+ return {
5127
+ config: loaded?.config ?? null,
5128
+ resolvedDirectory: redirected ?? directory,
5129
+ configSourceDirectory: loaded?.sourceDirectory ?? null
5130
+ };
5131
+ })
5132
+ });
5133
+ return Config.of({ resolve: Effect.fn("Config.resolve")(function* (directory) {
5134
+ return yield* Cache.get(cache, directory);
5135
+ }) });
5136
+ }));
5137
+ static layerOf = (resolved) => Layer.succeed(Config, Config.of({ resolve: () => Effect.succeed(resolved) }));
5138
+ };
5065
5139
  /**
5066
5140
  * `DeadCode` runs whole-project reachability analysis and streams
5067
5141
  * diagnostics. Reachability is a whole-project property — the
@@ -5567,12 +5641,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
5567
5641
  return null;
5568
5642
  }
5569
5643
  if (grepResult === null) return null;
5570
- return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
5644
+ return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
5571
5645
  };
5572
5646
  const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
5573
5647
  const matches = [];
5574
5648
  const checkFile = (relativePath) => {
5575
- if (!SOURCE_FILE_PATTERN.test(relativePath)) return;
5649
+ if (!isLintableSourceFile(relativePath)) return;
5576
5650
  const absolutePath = path.join(rootDirectory, relativePath);
5577
5651
  let content;
5578
5652
  try {
@@ -5939,6 +6013,149 @@ const appendReanimatedSharedValueHint = (help, rule, project) => {
5939
6013
  if (!help) return REANIMATED_SHARED_VALUE_HINT;
5940
6014
  return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
5941
6015
  };
6016
+ const REDACTED_PLACEHOLDER = "<redacted>";
6017
+ const KEEP_PREFIX = `$1${REDACTED_PLACEHOLDER}`;
6018
+ const KNOWN_SECRET_RULES = [
6019
+ {
6020
+ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
6021
+ replacement: REDACTED_PLACEHOLDER
6022
+ },
6023
+ {
6024
+ pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
6025
+ replacement: REDACTED_PLACEHOLDER
6026
+ },
6027
+ {
6028
+ pattern: /(?<=:\/\/)[^\s/:@]+:[^\s/@]+(?=@)/g,
6029
+ replacement: REDACTED_PLACEHOLDER
6030
+ },
6031
+ {
6032
+ pattern: /\b(AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA|A3T[A-Z0-9])[0-9A-Z]{16,}/g,
6033
+ replacement: KEEP_PREFIX
6034
+ },
6035
+ {
6036
+ pattern: /\b(gh[pousr]_)[A-Za-z0-9]{36,}/g,
6037
+ replacement: KEEP_PREFIX
6038
+ },
6039
+ {
6040
+ pattern: /\b(github_pat_)[A-Za-z0-9_]{22,}/g,
6041
+ replacement: KEEP_PREFIX
6042
+ },
6043
+ {
6044
+ pattern: /\b(glpat-)[A-Za-z0-9_-]{20,}/g,
6045
+ replacement: KEEP_PREFIX
6046
+ },
6047
+ {
6048
+ pattern: /\b(xox[baprs]-)[A-Za-z0-9-]{10,}/g,
6049
+ replacement: KEEP_PREFIX
6050
+ },
6051
+ {
6052
+ pattern: /(?<=hooks\.slack\.com\/services\/)[A-Za-z0-9/+_-]{20,}/g,
6053
+ replacement: REDACTED_PLACEHOLDER
6054
+ },
6055
+ {
6056
+ pattern: /\b((?:sk|rk)_(?:live|test)_)[0-9A-Za-z]{10,}/g,
6057
+ replacement: KEEP_PREFIX
6058
+ },
6059
+ {
6060
+ pattern: /\b(sk-(?:proj-|ant-)?)[A-Za-z0-9_-]{20,}/g,
6061
+ replacement: KEEP_PREFIX
6062
+ },
6063
+ {
6064
+ pattern: /\b(AIza)[0-9A-Za-z_-]{35,}/g,
6065
+ replacement: KEEP_PREFIX
6066
+ },
6067
+ {
6068
+ pattern: /\b(ya29\.)[0-9A-Za-z_-]{20,}/g,
6069
+ replacement: KEEP_PREFIX
6070
+ },
6071
+ {
6072
+ pattern: /\b(npm_)[A-Za-z0-9]{36,}/g,
6073
+ replacement: KEEP_PREFIX
6074
+ },
6075
+ {
6076
+ pattern: /\b(SG\.)[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{43,}/g,
6077
+ replacement: KEEP_PREFIX
6078
+ },
6079
+ {
6080
+ pattern: /\b(SK)[0-9a-fA-F]{32,}/g,
6081
+ replacement: KEEP_PREFIX
6082
+ },
6083
+ {
6084
+ pattern: /\b(dop_v1_)[a-f0-9]{64,}/g,
6085
+ replacement: KEEP_PREFIX
6086
+ },
6087
+ {
6088
+ pattern: /\b(shp(?:at|ca|pa|ss)_)[a-fA-F0-9]{32,}/g,
6089
+ replacement: KEEP_PREFIX
6090
+ },
6091
+ {
6092
+ pattern: /\b(sq0[a-z]{3}-)[0-9A-Za-z_-]{22,}/g,
6093
+ replacement: KEEP_PREFIX
6094
+ },
6095
+ {
6096
+ pattern: /\b([0-9]{8,10}:AA)[0-9A-Za-z_-]{32,}/g,
6097
+ replacement: KEEP_PREFIX
6098
+ },
6099
+ {
6100
+ pattern: /(?<=\bBearer\s)[A-Za-z0-9._~+/=-]{16,}/g,
6101
+ replacement: REDACTED_PLACEHOLDER
6102
+ },
6103
+ {
6104
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
6105
+ replacement: REDACTED_PLACEHOLDER
6106
+ }
6107
+ ];
6108
+ const CANDIDATE_TOKEN_PATTERN = /[A-Za-z0-9_][A-Za-z0-9_-]*/g;
6109
+ const HEX_DIGEST_PATTERN = /^(?:[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/;
6110
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6111
+ const HAS_LETTER_PATTERN = /[A-Za-z]/;
6112
+ const HAS_DIGIT_PATTERN = /[0-9]/;
6113
+ const shannonEntropyBits = (value) => {
6114
+ const counts = /* @__PURE__ */ new Map();
6115
+ for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
6116
+ let bits = 0;
6117
+ for (const count of counts.values()) {
6118
+ const probability = count / value.length;
6119
+ bits -= probability * Math.log2(probability);
6120
+ }
6121
+ return bits;
6122
+ };
6123
+ const looksLikeHighEntropySecret = (token) => {
6124
+ if (token.length < 32) return false;
6125
+ if (!HAS_LETTER_PATTERN.test(token) || !HAS_DIGIT_PATTERN.test(token)) return false;
6126
+ if (HEX_DIGEST_PATTERN.test(token) || UUID_PATTERN.test(token)) return false;
6127
+ return shannonEntropyBits(token) >= 3;
6128
+ };
6129
+ const redactHighEntropyTokens = (text) => text.replace(CANDIDATE_TOKEN_PATTERN, (token) => looksLikeHighEntropySecret(token) ? REDACTED_PLACEHOLDER : token);
6130
+ /**
6131
+ * Masks API keys, tokens, private keys, credentialed URLs, and emails
6132
+ * found anywhere inside a free-text string, returning the scrubbed text.
6133
+ * Applied to every diagnostic's `message` / `help` at construction time
6134
+ * so secrets never reach the terminal, the JSON report, or the score
6135
+ * API — react-doctor must never echo or transmit a user's secrets.
6136
+ *
6137
+ * Provider tokens keep their non-secret, type-identifying prefix (e.g.
6138
+ * `sk_live_<redacted>`, `ghp_<redacted>`, `AKIA<redacted>`) so the leaked
6139
+ * credential's type stays visible; structural or unknown-format secrets
6140
+ * with no meaningful prefix are masked whole.
6141
+ *
6142
+ * Runs the high-precision known-shape detectors first, then a generic
6143
+ * entropy-gated sweep for unknown-format secrets. Idempotent: the inert
6144
+ * `<redacted>` placeholder matches none of the detectors and is too
6145
+ * short for the generic sweep, so re-running leaves the text unchanged.
6146
+ *
6147
+ * Accepts `unknown` on purpose: callers feed it diagnostic `message` /
6148
+ * `help` that originate from oxlint JSON, which is only shape-checked at
6149
+ * the top level (the per-field `string` types are assumed, not validated).
6150
+ * A malformed non-string value returns `""` instead of throwing on
6151
+ * `.replace`, so one bad diagnostic can't abort parsing the whole batch.
6152
+ */
6153
+ const redactSensitiveText = (text) => {
6154
+ if (typeof text !== "string" || text === "") return "";
6155
+ let redacted = text;
6156
+ for (const rule of KNOWN_SECRET_RULES) redacted = redacted.replace(rule.pattern, rule.replacement);
6157
+ return redactHighEntropyTokens(redacted);
6158
+ };
5942
6159
  const REACT_MODULE_SOURCE = "react";
5943
6160
  const REQUIRE_IDENTIFIER = "require";
5944
6161
  const USE_IDENTIFIER = "use";
@@ -6233,25 +6450,26 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
6233
6450
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
6234
6451
  };
6235
6452
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
6236
- const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
6453
+ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
6454
+ const REACT_COMPILER_MESSAGE = "This component misses React Compiler's automatic memoization & re-renders more than it should. Rewrite the flagged code so the compiler can optimize it.";
6237
6455
  const PLUGIN_CATEGORY_MAP = {
6238
- react: "Correctness",
6239
- "react-hooks": "Correctness",
6240
- "react-hooks-js": "React Compiler",
6241
- "react-doctor": "Other",
6456
+ react: "Bugs",
6457
+ "react-hooks": "Bugs",
6458
+ "react-hooks-js": "Performance",
6459
+ "react-doctor": "Bugs",
6242
6460
  "jsx-a11y": "Accessibility",
6243
- effect: "State & Effects",
6244
- eslint: "Correctness",
6245
- oxc: "Correctness",
6246
- typescript: "Correctness",
6247
- unicorn: "Correctness",
6248
- import: "Bundle Size",
6249
- promise: "Correctness",
6250
- n: "Correctness",
6251
- node: "Correctness",
6252
- vitest: "Correctness",
6253
- jest: "Correctness",
6254
- nextjs: "Next.js"
6461
+ effect: "Bugs",
6462
+ eslint: "Bugs",
6463
+ oxc: "Bugs",
6464
+ typescript: "Bugs",
6465
+ unicorn: "Bugs",
6466
+ import: "Performance",
6467
+ promise: "Bugs",
6468
+ n: "Bugs",
6469
+ node: "Bugs",
6470
+ vitest: "Bugs",
6471
+ jest: "Bugs",
6472
+ nextjs: "Bugs"
6255
6473
  };
6256
6474
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
6257
6475
  const getRuleRecommendation = (ruleName, project) => {
@@ -6259,7 +6477,16 @@ const getRuleRecommendation = (ruleName, project) => {
6259
6477
  return reactDoctorPlugin.rules[ruleName]?.recommendation;
6260
6478
  };
6261
6479
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
6480
+ const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
6481
+ const resolveDiagnosticTitle = (plugin, rule) => plugin === "react-hooks-js" ? REACT_COMPILER_TITLE : getRuleTitle(rule);
6262
6482
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
6483
+ const cleaned = resolveCleanedDiagnostic(typeof message === "string" ? message : "", typeof help === "string" ? help : "", plugin, rule, project);
6484
+ return {
6485
+ message: redactSensitiveText(cleaned.message),
6486
+ help: redactSensitiveText(cleaned.help)
6487
+ };
6488
+ };
6489
+ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
6263
6490
  if (plugin === "react-hooks-js") return {
6264
6491
  message: REACT_COMPILER_MESSAGE,
6265
6492
  help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
@@ -6280,7 +6507,7 @@ const parseRuleCode = (code) => {
6280
6507
  rule: match[2]
6281
6508
  };
6282
6509
  };
6283
- const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Other";
6510
+ const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
6284
6511
  const isOxlintOutput = (value) => {
6285
6512
  if (typeof value !== "object" || value === null) return false;
6286
6513
  const candidate = value;
@@ -6304,7 +6531,16 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
6304
6531
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
6305
6532
  }
6306
6533
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
6307
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && SOURCE_FILE_PATTERN.test(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
6534
+ const minifiedFileCache = /* @__PURE__ */ new Map();
6535
+ const isMinifiedDiagnosticFile = (filename) => {
6536
+ const absolutePath = path.isAbsolute(filename) ? filename : path.resolve(rootDirectory || ".", filename);
6537
+ const cached = minifiedFileCache.get(absolutePath);
6538
+ if (cached !== void 0) return cached;
6539
+ const minified = isMinifiedSource(absolutePath);
6540
+ minifiedFileCache.set(absolutePath, minified);
6541
+ return minified;
6542
+ };
6543
+ return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
6308
6544
  const { plugin, rule } = parseRuleCode(diagnostic.code);
6309
6545
  const primaryLabel = diagnostic.labels[0];
6310
6546
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -6313,6 +6549,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
6313
6549
  plugin,
6314
6550
  rule,
6315
6551
  severity: diagnostic.severity,
6552
+ title: resolveDiagnosticTitle(plugin, rule),
6316
6553
  message: cleaned.message,
6317
6554
  help: cleaned.help,
6318
6555
  url: diagnostic.url,
@@ -6464,11 +6701,14 @@ const spawnLintBatches = async (input) => {
6464
6701
  onFileProgress(scannedFileCount + batchFileIndex, totalFileCount);
6465
6702
  }
6466
6703
  }, 50) : null;
6467
- const batchDiagnostics = await spawnLintBatch(batch);
6468
- if (progressInterval !== null) clearInterval(progressInterval);
6469
- allDiagnostics.push(...batchDiagnostics);
6470
- scannedFileCount += batch.length;
6471
- onFileProgress?.(scannedFileCount, totalFileCount);
6704
+ try {
6705
+ const batchDiagnostics = await spawnLintBatch(batch);
6706
+ allDiagnostics.push(...batchDiagnostics);
6707
+ scannedFileCount += batch.length;
6708
+ onFileProgress?.(scannedFileCount, totalFileCount);
6709
+ } finally {
6710
+ if (progressInterval !== null) clearInterval(progressInterval);
6711
+ }
6472
6712
  }
6473
6713
  if (droppedFiles.length > 0 && onPartialFailure) {
6474
6714
  const previewFiles = droppedFiles.slice(0, 3).join(", ");
@@ -6727,7 +6967,8 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
6727
6967
  static layerNoop = Layer.succeed(Progress, Progress.of({ start: () => Effect.succeed({
6728
6968
  update: () => Effect.void,
6729
6969
  succeed: () => Effect.void,
6730
- fail: () => Effect.void
6970
+ fail: () => Effect.void,
6971
+ stop: () => Effect.void
6731
6972
  }) }));
6732
6973
  static layerCapture = Layer.effect(Progress, Effect.map(ProgressCapture, (events) => Progress.of({ start: (text) => Effect.gen(function* () {
6733
6974
  yield* Ref.update(events, (existing) => [...existing, {
@@ -6746,6 +6987,10 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
6746
6987
  fail: (displayText) => Ref.update(events, (existing) => [...existing, {
6747
6988
  _tag: "Failed",
6748
6989
  text: displayText
6990
+ }]),
6991
+ stop: () => Ref.update(events, (existing) => [...existing, {
6992
+ _tag: "Stopped",
6993
+ text
6749
6994
  }])
6750
6995
  };
6751
6996
  }) }))).pipe(Layer.provideMerge(ProgressCapture.layer));
@@ -6817,17 +7062,21 @@ var Reporter = class Reporter extends Context.Service()("react-doctor/Reporter")
6817
7062
  });
6818
7063
  }));
6819
7064
  };
6820
- const parseScoreResult = (value) => {
6821
- if (typeof value !== "object" || value === null) return null;
6822
- if (!("score" in value) || !("label" in value)) return null;
6823
- const scoreValue = Reflect.get(value, "score");
6824
- const labelValue = Reflect.get(value, "label");
6825
- if (typeof scoreValue !== "number" || typeof labelValue !== "string") return null;
6826
- return {
6827
- score: scoreValue,
6828
- label: labelValue
6829
- };
6830
- };
7065
+ const RulePrioritySchema = Schema.Struct({
7066
+ priority: Schema.NullOr(Schema.Number),
7067
+ tier: Schema.Literals([
7068
+ "P0",
7069
+ "P1",
7070
+ "P2",
7071
+ "P3"
7072
+ ])
7073
+ });
7074
+ const ScoreApiResponseSchema = Schema.Struct({
7075
+ score: Schema.Number,
7076
+ label: Schema.String,
7077
+ rules: Schema.optional(Schema.Record(Schema.String, RulePrioritySchema))
7078
+ });
7079
+ const parseScoreResult = (value) => Option.getOrNull(Schema.decodeUnknownOption(ScoreApiResponseSchema)(value));
6831
7080
  const stripFilePaths = (diagnostics) => diagnostics.map(({ filePath: _filePath, ...rest }) => rest);
6832
7081
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
6833
7082
  const describeFailure = (error) => {
@@ -6991,15 +7240,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6991
7240
  repo
6992
7241
  }).pipe(Effect.orElseSucceed(() => null)) : Effect.succeed(null));
6993
7242
  const lintIncludePaths = computeJsxIncludePaths([...input.includePaths]) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
7243
+ const scannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => path.resolve(scanDirectory, relativePath)) : [];
6994
7244
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
6995
7245
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
6996
7246
  yield* beforeLint(project, lintIncludePaths ?? void 0);
6997
7247
  const isDiffMode = input.includePaths.length > 0;
7248
+ const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? false;
6998
7249
  const transform = buildDiagnosticPipeline({
6999
7250
  rootDirectory: scanDirectory,
7000
7251
  userConfig: resolvedConfig.config,
7001
7252
  readFileLinesSync: fileReader(filesService, scanDirectory),
7002
- respectInlineDisables: input.respectInlineDisables
7253
+ respectInlineDisables: input.respectInlineDisables,
7254
+ showWarnings
7003
7255
  });
7004
7256
  const applyPerElementPipeline = (rawStream) => rawStream.pipe(Stream.filterMap(filterMapNullable(transform.apply)), Stream.tap((diagnostic) => reporterService.emit(diagnostic)));
7005
7257
  const environmentDiagnostics = isDiffMode ? [] : [...checkReducedMotion(scanDirectory), ...checkPnpmHardening(scanDirectory)];
@@ -7045,7 +7297,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7045
7297
  const lintFailureState = yield* Ref.get(lintFailure);
7046
7298
  yield* afterLint(lintFailureState.didFail);
7047
7299
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
7048
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
7300
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
7049
7301
  const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
7050
7302
  rootDirectory: scanDirectory,
7051
7303
  userConfig: resolvedConfig.config
@@ -7057,9 +7309,11 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7057
7309
  return Stream.empty;
7058
7310
  }))))))));
7059
7311
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
7060
- const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
7312
+ const scanElapsedMilliseconds = Date.now() - scanStartTime;
7313
+ const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
7061
7314
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
7062
7315
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
7316
+ else if (input.suppressScanSummary) yield* scanProgress.stop();
7063
7317
  else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s`);
7064
7318
  yield* reporterService.finalize;
7065
7319
  const finalDiagnostics = [
@@ -7100,7 +7354,10 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7100
7354
  lintFailureReasonKind: lintFailureState.reasonKind,
7101
7355
  lintPartialFailures,
7102
7356
  didDeadCodeFail: deadCodeFailureState.didFail,
7103
- deadCodeFailureReason: deadCodeFailureState.reason
7357
+ deadCodeFailureReason: deadCodeFailureState.reason,
7358
+ scannedFileCount: totalFileCount,
7359
+ scannedFilePaths,
7360
+ scanElapsedMilliseconds
7104
7361
  };
7105
7362
  }).pipe(Effect.withSpan("runInspect", { attributes: {
7106
7363
  "inspect.directory": input.directory,
@@ -7226,7 +7483,7 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
7226
7483
  static layerNode = Layer.effect(StagedFiles, Effect.gen(function* () {
7227
7484
  const git = yield* Git;
7228
7485
  return StagedFiles.of({
7229
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter((entry) => SOURCE_FILE_PATTERN.test(entry)))),
7486
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter(isLintableSourceFile))),
7230
7487
  materialize: ({ directory, stagedFiles, tempDirectory }) => Effect.gen(function* () {
7231
7488
  const materializedFiles = [];
7232
7489
  const resolvedTempDirectory = path.resolve(tempDirectory);
@@ -7435,7 +7692,7 @@ const getDiffInfo = (directory, explicitBaseBranch) => Effect.runPromise(Effect.
7435
7692
  GitBaseBranchInvalid: (reason) => Effect.die(new Error(reason.detail)),
7436
7693
  GitBaseBranchMissing: (reason) => Effect.die(new Error(reason.message))
7437
7694
  })));
7438
- const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
7695
+ const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
7439
7696
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
7440
7697
  let p = process || {}, argv = p.argv || [], env = p.env || {};
7441
7698
  let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
@@ -7522,6 +7779,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7522
7779
  includePaths,
7523
7780
  customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
7524
7781
  respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
7782
+ warnings: options.warnings ?? effectiveConfig?.warnings ?? false,
7525
7783
  adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
7526
7784
  ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
7527
7785
  runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,