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/cli.js +13917 -5762
- package/dist/index.d.ts +69 -10
- package/dist/index.js +372 -156
- package/package.json +7 -4
- package/dist/cli-logger-BgVL1vBI.js +0 -7722
- 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,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
|
|
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.
|
|
3866
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
4054
|
+
explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
|
|
4055
|
+
explicitSeverityOverride = resolveRuleSeverityOverride({
|
|
3931
4056
|
ruleKey,
|
|
3932
4057
|
category
|
|
3933
4058
|
}, severityControls);
|
|
3934
|
-
if (
|
|
3935
|
-
if (
|
|
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
|
|
4357
|
+
const validatedSurfaceControls = {};
|
|
4221
4358
|
for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
|
|
4222
4359
|
if (rawControls[fieldName] === void 0) continue;
|
|
4223
|
-
const
|
|
4224
|
-
|
|
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
|
|
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, [
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 &&
|
|
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 (!
|
|
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
|
|
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 (
|
|
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
|
|
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: "
|
|
6382
|
-
"react-hooks": "
|
|
6383
|
-
"react-hooks-js": "
|
|
6384
|
-
"react-doctor": "
|
|
6570
|
+
react: "Bugs",
|
|
6571
|
+
"react-hooks": "Bugs",
|
|
6572
|
+
"react-hooks-js": "Performance",
|
|
6573
|
+
"react-doctor": "Bugs",
|
|
6385
6574
|
"jsx-a11y": "Accessibility",
|
|
6386
|
-
effect: "
|
|
6387
|
-
eslint: "
|
|
6388
|
-
oxc: "
|
|
6389
|
-
typescript: "
|
|
6390
|
-
unicorn: "
|
|
6391
|
-
import: "
|
|
6392
|
-
promise: "
|
|
6393
|
-
n: "
|
|
6394
|
-
node: "
|
|
6395
|
-
vitest: "
|
|
6396
|
-
jest: "
|
|
6397
|
-
nextjs: "
|
|
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) ?? "
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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({
|