react-doctor 0.2.14-dev.b612664 → 0.2.14-dev.daef23c

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,18 @@ 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
+ ];
3283
+ const APP_ONLY_RULE_KEYS = new Set(["react-hooks-js/static-components", "react-doctor/no-render-prop-children"]);
3284
+ const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
3285
+ const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
3244
3286
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
3245
3287
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
3246
3288
  var InvalidGlobPatternError = class extends Error {
@@ -3360,10 +3402,11 @@ const restampSeverity = (diagnostic, override) => {
3360
3402
  */
3361
3403
  const buildRuleSeverityControls = (config) => {
3362
3404
  if (!config) return void 0;
3363
- if (config.rules === void 0 && config.categories === void 0) return void 0;
3405
+ if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
3364
3406
  return {
3365
3407
  ...config.rules !== void 0 ? { rules: config.rules } : {},
3366
- ...config.categories !== void 0 ? { categories: config.categories } : {}
3408
+ ...config.categories !== void 0 ? { categories: config.categories } : {},
3409
+ ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
3367
3410
  };
3368
3411
  };
3369
3412
  const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
@@ -3727,6 +3770,69 @@ const resolveRuleSeverityOverride = (input, controls) => {
3727
3770
  }
3728
3771
  return input.category !== void 0 ? controls.categories?.[input.category] : void 0;
3729
3772
  };
3773
+ const cachedRoleByPackageDirectory = /* @__PURE__ */ new Map();
3774
+ const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
3775
+ const findNearestPackageDirectory = (filename) => {
3776
+ if (!filename) return null;
3777
+ const fromCache = cachedPackageDirectoryByFilename.get(filename);
3778
+ if (fromCache !== void 0) return fromCache;
3779
+ let currentDirectory = path.dirname(filename);
3780
+ while (true) {
3781
+ const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
3782
+ let hasPackageJson = false;
3783
+ try {
3784
+ hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
3785
+ } catch {
3786
+ hasPackageJson = false;
3787
+ }
3788
+ if (hasPackageJson) {
3789
+ cachedPackageDirectoryByFilename.set(filename, currentDirectory);
3790
+ return currentDirectory;
3791
+ }
3792
+ const parentDirectory = path.dirname(currentDirectory);
3793
+ if (parentDirectory === currentDirectory) {
3794
+ cachedPackageDirectoryByFilename.set(filename, null);
3795
+ return null;
3796
+ }
3797
+ currentDirectory = parentDirectory;
3798
+ }
3799
+ };
3800
+ const readManifest = (packageJsonPath) => {
3801
+ try {
3802
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
3803
+ if (typeof parsed === "object" && parsed !== null) return parsed;
3804
+ return null;
3805
+ } catch {
3806
+ return null;
3807
+ }
3808
+ };
3809
+ const hasPublishContract = (manifest) => typeof manifest.name === "string" && manifest.name.length > 0 && manifest.exports !== void 0 && manifest.exports !== null && manifest.private !== true;
3810
+ const classifyByDirectoryCohort = (packageDirectory) => {
3811
+ let current = packageDirectory;
3812
+ while (true) {
3813
+ if (path.basename(current) === "apps") return "app";
3814
+ const parent = path.dirname(current);
3815
+ if (parent === current) return null;
3816
+ current = parent;
3817
+ }
3818
+ };
3819
+ const clearPackageRoleCache = () => {
3820
+ cachedRoleByPackageDirectory.clear();
3821
+ cachedPackageDirectoryByFilename.clear();
3822
+ };
3823
+ const classifyPackageRole = (filename) => {
3824
+ if (!filename) return "unknown";
3825
+ const packageDirectory = findNearestPackageDirectory(filename);
3826
+ if (!packageDirectory) return "unknown";
3827
+ const cached = cachedRoleByPackageDirectory.get(packageDirectory);
3828
+ if (cached !== void 0) return cached;
3829
+ const manifest = readManifest(path.join(packageDirectory, "package.json"));
3830
+ let result;
3831
+ if (manifest && hasPublishContract(manifest)) result = "library";
3832
+ else result = classifyByDirectoryCohort(path.dirname(packageDirectory)) ?? "unknown";
3833
+ cachedRoleByPackageDirectory.set(packageDirectory, result);
3834
+ return result;
3835
+ };
3730
3836
  /**
3731
3837
  * Resolves the absolute path to read for a diagnostic's `filePath`,
3732
3838
  * accounting for the various shapes oxlint emits:
@@ -3862,10 +3968,13 @@ const collectStringSet = (values) => {
3862
3968
  * wins over `test-noise`)
3863
3969
  * 2. severity overrides (top-level `rules` / `categories`, with
3864
3970
  * `"off"` dropping)
3865
- * 3. ignore filters (rules / file patterns / per-file overrides)
3866
- * 4. `rn-no-raw-text` suppression via configured `textComponents` and
3971
+ * 3. warning suppression (only when `showWarnings` is false: drops every
3972
+ * `"warning"`-severity diagnostic unless a severity override opts a
3973
+ * specific rule / category back in)
3974
+ * 4. ignore filters (rules / file patterns / per-file overrides)
3975
+ * 5. `rn-no-raw-text` suppression via configured `textComponents` and
3867
3976
  * `rawTextWrapperComponents` (config-driven JSX enclosure checks)
3868
- * 5. inline suppressions (`// react-doctor-disable-next-line ...`)
3977
+ * 6. inline suppressions (`// react-doctor-disable-next-line ...`)
3869
3978
  *
3870
3979
  * Returns `null` when the diagnostic is dropped, the (possibly
3871
3980
  * severity-restamped) diagnostic otherwise.
@@ -3875,7 +3984,7 @@ const collectStringSet = (values) => {
3875
3984
  * `mergeAndFilterDiagnostics` wrapper apply this closure per element.
3876
3985
  */
3877
3986
  const buildDiagnosticPipeline = (input) => {
3878
- const { rootDirectory, userConfig, readFileLinesSync, respectInlineDisables } = input;
3987
+ const { rootDirectory, userConfig, readFileLinesSync, respectInlineDisables, showWarnings } = input;
3879
3988
  const severityControls = buildRuleSeverityControls(userConfig);
3880
3989
  const ignoredRules = new Set(Array.isArray(userConfig?.ignore?.rules) ? userConfig.ignore.rules.filter((rule) => typeof rule === "string") : []);
3881
3990
  const ignoredFilePatterns = compileIgnoredFilePatterns(userConfig);
@@ -3886,6 +3995,15 @@ const buildDiagnosticPipeline = (input) => {
3886
3995
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
3887
3996
  const fileLinesCache = /* @__PURE__ */ new Map();
3888
3997
  const testFileCache = /* @__PURE__ */ new Map();
3998
+ const libraryFileCache = /* @__PURE__ */ new Map();
3999
+ const isLibraryFile = (filePath) => {
4000
+ let cached = libraryFileCache.get(filePath);
4001
+ if (cached === void 0) {
4002
+ cached = classifyPackageRole(resolveCandidateReadPath(rootDirectory, filePath)) === "library";
4003
+ libraryFileCache.set(filePath, cached);
4004
+ }
4005
+ return cached;
4006
+ };
3889
4007
  const getFileLines = (filePath) => {
3890
4008
  const cached = fileLinesCache.get(filePath);
3891
4009
  if (cached !== void 0) return cached;
@@ -3912,6 +4030,10 @@ const buildDiagnosticPipeline = (input) => {
3912
4030
  for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
3913
4031
  return false;
3914
4032
  };
4033
+ const isAppOnlyRule = (ruleIdentifier) => {
4034
+ for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
4035
+ return false;
4036
+ };
3915
4037
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
3916
4038
  if (diagnostic.rule !== "rn-no-raw-text") return false;
3917
4039
  if (diagnostic.line <= 0) return false;
@@ -3925,15 +4047,22 @@ const buildDiagnosticPipeline = (input) => {
3925
4047
  return { apply: (diagnostic) => {
3926
4048
  if (shouldAutoSuppress(diagnostic)) return null;
3927
4049
  let current = diagnostic;
4050
+ let explicitSeverityOverride;
4051
+ let explicitRuleOverride;
3928
4052
  if (severityControls) {
3929
4053
  const { ruleKey, category } = getDiagnosticRuleIdentity(current);
3930
- const override = resolveRuleSeverityOverride({
4054
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
4055
+ explicitSeverityOverride = resolveRuleSeverityOverride({
3931
4056
  ruleKey,
3932
4057
  category
3933
4058
  }, severityControls);
3934
- if (override === "off") return null;
3935
- if (override !== void 0) current = restampSeverity(current, override);
4059
+ if (explicitSeverityOverride === "off") return null;
4060
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
4061
+ }
4062
+ if (explicitRuleOverride === void 0) {
4063
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
3936
4064
  }
4065
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
3937
4066
  if (userConfig) {
3938
4067
  if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
3939
4068
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
@@ -4165,10 +4294,18 @@ const VALID_RULE_SEVERITIES = [
4165
4294
  "warn",
4166
4295
  "off"
4167
4296
  ];
4297
+ const KNOWN_CATEGORY_LABEL = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
4298
+ const isDiagnosticCategoryBucket = (value) => DIAGNOSTIC_CATEGORY_BUCKETS.includes(value);
4299
+ const filterKnownCategories = (fieldName, categories) => categories.filter((category) => {
4300
+ if (isDiagnosticCategoryBucket(category)) return true;
4301
+ warnConfigIssue(`config field "${fieldName}" lists "${category}", which is not a known category (expected one of: ${KNOWN_CATEGORY_LABEL}); ignoring the entry.`);
4302
+ return false;
4303
+ });
4168
4304
  const BOOLEAN_FIELD_NAMES = [
4169
4305
  "lint",
4170
4306
  "deadCode",
4171
4307
  "verbose",
4308
+ "warnings",
4172
4309
  "customRulesOnly",
4173
4310
  "share",
4174
4311
  "noScore",
@@ -4217,13 +4354,15 @@ const validateSurfaceControls = (surface, rawControls) => {
4217
4354
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
4218
4355
  return;
4219
4356
  }
4220
- const validated = {};
4357
+ const validatedSurfaceControls = {};
4221
4358
  for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
4222
4359
  if (rawControls[fieldName] === void 0) continue;
4223
- const result = validateStringArrayField(`surfaces.${surface}.${fieldName}`, rawControls[fieldName]);
4224
- if (result !== void 0) validated[fieldName] = result;
4360
+ const qualifiedName = `surfaces.${surface}.${fieldName}`;
4361
+ const result = validateStringArrayField(qualifiedName, rawControls[fieldName]);
4362
+ if (result === void 0) continue;
4363
+ validatedSurfaceControls[fieldName] = fieldName === "includeCategories" || fieldName === "excludeCategories" ? filterKnownCategories(qualifiedName, result) : result;
4225
4364
  }
4226
- return validated;
4365
+ return validatedSurfaceControls;
4227
4366
  };
4228
4367
  const validateSurfacesField = (rawSurfaces) => {
4229
4368
  if (!isPlainObject$1(rawSurfaces)) {
@@ -4241,7 +4380,7 @@ const validateSurfacesField = (rawSurfaces) => {
4241
4380
  }
4242
4381
  return validated;
4243
4382
  };
4244
- const validateSeverityMap = (fieldName, rawMap) => {
4383
+ const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
4245
4384
  if (!isPlainObject$1(rawMap)) {
4246
4385
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
4247
4386
  return;
@@ -4252,6 +4391,10 @@ const validateSeverityMap = (fieldName, rawMap) => {
4252
4391
  warnConfigIssue(`config field "${fieldName}" has an empty key; ignoring the entry.`);
4253
4392
  continue;
4254
4393
  }
4394
+ if (keysAreCategories && !isDiagnosticCategoryBucket(key)) {
4395
+ warnConfigIssue(`config field "${fieldName}.${key}" is not a known category (expected one of: ${KNOWN_CATEGORY_LABEL}); ignoring the entry.`);
4396
+ continue;
4397
+ }
4255
4398
  if (!isRuleSeverity(value)) {
4256
4399
  warnConfigIssue(`config field "${fieldName}.${key}" must be one of: ${VALID_RULE_SEVERITIES.join(", ")} (got ${formatType(value)}); ignoring the entry.`);
4257
4400
  continue;
@@ -4272,7 +4415,7 @@ const validateConfigTypes = (config) => {
4272
4415
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
4273
4416
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
4274
4417
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
4275
- for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value));
4418
+ for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
4276
4419
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
4277
4420
  return validated;
4278
4421
  };
@@ -4356,11 +4499,12 @@ const resolveConfigRootDir = (config, configSourceDirectory) => {
4356
4499
  }
4357
4500
  return resolvedRootDir;
4358
4501
  };
4359
- const resolveDiagnoseTarget = (directory) => {
4502
+ const resolveDiagnoseTarget = (directory, options = {}) => {
4360
4503
  if (isFile(path.join(directory, "package.json"))) return directory;
4361
4504
  const reactSubprojects = discoverReactSubprojects(directory);
4362
4505
  if (reactSubprojects.length === 0) return null;
4363
4506
  if (reactSubprojects.length === 1) return reactSubprojects[0].directory;
4507
+ if (options.allowAmbiguous === true) return null;
4364
4508
  throw new AmbiguousProjectError(directory, reactSubprojects.map((subproject) => path.relative(directory, subproject.directory)).toSorted());
4365
4509
  };
4366
4510
  /**
@@ -4374,7 +4518,8 @@ const resolveDiagnoseTarget = (directory) => {
4374
4518
  * project root, if configured.
4375
4519
  * 4. Walk into a nested React subproject when the requested
4376
4520
  * directory has no `package.json` of its own (raises
4377
- * `AmbiguousProjectError` when multiple candidates exist).
4521
+ * `AmbiguousProjectError` when multiple candidates exist unless
4522
+ * the caller opts into keeping the wrapper directory).
4378
4523
  *
4379
4524
  * Throws `ProjectNotFoundError` when neither the requested directory
4380
4525
  * nor any discoverable nested project has a `package.json`.
@@ -4386,14 +4531,14 @@ const resolveDiagnoseTarget = (directory) => {
4386
4531
  * via its own cache). Routing through `resolveScanTarget` keeps every
4387
4532
  * shell in agreement on what "the scan directory" means.
4388
4533
  */
4389
- const resolveScanTarget = (requestedDirectory) => {
4534
+ const resolveScanTarget = (requestedDirectory, options = {}) => {
4390
4535
  const absoluteRequested = path.resolve(requestedDirectory);
4391
4536
  const loadedConfig = loadConfigWithSource(absoluteRequested);
4392
4537
  const userConfig = loadedConfig?.config ?? null;
4393
4538
  const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
4394
4539
  const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
4395
4540
  const directoryAfterRedirect = redirectedDirectory ?? absoluteRequested;
4396
- const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect;
4541
+ const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect, options) ?? directoryAfterRedirect;
4397
4542
  if (!isDirectory(resolvedDirectory)) throw existsSync(resolvedDirectory) ? new NotADirectoryError(resolvedDirectory) : new ProjectNotFoundError(resolvedDirectory);
4398
4543
  return {
4399
4544
  resolvedDirectory,
@@ -4572,99 +4717,6 @@ const checkReducedMotion = (rootDirectory) => {
4572
4717
  return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
4573
4718
  };
4574
4719
  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
4720
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
4669
4721
  const FALSY_VALUES = new Set([
4670
4722
  "false",
@@ -4746,6 +4798,30 @@ const collectIgnorePatterns = (rootDirectory) => {
4746
4798
  cachedPatternsByRoot.set(rootDirectory, patterns);
4747
4799
  return patterns;
4748
4800
  };
4801
+ /**
4802
+ * Resolves a path to its canonical, symlink-free form, falling back to
4803
+ * the input when it cannot be realpath'd (broken symlink, permission
4804
+ * error) so a best-effort normalization never throws.
4805
+ *
4806
+ * deslop's dead-code module graph is collected with `fast-glob` (which
4807
+ * keeps the scan root's symlinks intact) while imports are resolved
4808
+ * through `oxc-resolver` (which returns realpath'd targets). When the
4809
+ * project root sits behind a symlink — e.g. macOS iCloud-synced
4810
+ * `~/Documents` / `~/Desktop`, or a symlinked checkout — those two path
4811
+ * spaces diverge: every resolved import misses the graph and the files
4812
+ * they point at (commonly every `@/…` alias target) are mis-reported as
4813
+ * unreachable. Canonicalizing the root before the scan keeps both path
4814
+ * spaces in agreement.
4815
+ */
4816
+ const toCanonicalPath = (filePath) => {
4817
+ try {
4818
+ return fs.realpathSync(filePath);
4819
+ } catch {
4820
+ return filePath;
4821
+ }
4822
+ };
4823
+ const DEAD_CODE_PLUGIN = "deslop";
4824
+ const DEAD_CODE_CATEGORY = "Maintainability";
4749
4825
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4750
4826
  const DEAD_CODE_WORKER_SCRIPT = `
4751
4827
  const inputChunks = [];
@@ -4921,7 +4997,11 @@ const buildDeadCodeWorkerError = (workerError) => {
4921
4997
  return error;
4922
4998
  };
4923
4999
  const createDeadCodeWorker = (input) => {
4924
- const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
5000
+ const child = spawn(process.execPath, [
5001
+ `--max-old-space-size=${DEAD_CODE_WORKER_MAX_OLD_SPACE_MB}`,
5002
+ "-e",
5003
+ DEAD_CODE_WORKER_SCRIPT
5004
+ ], {
4925
5005
  stdio: [
4926
5006
  "pipe",
4927
5007
  "pipe",
@@ -4996,7 +5076,8 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
4996
5076
  });
4997
5077
  });
4998
5078
  const checkDeadCode = async (options) => {
4999
- const { rootDirectory, userConfig } = options;
5079
+ const { userConfig } = options;
5080
+ const rootDirectory = toCanonicalPath(options.rootDirectory);
5000
5081
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
5001
5082
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
5002
5083
  const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
@@ -5009,59 +5090,162 @@ const checkDeadCode = async (options) => {
5009
5090
  const diagnostics = [];
5010
5091
  for (const unusedFile of result.unusedFiles) diagnostics.push({
5011
5092
  filePath: toRelative(unusedFile.path),
5012
- plugin: "deslop",
5093
+ plugin: DEAD_CODE_PLUGIN,
5013
5094
  rule: "unused-file",
5014
5095
  severity: "warning",
5015
5096
  message: "Unused file — not reachable from any entry point",
5016
5097
  help: "Delete the file if it is truly unreachable, or import it from an entry point.",
5017
5098
  line: 0,
5018
5099
  column: 0,
5019
- category: "Dead Code"
5100
+ category: DEAD_CODE_CATEGORY
5020
5101
  });
5021
5102
  for (const unusedExport of result.unusedExports) {
5022
5103
  const label = unusedExport.isTypeOnly ? "type export" : "export";
5023
5104
  diagnostics.push({
5024
5105
  filePath: toRelative(unusedExport.path),
5025
- plugin: "deslop",
5106
+ plugin: DEAD_CODE_PLUGIN,
5026
5107
  rule: unusedExport.isTypeOnly ? "unused-type" : "unused-export",
5027
5108
  severity: "warning",
5028
5109
  message: `Unused ${label}: \`${unusedExport.name}\``,
5029
5110
  help: "Drop the `export` keyword (or remove the declaration) if no other module uses this symbol.",
5030
5111
  line: unusedExport.line,
5031
5112
  column: unusedExport.column,
5032
- category: "Dead Code"
5113
+ category: DEAD_CODE_CATEGORY
5033
5114
  });
5034
5115
  }
5035
5116
  for (const unusedDependency of result.unusedDependencies) {
5036
5117
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
5037
5118
  diagnostics.push({
5038
5119
  filePath: "package.json",
5039
- plugin: "deslop",
5120
+ plugin: DEAD_CODE_PLUGIN,
5040
5121
  rule: unusedDependency.isDevDependency ? "unused-dev-dependency" : "unused-dependency",
5041
5122
  severity: "warning",
5042
5123
  message: `Unused ${label}: \`${unusedDependency.name}\``,
5043
5124
  help: "Remove the dependency from package.json if it is genuinely unused.",
5044
5125
  line: 0,
5045
5126
  column: 0,
5046
- category: "Dead Code"
5127
+ category: DEAD_CODE_CATEGORY
5047
5128
  });
5048
5129
  }
5049
5130
  for (const cycle of result.circularDependencies) {
5050
5131
  if (cycle.files.length === 0) continue;
5051
5132
  diagnostics.push({
5052
5133
  filePath: toRelative(cycle.files[0]),
5053
- plugin: "deslop",
5134
+ plugin: DEAD_CODE_PLUGIN,
5054
5135
  rule: "circular-dependency",
5055
5136
  severity: "warning",
5056
5137
  message: `Circular import cycle: ${cycle.files.map(toRelative).join(" → ")}`,
5057
5138
  help: "Break the cycle by extracting the shared code into a third module that both files import.",
5058
5139
  line: 0,
5059
5140
  column: 0,
5060
- category: "Dead Code"
5141
+ category: DEAD_CODE_CATEGORY
5061
5142
  });
5062
5143
  }
5063
5144
  return diagnostics;
5064
5145
  };
5146
+ const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
5147
+ const isSurfacingOverride = (override) => override === "warn" || override === "error";
5148
+ const deadCodeMaySurfaceWhenWarningsHidden = (userConfig) => {
5149
+ const severityControls = buildRuleSeverityControls(userConfig);
5150
+ if (!severityControls) return false;
5151
+ if (isSurfacingOverride(severityControls.categories?.["Maintainability"])) return true;
5152
+ for (const [ruleKey, override] of Object.entries(severityControls.rules ?? {})) if (ruleKey.startsWith(DEAD_CODE_RULE_KEY_PREFIX) && isSurfacingOverride(override)) return true;
5153
+ return false;
5154
+ };
5155
+ const toStringSet = (values) => {
5156
+ if (!values || values.length === 0) return /* @__PURE__ */ new Set();
5157
+ return new Set(values.filter((value) => typeof value === "string" && value.length > 0));
5158
+ };
5159
+ const buildResolvedControls = (surface, userControls) => {
5160
+ const excludeTags = new Set(DEFAULT_SURFACE_EXCLUDED_TAGS[surface]);
5161
+ const includeTags = toStringSet(userControls?.includeTags);
5162
+ for (const tag of includeTags) excludeTags.delete(tag);
5163
+ for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
5164
+ return {
5165
+ includeTags,
5166
+ excludeTags,
5167
+ includeCategories: toStringSet(userControls?.includeCategories),
5168
+ excludeCategories: toStringSet(userControls?.excludeCategories),
5169
+ includeRuleKeys: toStringSet(userControls?.includeRules),
5170
+ excludeRuleKeys: toStringSet(userControls?.excludeRules)
5171
+ };
5172
+ };
5173
+ const intersects = (values, candidates) => values.some((value) => candidates.has(value));
5174
+ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
5175
+ const resolved = buildResolvedControls(surface, config?.surfaces?.[surface]);
5176
+ const { ruleKey, category, tags } = getDiagnosticRuleIdentity(diagnostic);
5177
+ if (resolved.includeRuleKeys.has(ruleKey)) return true;
5178
+ if (resolved.includeCategories.has(category)) return true;
5179
+ if (intersects(tags, resolved.includeTags)) return true;
5180
+ if (resolved.excludeRuleKeys.has(ruleKey)) return false;
5181
+ if (resolved.excludeCategories.has(category)) return false;
5182
+ if (intersects(tags, resolved.excludeTags)) return false;
5183
+ return true;
5184
+ };
5185
+ const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
5186
+ const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(path.resolve(rootDirectory, relativePath)));
5187
+ const listSourceFilesViaGit = (rootDirectory) => {
5188
+ const result = spawnSync("git", [
5189
+ "ls-files",
5190
+ "-z",
5191
+ "--cached",
5192
+ "--others",
5193
+ "--exclude-standard"
5194
+ ], {
5195
+ cwd: rootDirectory,
5196
+ encoding: "utf-8",
5197
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
5198
+ });
5199
+ if (result.error || result.status !== 0) return null;
5200
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
5201
+ };
5202
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
5203
+ const filePaths = [];
5204
+ const stack = [rootDirectory];
5205
+ while (stack.length > 0) {
5206
+ const currentDirectory = stack.pop();
5207
+ const entries = readDirectoryEntries(currentDirectory);
5208
+ for (const entry of entries) {
5209
+ const absolutePath = path.join(currentDirectory, entry.name);
5210
+ if (entry.isDirectory()) {
5211
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
5212
+ continue;
5213
+ }
5214
+ if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
5215
+ }
5216
+ }
5217
+ return filePaths;
5218
+ };
5219
+ const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
5220
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
5221
+ if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
5222
+ const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
5223
+ return listSourceFiles(rootDirectory).filter((filePath) => {
5224
+ if (!JSX_FILE_PATTERN.test(filePath)) return false;
5225
+ return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
5226
+ });
5227
+ };
5228
+ var Config = class Config extends Context.Service()("react-doctor/Config") {
5229
+ static layerNode = Layer.effect(Config, Effect.gen(function* () {
5230
+ const cache = yield* Cache.make({
5231
+ capacity: 16,
5232
+ timeToLive: CONFIG_CACHE_TTL_MS,
5233
+ lookup: (directory) => Effect.sync(() => {
5234
+ const loaded = loadConfigWithSource(directory);
5235
+ const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
5236
+ return {
5237
+ config: loaded?.config ?? null,
5238
+ resolvedDirectory: redirected ?? directory,
5239
+ configSourceDirectory: loaded?.sourceDirectory ?? null
5240
+ };
5241
+ })
5242
+ });
5243
+ return Config.of({ resolve: Effect.fn("Config.resolve")(function* (directory) {
5244
+ return yield* Cache.get(cache, directory);
5245
+ }) });
5246
+ }));
5247
+ static layerOf = (resolved) => Layer.succeed(Config, Config.of({ resolve: () => Effect.succeed(resolved) }));
5248
+ };
5065
5249
  /**
5066
5250
  * `DeadCode` runs whole-project reachability analysis and streams
5067
5251
  * diagnostics. Reachability is a whole-project property — the
@@ -5567,12 +5751,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
5567
5751
  return null;
5568
5752
  }
5569
5753
  if (grepResult === null) return null;
5570
- return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
5754
+ return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
5571
5755
  };
5572
5756
  const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
5573
5757
  const matches = [];
5574
5758
  const checkFile = (relativePath) => {
5575
- if (!SOURCE_FILE_PATTERN.test(relativePath)) return;
5759
+ if (!isLintableSourceFile(relativePath)) return;
5576
5760
  const absolutePath = path.join(rootDirectory, relativePath);
5577
5761
  let content;
5578
5762
  try {
@@ -5815,10 +5999,14 @@ const resolveSettingsRootDirectory = (rootDirectory) => {
5815
5999
  if (!fs.existsSync(rootDirectory)) return rootDirectory;
5816
6000
  return fs.realpathSync(rootDirectory);
5817
6001
  };
6002
+ const resolveCompilerCleanupBucketSeverity = (ruleKey, severityControls) => {
6003
+ if (!COMPILER_CLEANUP_RULE_KEYS.has(ruleKey)) return void 0;
6004
+ return severityControls?.buckets?.[COMPILER_CLEANUP_BUCKET];
6005
+ };
5818
6006
  const applyRuleSeverityControls = (rules, severityControls) => {
5819
6007
  const enabledRules = {};
5820
6008
  for (const [ruleKey, defaultSeverity] of Object.entries(rules)) {
5821
- const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? defaultSeverity;
6009
+ const severity = resolveRuleSeverityOverride({ ruleKey }, severityControls) ?? resolveCompilerCleanupBucketSeverity(ruleKey, severityControls) ?? defaultSeverity;
5822
6010
  if (severity === "off") continue;
5823
6011
  enabledRules[ruleKey] = severity;
5824
6012
  }
@@ -5860,7 +6048,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
5860
6048
  category: rule.category
5861
6049
  }, severityControls);
5862
6050
  if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
5863
- const severity = explicitSeverity ?? rule.severity;
6051
+ const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
5864
6052
  if (severity === "off") continue;
5865
6053
  enabledReactDoctorRules[registryEntry.key] = severity;
5866
6054
  }
@@ -6032,7 +6220,7 @@ const KNOWN_SECRET_RULES = [
6032
6220
  }
6033
6221
  ];
6034
6222
  const CANDIDATE_TOKEN_PATTERN = /[A-Za-z0-9_][A-Za-z0-9_-]*/g;
6035
- const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
6223
+ const HEX_DIGEST_PATTERN = /^(?:[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/;
6036
6224
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6037
6225
  const HAS_LETTER_PATTERN = /[A-Za-z]/;
6038
6226
  const HAS_DIGIT_PATTERN = /[0-9]/;
@@ -6049,7 +6237,7 @@ const shannonEntropyBits = (value) => {
6049
6237
  const looksLikeHighEntropySecret = (token) => {
6050
6238
  if (token.length < 32) return false;
6051
6239
  if (!HAS_LETTER_PATTERN.test(token) || !HAS_DIGIT_PATTERN.test(token)) return false;
6052
- if (GIT_OBJECT_ID_PATTERN.test(token) || UUID_PATTERN.test(token)) return false;
6240
+ if (HEX_DIGEST_PATTERN.test(token) || UUID_PATTERN.test(token)) return false;
6053
6241
  return shannonEntropyBits(token) >= 3;
6054
6242
  };
6055
6243
  const redactHighEntropyTokens = (text) => text.replace(CANDIDATE_TOKEN_PATTERN, (token) => looksLikeHighEntropySecret(token) ? REDACTED_PLACEHOLDER : token);
@@ -6376,25 +6564,26 @@ const shouldSuppressLocalUseHookDiagnostic = (diagnostic, rootDirectory) => {
6376
6564
  return bindingResolution !== null && !bindingResolution.isReactUseBinding;
6377
6565
  };
6378
6566
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
6379
- const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
6567
+ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
6568
+ 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.";
6380
6569
  const PLUGIN_CATEGORY_MAP = {
6381
- react: "Correctness",
6382
- "react-hooks": "Correctness",
6383
- "react-hooks-js": "React Compiler",
6384
- "react-doctor": "Other",
6570
+ react: "Bugs",
6571
+ "react-hooks": "Bugs",
6572
+ "react-hooks-js": "Performance",
6573
+ "react-doctor": "Bugs",
6385
6574
  "jsx-a11y": "Accessibility",
6386
- effect: "State & Effects",
6387
- eslint: "Correctness",
6388
- oxc: "Correctness",
6389
- typescript: "Correctness",
6390
- unicorn: "Correctness",
6391
- import: "Bundle Size",
6392
- promise: "Correctness",
6393
- n: "Correctness",
6394
- node: "Correctness",
6395
- vitest: "Correctness",
6396
- jest: "Correctness",
6397
- nextjs: "Next.js"
6575
+ effect: "Bugs",
6576
+ eslint: "Bugs",
6577
+ oxc: "Bugs",
6578
+ typescript: "Bugs",
6579
+ unicorn: "Bugs",
6580
+ import: "Performance",
6581
+ promise: "Bugs",
6582
+ n: "Bugs",
6583
+ node: "Bugs",
6584
+ vitest: "Bugs",
6585
+ jest: "Bugs",
6586
+ nextjs: "Bugs"
6398
6587
  };
6399
6588
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
6400
6589
  const getRuleRecommendation = (ruleName, project) => {
@@ -6402,6 +6591,8 @@ const getRuleRecommendation = (ruleName, project) => {
6402
6591
  return reactDoctorPlugin.rules[ruleName]?.recommendation;
6403
6592
  };
6404
6593
  const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
6594
+ const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
6595
+ const resolveDiagnosticTitle = (plugin, rule) => plugin === "react-hooks-js" ? REACT_COMPILER_TITLE : getRuleTitle(rule);
6405
6596
  const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
6406
6597
  const cleaned = resolveCleanedDiagnostic(typeof message === "string" ? message : "", typeof help === "string" ? help : "", plugin, rule, project);
6407
6598
  return {
@@ -6430,7 +6621,7 @@ const parseRuleCode = (code) => {
6430
6621
  rule: match[2]
6431
6622
  };
6432
6623
  };
6433
- const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Other";
6624
+ const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
6434
6625
  const isOxlintOutput = (value) => {
6435
6626
  if (typeof value !== "object" || value === null) return false;
6436
6627
  const candidate = value;
@@ -6454,7 +6645,16 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
6454
6645
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
6455
6646
  }
6456
6647
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
6457
- return parsed.diagnostics.filter((diagnostic) => diagnostic.code && SOURCE_FILE_PATTERN.test(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
6648
+ const minifiedFileCache = /* @__PURE__ */ new Map();
6649
+ const isMinifiedDiagnosticFile = (filename) => {
6650
+ const absolutePath = path.isAbsolute(filename) ? filename : path.resolve(rootDirectory || ".", filename);
6651
+ const cached = minifiedFileCache.get(absolutePath);
6652
+ if (cached !== void 0) return cached;
6653
+ const minified = isMinifiedSource(absolutePath);
6654
+ minifiedFileCache.set(absolutePath, minified);
6655
+ return minified;
6656
+ };
6657
+ return parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory)).map((diagnostic) => {
6458
6658
  const { plugin, rule } = parseRuleCode(diagnostic.code);
6459
6659
  const primaryLabel = diagnostic.labels[0];
6460
6660
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -6463,6 +6663,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
6463
6663
  plugin,
6464
6664
  rule,
6465
6665
  severity: diagnostic.severity,
6666
+ title: resolveDiagnosticTitle(plugin, rule),
6466
6667
  message: cleaned.message,
6467
6668
  help: cleaned.help,
6468
6669
  url: diagnostic.url,
@@ -6880,7 +7081,8 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
6880
7081
  static layerNoop = Layer.succeed(Progress, Progress.of({ start: () => Effect.succeed({
6881
7082
  update: () => Effect.void,
6882
7083
  succeed: () => Effect.void,
6883
- fail: () => Effect.void
7084
+ fail: () => Effect.void,
7085
+ stop: () => Effect.void
6884
7086
  }) }));
6885
7087
  static layerCapture = Layer.effect(Progress, Effect.map(ProgressCapture, (events) => Progress.of({ start: (text) => Effect.gen(function* () {
6886
7088
  yield* Ref.update(events, (existing) => [...existing, {
@@ -6899,6 +7101,10 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
6899
7101
  fail: (displayText) => Ref.update(events, (existing) => [...existing, {
6900
7102
  _tag: "Failed",
6901
7103
  text: displayText
7104
+ }]),
7105
+ stop: () => Ref.update(events, (existing) => [...existing, {
7106
+ _tag: "Stopped",
7107
+ text
6902
7108
  }])
6903
7109
  };
6904
7110
  }) }))).pipe(Layer.provideMerge(ProgressCapture.layer));
@@ -7148,15 +7354,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7148
7354
  repo
7149
7355
  }).pipe(Effect.orElseSucceed(() => null)) : Effect.succeed(null));
7150
7356
  const lintIncludePaths = computeJsxIncludePaths([...input.includePaths]) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
7357
+ const scannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => path.resolve(scanDirectory, relativePath)) : [];
7151
7358
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
7152
7359
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
7153
7360
  yield* beforeLint(project, lintIncludePaths ?? void 0);
7154
7361
  const isDiffMode = input.includePaths.length > 0;
7362
+ const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? false;
7155
7363
  const transform = buildDiagnosticPipeline({
7156
7364
  rootDirectory: scanDirectory,
7157
7365
  userConfig: resolvedConfig.config,
7158
7366
  readFileLinesSync: fileReader(filesService, scanDirectory),
7159
- respectInlineDisables: input.respectInlineDisables
7367
+ respectInlineDisables: input.respectInlineDisables,
7368
+ showWarnings
7160
7369
  });
7161
7370
  const applyPerElementPipeline = (rawStream) => rawStream.pipe(Stream.filterMap(filterMapNullable(transform.apply)), Stream.tap((diagnostic) => reporterService.emit(diagnostic)));
7162
7371
  const environmentDiagnostics = isDiffMode ? [] : [...checkReducedMotion(scanDirectory), ...checkPnpmHardening(scanDirectory)];
@@ -7202,7 +7411,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7202
7411
  const lintFailureState = yield* Ref.get(lintFailure);
7203
7412
  yield* afterLint(lintFailureState.didFail);
7204
7413
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
7205
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
7414
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
7206
7415
  const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
7207
7416
  rootDirectory: scanDirectory,
7208
7417
  userConfig: resolvedConfig.config
@@ -7214,9 +7423,11 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7214
7423
  return Stream.empty;
7215
7424
  }))))))));
7216
7425
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
7217
- const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
7426
+ const scanElapsedMilliseconds = Date.now() - scanStartTime;
7427
+ const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
7218
7428
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
7219
7429
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
7430
+ else if (input.suppressScanSummary) yield* scanProgress.stop();
7220
7431
  else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s`);
7221
7432
  yield* reporterService.finalize;
7222
7433
  const finalDiagnostics = [
@@ -7257,7 +7468,10 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
7257
7468
  lintFailureReasonKind: lintFailureState.reasonKind,
7258
7469
  lintPartialFailures,
7259
7470
  didDeadCodeFail: deadCodeFailureState.didFail,
7260
- deadCodeFailureReason: deadCodeFailureState.reason
7471
+ deadCodeFailureReason: deadCodeFailureState.reason,
7472
+ scannedFileCount: totalFileCount,
7473
+ scannedFilePaths,
7474
+ scanElapsedMilliseconds
7261
7475
  };
7262
7476
  }).pipe(Effect.withSpan("runInspect", { attributes: {
7263
7477
  "inspect.directory": input.directory,
@@ -7383,7 +7597,7 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
7383
7597
  static layerNode = Layer.effect(StagedFiles, Effect.gen(function* () {
7384
7598
  const git = yield* Git;
7385
7599
  return StagedFiles.of({
7386
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter((entry) => SOURCE_FILE_PATTERN.test(entry)))),
7600
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter(isLintableSourceFile))),
7387
7601
  materialize: ({ directory, stagedFiles, tempDirectory }) => Effect.gen(function* () {
7388
7602
  const materializedFiles = [];
7389
7603
  const resolvedTempDirectory = path.resolve(tempDirectory);
@@ -7592,7 +7806,7 @@ const getDiffInfo = (directory, explicitBaseBranch) => Effect.runPromise(Effect.
7592
7806
  GitBaseBranchInvalid: (reason) => Effect.die(new Error(reason.detail)),
7593
7807
  GitBaseBranchMissing: (reason) => Effect.die(new Error(reason.message))
7594
7808
  })));
7595
- const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
7809
+ const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
7596
7810
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
7597
7811
  let p = process || {}, argv = p.argv || [], env = p.env || {};
7598
7812
  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);
@@ -7679,6 +7893,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
7679
7893
  includePaths,
7680
7894
  customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
7681
7895
  respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
7896
+ warnings: options.warnings ?? effectiveConfig?.warnings ?? false,
7682
7897
  adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
7683
7898
  ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
7684
7899
  runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
@@ -7716,6 +7931,7 @@ const clearCaches = () => {
7716
7931
  clearConfigCache();
7717
7932
  clearPackageJsonCache();
7718
7933
  clearIgnorePatternsCache();
7934
+ clearPackageRoleCache();
7719
7935
  clearAutoSuppressionCaches();
7720
7936
  };
7721
7937
  const toJsonReport = (result, options) => buildJsonReport({