react-doctor 0.2.14-dev.5dff3b5 → 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/README.md +2 -0
- package/dist/cli.js +13796 -5739
- package/dist/index.d.ts +53 -10
- package/dist/index.js +411 -156
- package/package.json +5 -2
- package/dist/cli-logger-BGXguXLj.js +0 -7567
- package/dist/rolldown-runtime-uZX_iqCz.js +0 -35
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() &&
|
|
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 &&
|
|
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.
|
|
3866
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
3973
|
+
explicitSeverityOverride = resolveRuleSeverityOverride({
|
|
3931
3974
|
ruleKey,
|
|
3932
3975
|
category
|
|
3933
3976
|
}, severityControls);
|
|
3934
|
-
if (
|
|
3935
|
-
if (
|
|
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
|
|
4272
|
+
const validatedSurfaceControls = {};
|
|
4221
4273
|
for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
|
|
4222
4274
|
if (rawControls[fieldName] === void 0) continue;
|
|
4223
|
-
const
|
|
4224
|
-
|
|
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
|
|
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, [
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 &&
|
|
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 (!
|
|
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
|
|
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: "
|
|
6239
|
-
"react-hooks": "
|
|
6240
|
-
"react-hooks-js": "
|
|
6241
|
-
"react-doctor": "
|
|
6456
|
+
react: "Bugs",
|
|
6457
|
+
"react-hooks": "Bugs",
|
|
6458
|
+
"react-hooks-js": "Performance",
|
|
6459
|
+
"react-doctor": "Bugs",
|
|
6242
6460
|
"jsx-a11y": "Accessibility",
|
|
6243
|
-
effect: "
|
|
6244
|
-
eslint: "
|
|
6245
|
-
oxc: "
|
|
6246
|
-
typescript: "
|
|
6247
|
-
unicorn: "
|
|
6248
|
-
import: "
|
|
6249
|
-
promise: "
|
|
6250
|
-
n: "
|
|
6251
|
-
node: "
|
|
6252
|
-
vitest: "
|
|
6253
|
-
jest: "
|
|
6254
|
-
nextjs: "
|
|
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) ?? "
|
|
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
|
-
|
|
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,
|
|
@@ -6730,7 +6967,8 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
|
|
|
6730
6967
|
static layerNoop = Layer.succeed(Progress, Progress.of({ start: () => Effect.succeed({
|
|
6731
6968
|
update: () => Effect.void,
|
|
6732
6969
|
succeed: () => Effect.void,
|
|
6733
|
-
fail: () => Effect.void
|
|
6970
|
+
fail: () => Effect.void,
|
|
6971
|
+
stop: () => Effect.void
|
|
6734
6972
|
}) }));
|
|
6735
6973
|
static layerCapture = Layer.effect(Progress, Effect.map(ProgressCapture, (events) => Progress.of({ start: (text) => Effect.gen(function* () {
|
|
6736
6974
|
yield* Ref.update(events, (existing) => [...existing, {
|
|
@@ -6749,6 +6987,10 @@ var Progress = class Progress extends Context.Service()("react-doctor/Progress")
|
|
|
6749
6987
|
fail: (displayText) => Ref.update(events, (existing) => [...existing, {
|
|
6750
6988
|
_tag: "Failed",
|
|
6751
6989
|
text: displayText
|
|
6990
|
+
}]),
|
|
6991
|
+
stop: () => Ref.update(events, (existing) => [...existing, {
|
|
6992
|
+
_tag: "Stopped",
|
|
6993
|
+
text
|
|
6752
6994
|
}])
|
|
6753
6995
|
};
|
|
6754
6996
|
}) }))).pipe(Layer.provideMerge(ProgressCapture.layer));
|
|
@@ -6820,17 +7062,21 @@ var Reporter = class Reporter extends Context.Service()("react-doctor/Reporter")
|
|
|
6820
7062
|
});
|
|
6821
7063
|
}));
|
|
6822
7064
|
};
|
|
6823
|
-
const
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
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));
|
|
6834
7080
|
const stripFilePaths = (diagnostics) => diagnostics.map(({ filePath: _filePath, ...rest }) => rest);
|
|
6835
7081
|
const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
6836
7082
|
const describeFailure = (error) => {
|
|
@@ -6994,15 +7240,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6994
7240
|
repo
|
|
6995
7241
|
}).pipe(Effect.orElseSucceed(() => null)) : Effect.succeed(null));
|
|
6996
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)) : [];
|
|
6997
7244
|
const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
|
|
6998
7245
|
const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
|
|
6999
7246
|
yield* beforeLint(project, lintIncludePaths ?? void 0);
|
|
7000
7247
|
const isDiffMode = input.includePaths.length > 0;
|
|
7248
|
+
const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? false;
|
|
7001
7249
|
const transform = buildDiagnosticPipeline({
|
|
7002
7250
|
rootDirectory: scanDirectory,
|
|
7003
7251
|
userConfig: resolvedConfig.config,
|
|
7004
7252
|
readFileLinesSync: fileReader(filesService, scanDirectory),
|
|
7005
|
-
respectInlineDisables: input.respectInlineDisables
|
|
7253
|
+
respectInlineDisables: input.respectInlineDisables,
|
|
7254
|
+
showWarnings
|
|
7006
7255
|
});
|
|
7007
7256
|
const applyPerElementPipeline = (rawStream) => rawStream.pipe(Stream.filterMap(filterMapNullable(transform.apply)), Stream.tap((diagnostic) => reporterService.emit(diagnostic)));
|
|
7008
7257
|
const environmentDiagnostics = isDiffMode ? [] : [...checkReducedMotion(scanDirectory), ...checkPnpmHardening(scanDirectory)];
|
|
@@ -7048,7 +7297,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7048
7297
|
const lintFailureState = yield* Ref.get(lintFailure);
|
|
7049
7298
|
yield* afterLint(lintFailureState.didFail);
|
|
7050
7299
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
7051
|
-
const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
|
|
7300
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
7052
7301
|
const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
7053
7302
|
rootDirectory: scanDirectory,
|
|
7054
7303
|
userConfig: resolvedConfig.config
|
|
@@ -7060,9 +7309,11 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7060
7309
|
return Stream.empty;
|
|
7061
7310
|
}))))))));
|
|
7062
7311
|
const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
|
|
7063
|
-
const
|
|
7312
|
+
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
7313
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
|
|
7064
7314
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
7065
7315
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
7316
|
+
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
7066
7317
|
else yield* scanProgress.succeed(`Scanned ${totalFileCount} ${totalFileCount === 1 ? "file" : "files"} in ${scanElapsedSeconds}s`);
|
|
7067
7318
|
yield* reporterService.finalize;
|
|
7068
7319
|
const finalDiagnostics = [
|
|
@@ -7103,7 +7354,10 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
7103
7354
|
lintFailureReasonKind: lintFailureState.reasonKind,
|
|
7104
7355
|
lintPartialFailures,
|
|
7105
7356
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
7106
|
-
deadCodeFailureReason: deadCodeFailureState.reason
|
|
7357
|
+
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
7358
|
+
scannedFileCount: totalFileCount,
|
|
7359
|
+
scannedFilePaths,
|
|
7360
|
+
scanElapsedMilliseconds
|
|
7107
7361
|
};
|
|
7108
7362
|
}).pipe(Effect.withSpan("runInspect", { attributes: {
|
|
7109
7363
|
"inspect.directory": input.directory,
|
|
@@ -7229,7 +7483,7 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
|
|
|
7229
7483
|
static layerNode = Layer.effect(StagedFiles, Effect.gen(function* () {
|
|
7230
7484
|
const git = yield* Git;
|
|
7231
7485
|
return StagedFiles.of({
|
|
7232
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter(
|
|
7486
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(Effect.map((entries) => entries.filter(isLintableSourceFile))),
|
|
7233
7487
|
materialize: ({ directory, stagedFiles, tempDirectory }) => Effect.gen(function* () {
|
|
7234
7488
|
const materializedFiles = [];
|
|
7235
7489
|
const resolvedTempDirectory = path.resolve(tempDirectory);
|
|
@@ -7438,7 +7692,7 @@ const getDiffInfo = (directory, explicitBaseBranch) => Effect.runPromise(Effect.
|
|
|
7438
7692
|
GitBaseBranchInvalid: (reason) => Effect.die(new Error(reason.detail)),
|
|
7439
7693
|
GitBaseBranchMissing: (reason) => Effect.die(new Error(reason.message))
|
|
7440
7694
|
})));
|
|
7441
|
-
const filterSourceFiles = (filePaths) => filePaths.filter(
|
|
7695
|
+
const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
|
|
7442
7696
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
7443
7697
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
7444
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);
|
|
@@ -7525,6 +7779,7 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7525
7779
|
includePaths,
|
|
7526
7780
|
customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
|
|
7527
7781
|
respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
|
|
7782
|
+
warnings: options.warnings ?? effectiveConfig?.warnings ?? false,
|
|
7528
7783
|
adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
|
|
7529
7784
|
ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
|
|
7530
7785
|
runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
|