deslop-js 0.0.25-dev.aeeb44a → 0.5.6

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 ADDED
@@ -0,0 +1,308 @@
1
+ # deslop-js
2
+
3
+ [![version](https://img.shields.io/npm/v/deslop-js?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/deslop-js)
4
+ [![downloads](https://img.shields.io/npm/dt/deslop-js.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/deslop-js)
5
+
6
+ Deslop JavaScript code.
7
+
8
+ Finds unused files, dead exports, dead dependencies, circular imports, redundant aliases, duplicate types, and other DRY violations. Each finding carries a confidence tier so you can gate CI on the high-signal ones and treat the rest as code-review prompts.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install deslop-js
14
+ ```
15
+
16
+ ## CLI
17
+
18
+ The `deslop-cli` package provides a command-line interface:
19
+
20
+ ```bash
21
+ npm install -g deslop-cli
22
+ ```
23
+
24
+ ### Quick start
25
+
26
+ ```bash
27
+ # scan the current directory
28
+ deslop
29
+
30
+ # scan a specific project
31
+ deslop ./my-project
32
+
33
+ # use the explicit analyze sub-command (equivalent to the above)
34
+ deslop analyze ./my-project
35
+ ```
36
+
37
+ ### Options
38
+
39
+ ```bash
40
+ deslop [root] [options]
41
+
42
+ # custom entry points
43
+ deslop --entry src/main.ts --entry src/worker.ts
44
+
45
+ # ignore test files
46
+ deslop --ignore "**/*.test.ts" --ignore "**/__mocks__/**"
47
+
48
+ # only scan specific extensions
49
+ deslop --extensions .ts .tsx
50
+
51
+ # resolve path aliases via tsconfig
52
+ deslop --tsconfig ./tsconfig.json
53
+
54
+ # add explicit path aliases (in addition to the auto-detected ones)
55
+ deslop --paths "@app/*=src/*" --paths "@lib/*=packages/lib/*"
56
+
57
+ # include type-only exports in results
58
+ deslop --report-types
59
+
60
+ # report unused exports from entry files too
61
+ deslop --include-entry-exports
62
+
63
+ # output results as JSON (useful for CI or piping to other tools)
64
+ deslop --json
65
+
66
+ # exit with code 1 when unused code is found (for CI gates)
67
+ deslop --fail-on-issues
68
+
69
+ # exit with code 1 when circular imports are found
70
+ deslop --fail-on-cycles
71
+ ```
72
+
73
+ ### CI example
74
+
75
+ ```bash
76
+ # fail the build if there are unused exports or circular imports
77
+ deslop ./src --fail-on-issues --fail-on-cycles --ignore "**/*.test.ts"
78
+ ```
79
+
80
+ ## Programmatic Usage
81
+
82
+ ```ts
83
+ import { analyze, defineConfig } from "deslop-js";
84
+
85
+ const config = defineConfig({ rootDir: "./my-project" });
86
+ const result = await analyze(config);
87
+
88
+ // unused-code findings (syntactic)
89
+ result.unusedFiles; // files unreachable from any entry point
90
+ result.unusedExports; // exported symbols never imported
91
+ result.unusedDependencies; // package.json deps not imported anywhere
92
+ result.circularDependencies; // import cycles
93
+
94
+ // redundancy / DRY findings (syntactic, on by default)
95
+ result.redundantAliases; // `import { x as x }`, useless re-export renames
96
+ result.duplicateExports; // same name exported twice from one module
97
+ result.duplicateImports; // same specifier imported multiple times
98
+ result.redundantTypePatterns; // `T & {}`, `Partial<Partial<T>>`, etc.
99
+ result.identityWrappers; // `const wrap = (x) => fn(x)`
100
+ result.duplicateTypeDefinitions; // same-shape type declared in N files
101
+ result.duplicateInlineTypes; // anonymous `{ a, b, c }` repeated across modules
102
+ result.simplifiableFunctions; // `() => { return x }`, `await x; return x`
103
+ result.simplifiableExpressions; // `!!x`, `x ? x : y`, `cond ? true : false`
104
+ result.duplicateConstants; // same literal value across files
105
+ result.crossFileDuplicateExports; // same export name shipped by 2+ files that share an importer
106
+ result.reExportCycles; // `export * from "./a"` cycles (self-loop or multi-node)
107
+ result.privateTypeLeaks; // exported signature references a non-exported local type
108
+
109
+ // duplicate-block detection (token-based copy-paste; on by default, disable via `duplicateBlocks.enabled: false`)
110
+ result.duplicateBlocks; // suffix-array + LCP detected duplicate code blocks
111
+ result.duplicateBlockClusters; // clones grouped by file set + refactoring suggestions
112
+ result.shadowedDirectoryPairs; // directory pairs with many identical files
113
+
114
+ // feature flag inventory (on by default, disable via `featureFlags.enabled: false`)
115
+ result.featureFlags; // LaunchDarkly/Statsig/Unleash/PostHog/Vercel Flags/process.env.* uses
116
+
117
+ // function complexity hotspots (on by default, disable via `complexity.enabled: false`)
118
+ result.complexFunctions; // McCabe cyclomatic + SonarSource cognitive per function
119
+
120
+ // TypeScript-specific smells (on by default)
121
+ result.unnecessaryAssertions; // `x as unknown as T`, `x as any`, `x!!`, `<T>x`, `"foo"!`
122
+ result.lazyImportsAtTopLevel; // top-level `await import(...)` / `.then(...)` that should be static
123
+ result.commonjsInEsm; // `require()`, `module.exports`, `exports.x` inside ESM modules
124
+ result.typeScriptEscapeHatches; // `// @ts-ignore`, `// @ts-nocheck`, undocumented `@ts-expect-error`
125
+
126
+ // semantic findings (type-aware; on by default, disable via `semantic: { enabled: false }`)
127
+ result.unusedTypes; // type aliases / interfaces never referenced
128
+ result.unusedEnumMembers; // enum members no reference site uses
129
+ result.unusedClassMembers; // class members no caller invokes (skips React/Angular lifecycle methods)
130
+ result.misclassifiedDependencies; // `dependencies` entries used only as types
131
+
132
+ // diagnostics
133
+ result.analysisErrors; // structured errors from any pipeline stage
134
+ result.totalFiles;
135
+ result.totalExports;
136
+ result.analysisTimeMs;
137
+ ```
138
+
139
+ ## Programmatic Options
140
+
141
+ `defineConfig` accepts a required `rootDir` and optional overrides:
142
+
143
+ ```ts
144
+ const config = defineConfig({
145
+ rootDir: "./my-project",
146
+ entryPatterns: ["src/main.ts"],
147
+ ignorePatterns: ["**/*.test.ts"],
148
+ tsConfigPath: "./tsconfig.json",
149
+ reportTypes: true,
150
+ includeEntryExports: true,
151
+ reportRedundancy: true,
152
+ semantic: { enabled: true },
153
+ });
154
+ ```
155
+
156
+ | Option | Type | Default | Description |
157
+ | --------------------- | --------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
158
+ | `rootDir` | `string` | required | Project root directory |
159
+ | `entryPatterns` | `string[]` | auto-detected | Entry point glob patterns |
160
+ | `ignorePatterns` | `string[]` | `[]` | Glob patterns to exclude from analysis |
161
+ | `includeExtensions` | `string[]` | `[".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts"]` | File extensions to scan |
162
+ | `tsConfigPath` | `string \| undefined` | `undefined` | Path to tsconfig.json for path alias resolution |
163
+ | `paths` | `Record<string, string[]> \| undefined` | `undefined` | Explicit path-alias mappings (e.g. `{ "@app/*": ["src/*"] }`), resolved alongside auto-detected aliases |
164
+ | `reportTypes` | `boolean` | `false` | Include type-only exports in `unusedExports` |
165
+ | `includeEntryExports` | `boolean` | `false` | Report unused exports from entry files |
166
+ | `reportRedundancy` | `boolean` | `true` | Emit the redundancy / DRY findings listed above |
167
+ | `semantic` | `SemanticConfig` | `undefined` | Opt-in TypeScript type-aware analysis (see below) |
168
+
169
+ Path aliases are auto-detected by default — from `tsconfig` `paths`, Vite (`resolve.alias`), webpack, Babel (`module-resolver`), and Jest (`moduleNameMapper`) configs, plus the workspace layout (a `@scope/<dir>` import resolves to the matching workspace package even when its `package.json` name differs). Use `paths` / `--paths` only for mappings none of those cover.
170
+
171
+ ### Semantic (type-aware) analysis
172
+
173
+ On by default. Loads the TypeScript program when the project has a valid `tsconfig.json`; gracefully no-ops on JS-only projects. Disable with `semantic: { enabled: false }` to skip the ~1–3s program load.
174
+
175
+ ```ts
176
+ const config = defineConfig({
177
+ rootDir: "./my-project",
178
+ semantic: {
179
+ enabled: true,
180
+ reportUnusedTypes: true,
181
+ reportUnusedEnumMembers: true,
182
+ reportUnusedClassMembers: false, // off by default, noisy on framework code
183
+ reportMisclassifiedDependencies: true,
184
+ reportRedundantVariableAliases: true,
185
+ reportRoundTripAliases: true,
186
+ },
187
+ });
188
+ ```
189
+
190
+ | Option | Default | Notes |
191
+ | --------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
192
+ | `enabled` | `false` | Master switch; semantic analysis loads the TS program and adds ~1–3s per scan |
193
+ | `reportUnusedTypes` | `true` | Type aliases / interfaces / type-only exports never referenced |
194
+ | `reportUnusedEnumMembers` | `true` | Enum members no reference site reads or writes |
195
+ | `reportUnusedClassMembers` | **`false`** | Subclass overrides, framework method-by-name invocation (`@HttpGet`, lifecycle hooks) produce too many stylistic FPs to enable by default. Opt in selectively. |
196
+ | `reportMisclassifiedDependencies` | `true` | `dependencies` packages used only via `import type` |
197
+ | `reportRedundantVariableAliases` | `true` | Local aliases like `const X = Y; export { X }` |
198
+ | `reportRoundTripAliases` | `true` | `import { X as Y } from "./a"; export { Y as X }` |
199
+
200
+ ### Duplicate blocks (token-based copy-paste detection)
201
+
202
+ On by default. Detects maximal duplicated token sequences across files via a suffix array + LCP pass over a normalized AST token stream. Tune thresholds or disable entirely:
203
+
204
+ ```ts
205
+ const config = defineConfig({
206
+ rootDir: "./my-project",
207
+ duplicateBlocks: {
208
+ enabled: true, // default
209
+ mode: "semantic", // "strict" preserves identifiers/literals; "semantic" (default) blinds them
210
+ minTokens: 50,
211
+ minLines: 5,
212
+ minOccurrences: 2,
213
+ skipLocal: false, // true => only report cross-directory duplicates
214
+ },
215
+ });
216
+ ```
217
+
218
+ Surfaces in three result fields:
219
+
220
+ - `result.duplicateBlocks` — every duplicated block group with all its occurrences
221
+ - `result.duplicateBlockClusters` — duplicate blocks sharing the same file set, plus an `extract-function` / `extract-module` refactoring hint
222
+ - `result.shadowedDirectoryPairs` — directory pairs (e.g. `src/` and `deno/lib/`) whose files mirror each other
223
+
224
+ ### Feature flag inventory
225
+
226
+ On by default. Scans the codebase for every place a feature flag is _read_ and produces a finding per use. The detector recognizes three families:
227
+
228
+ 1. **Env-var flags** — `process.env.X` whose name starts with one of the built-in prefixes `FEATURE_`, `NEXT_PUBLIC_FEATURE_`, `REACT_APP_FEATURE_`, `VITE_FEATURE_`, `NUXT_PUBLIC_FEATURE_`, `ENABLE_`, `FF_`, `FLAG_`, `TOGGLE_` (extend with `extraEnvPrefixes`).
229
+ 2. **SDK calls** with provider attribution — LaunchDarkly (`useFlag`/`variation`/...), Statsig (`useGate`/`checkGate`/...), Unleash (`isEnabled`/`getVariant`), GrowthBook (`isOn`/`isOff`/`getFeatureValue`), Split (`getTreatment`), PostHog (`useFeatureFlagEnabled`/...), ConfigCat, Flagsmith, Optimizely, Eppo, and Vercel Flags (`flag()` / `evaluate()` from `flags` or `@vercel/flags`).
230
+ 3. **Config-object access** — `config.features.X` style; off by default because it's heuristic. Opt in with `detectConfigObjects: true`.
231
+
232
+ Each finding carries `name`, `path`, `line`, `column`, `sdkProvider` (when known), and `kind: "env-var" | "sdk-call" | "config-object"`. The detector also tracks the surrounding `if` / ternary guard span and sets `guardsDeadCode: true` when an `unusedExports` finding falls inside that guard — so a flag whose enabled branch contains only dead code lights up immediately.
233
+
234
+ **The actionable angle**: cross-reference `result.featureFlags` with your live flag dashboard.
235
+
236
+ - Flags in the dashboard but missing from `result.featureFlags` → no longer read by the codebase, safe to retire from the platform.
237
+ - Flags in `result.featureFlags` with `guardsDeadCode: true` → the guarded code is unreachable, delete both the flag and its body.
238
+
239
+ ```ts
240
+ const config = defineConfig({
241
+ rootDir: "./my-project",
242
+ featureFlags: {
243
+ enabled: true, // default
244
+ extraEnvPrefixes: ["MYAPP_FF_"],
245
+ extraSdkFunctionNames: ["myCustomFlag"],
246
+ detectConfigObjects: false, // heuristic config.features.x — opt in if you use that pattern
247
+ },
248
+ });
249
+ ```
250
+
251
+ ### Function complexity (cyclomatic + cognitive)
252
+
253
+ On by default. Reports per-function McCabe cyclomatic and SonarSource cognitive complexity, function size, and parameter count for every function that breaches at least one threshold. Tune the thresholds or disable entirely:
254
+
255
+ ```ts
256
+ const config = defineConfig({
257
+ rootDir: "./my-project",
258
+ complexity: {
259
+ enabled: true, // default
260
+ cyclomaticThreshold: 10,
261
+ cognitiveThreshold: 15,
262
+ paramCountThreshold: 5,
263
+ functionLineThreshold: 80,
264
+ },
265
+ });
266
+ ```
267
+
268
+ ### TypeScript code smells
269
+
270
+ On by default. Four families of TypeScript-specific patterns surfaced at high or medium confidence — no extra config required.
271
+
272
+ ```ts
273
+ result.unnecessaryAssertions; // type assertions that drop type-safety or do nothing
274
+ result.lazyImportsAtTopLevel; // dynamic imports at the module top level
275
+ result.commonjsInEsm; // CommonJS forms inside ESM modules
276
+ result.typeScriptEscapeHatches; // @ts-ignore / @ts-nocheck / undocumented @ts-expect-error
277
+ ```
278
+
279
+ | Finding | Kinds |
280
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
281
+ | `result.unnecessaryAssertions` | `redundant-double-assertion` (`x as unknown as T`), `assertion-to-any`, `redundant-non-null-on-literal` (`"foo"!`), `double-non-null` (`x!!`), `angle-bracket-assertion` (`<T>x`) |
282
+ | `result.lazyImportsAtTopLevel` | `top-level-await-import`, `top-level-then-import` |
283
+ | `result.commonjsInEsm` | `require`, `module-exports`, `exports-assignment` |
284
+ | `result.typeScriptEscapeHatches` | `ts-ignore`, `ts-nocheck`, `ts-expect-error-without-explanation` |
285
+
286
+ ESM detection follows the runtime rules: `.mts`/`.mjs` extensions are always ESM, `.cts`/`.cjs` are always CommonJS, and other files inherit from the nearest `package.json`'s `"type"` field.
287
+
288
+ ## Findings have confidence tiers
289
+
290
+ Every redundancy / semantic finding carries `confidence: "high" | "medium" | "low"`. Use `"high"` for CI gates; `"medium"` and `"low"` are best treated as code-review prompts since intent is sometimes unknowable from syntax alone (e.g. `?? null` may be required by a typed callback signature).
291
+
292
+ ## Error handling
293
+
294
+ `analyze()` never throws on a corrupted file, unparseable `tsconfig`, or missing dependency. Failures surface as `analysisErrors: DeslopError[]` with structured `code`, `module`, `severity`, and `path` fields. See `errors.ts` for the full taxonomy. Errors at `severity: "info"` (empty files, binary files, minified bundles skipped from redundancy analysis) are informational and do not indicate problems.
295
+
296
+ ## Development
297
+
298
+ ```bash
299
+ pnpm install
300
+ pnpm build
301
+ pnpm test
302
+ pnpm lint
303
+ pnpm format
304
+ ```
305
+
306
+ ## License
307
+
308
+ MIT
package/dist/index.cjs CHANGED
@@ -96,8 +96,8 @@ const DEFAULT_EXCLUSIONS = [
96
96
  "**/*.min.mjs",
97
97
  "**/mockServiceWorker.js"
98
98
  ];
99
- const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(?:(?:-[\w-]+(?:[=\s][\w./@=-]+)?\s+)|(?:[\w/-]+\s+))*([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:\s|$)/;
100
- const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(?:(?:-[\w-]+(?:[=\s][\w./@=-]+)?\s+))*((?:[./]|[\w@][\w@-]*\/)[\w./@-]+)(?:\s|$)/;
99
+ const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:\s|$)/;
100
+ const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?((?:[./]|[\w@][\w@-]*\/)[\w./@-]+)(?:\s|$)/;
101
101
  const SCRIPT_CONFIG_FILE_PATTERN = /--config\s+([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))/;
102
102
  const SCRIPT_ENTRY_PATTERNS = [];
103
103
  const DEFAULT_ENTRY_GLOBS = [
@@ -268,13 +268,6 @@ const PLATFORM_SUFFIXES = [
268
268
  ".server",
269
269
  ".client"
270
270
  ];
271
- const REACT_NATIVE_PLATFORM_SUFFIXES = [
272
- ".web",
273
- ".react-native",
274
- ".native",
275
- ".ios",
276
- ".android"
277
- ];
278
271
  const REACT_NATIVE_PLATFORM_EXTENSIONS = [
279
272
  ".web.ts",
280
273
  ".web.tsx",
@@ -1589,7 +1582,7 @@ const extractMdxImportsExports = (sourceText) => {
1589
1582
  return statements.join("\n");
1590
1583
  };
1591
1584
  const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
1592
- const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
1585
+ const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script\b[^>]*>/gi;
1593
1586
  const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
1594
1587
  const extractAstroSources = (sourceText) => {
1595
1588
  const sections = [];
@@ -1608,7 +1601,7 @@ const extractAstroSources = (sourceText) => {
1608
1601
  }
1609
1602
  return sections.join("\n");
1610
1603
  };
1611
- const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script>/gi;
1604
+ const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script\b[^>]*>/gi;
1612
1605
  const extractVueScriptContent = (sourceText) => {
1613
1606
  const scriptBlocks = [];
1614
1607
  let scriptMatch;
@@ -1616,7 +1609,7 @@ const extractVueScriptContent = (sourceText) => {
1616
1609
  while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
1617
1610
  return scriptBlocks.join("\n");
1618
1611
  };
1619
- const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1612
+ const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script\b[^>]*>/gi;
1620
1613
  const extractSvelteScriptContent = (sourceText) => {
1621
1614
  const scriptBlocks = [];
1622
1615
  let scriptMatch;
@@ -3853,7 +3846,7 @@ const matchCompiledMapping = (specifier, mappings) => {
3853
3846
  matchedWildcard = specifier.slice(mapping.prefix.length, specifier.length - mapping.suffix.length);
3854
3847
  } else if (specifier !== mapping.prefix) continue;
3855
3848
  for (const target of mapping.targets) {
3856
- const resolved = resolveAliasTarget(target.replace("*", matchedWildcard));
3849
+ const resolved = resolveAliasTarget(target.replaceAll("*", matchedWildcard));
3857
3850
  if (resolved) return resolved;
3858
3851
  }
3859
3852
  }
@@ -3903,11 +3896,11 @@ const BABEL_CONFIG_GLOBS = [
3903
3896
  ];
3904
3897
  const JEST_CONFIG_GLOBS = ["jest.config.{js,ts,mjs,cjs,json}", "**/jest.config.{js,ts,mjs,cjs,json}"];
3905
3898
  const ALIAS_BLOCK_PATTERN = /alias\s*:\s*\{([\s\S]*?)\}/g;
3906
- const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s*,?\s*)+)\)|fileURLToPath\(\s*new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*\)|["']([^"']+)["'])/g;
3899
+ const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)|fileURLToPath\(\s*new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*\)|["']([^"']+)["'])/g;
3907
3900
  const JEST_MODULE_NAME_MAPPER_BLOCK_PATTERN = /moduleNameMapper\s*:\s*\{([\s\S]*?)\}/g;
3908
3901
  const JEST_MODULE_NAME_MAPPER_ENTRY_PATTERN = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
3909
3902
  const WEBPACK_MODULES_BLOCK_PATTERN = /modules\s*:\s*\[([\s\S]*?)\]/g;
3910
- const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s*,?\s*)+)\)/g;
3903
+ const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)/g;
3911
3904
  const STRING_LITERAL_PATTERN$1 = /["']([^"']+)["']/g;
3912
3905
  const TSCONFIG_FILENAMES = [
3913
3906
  "tsconfig.json",
@@ -5512,7 +5505,7 @@ const extractBundlerConfigEntryPoints = (directory) => {
5512
5505
  };
5513
5506
  const WEBPACK_ENTRY_BLOCK_PATTERN = /entry\s*:\s*(?:\{[^}]*\}|\[[^\]]*\]|['"][^'"]+['"]|path\.(?:join|resolve)\([^)]*\))/gs;
5514
5507
  const WEBPACK_ENTRY_FILE_PATTERN = /['"]([^'"]+)['"]/g;
5515
- const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"]\s*,?\s*)+)\)/g;
5508
+ const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"][\s,]*)+)\)/g;
5516
5509
  const REQUIRE_RESOLVE_PATTERN = /require\.resolve\(\s*['"]([^'"]+)['"]\s*\)/g;
5517
5510
  const extractWebpackEntryPoints = (directory) => {
5518
5511
  const entries = [];
@@ -7231,10 +7224,13 @@ const discoverToolingEntryPoints = (rootDir, workspacePackages) => {
7231
7224
 
7232
7225
  //#endregion
7233
7226
  //#region src/utils/resolve-available-concurrency.ts
7227
+ const clampParseConcurrency = (value) => Math.max(1, Math.min(Math.floor(value), 16));
7234
7228
  const resolveAvailableConcurrency = () => {
7229
+ const requestedConcurrency = Number(process.env["DESLOP_PARSE_CONCURRENCY"]);
7230
+ if (Number.isFinite(requestedConcurrency) && requestedConcurrency >= 1) return clampParseConcurrency(requestedConcurrency);
7235
7231
  const available = node_os.default.availableParallelism();
7236
7232
  if (!Number.isFinite(available) || available < 1) return 1;
7237
- return Math.max(1, Math.min(Math.floor(available), 16));
7233
+ return clampParseConcurrency(available);
7238
7234
  };
7239
7235
 
7240
7236
  //#endregion
@@ -7778,21 +7774,10 @@ const hasReachableDirectImporter = (targetModuleIndex, graph) => {
7778
7774
  return false;
7779
7775
  };
7780
7776
 
7781
- //#endregion
7782
- //#region src/utils/platform-stripped-base-path.ts
7783
- const platformStrippedBasePath = (modulePath, suffixes = PLATFORM_SUFFIXES) => {
7784
- const extensionIndex = modulePath.lastIndexOf(".");
7785
- if (extensionIndex === -1) return modulePath;
7786
- const withoutExtension = modulePath.slice(0, extensionIndex);
7787
- for (const suffix of suffixes) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
7788
- return modulePath;
7789
- };
7790
-
7791
7777
  //#endregion
7792
7778
  //#region src/report/exports.ts
7793
7779
  const detectDeadExports = (graph, config) => {
7794
7780
  const usageMap = buildUsageMap(graph);
7795
- const platformSiblingUsageKeys = graph.hasReactNative ? buildPlatformSiblingUsageKeys(usageMap) : void 0;
7796
7781
  const unusedExports = [];
7797
7782
  for (const module of graph.modules) {
7798
7783
  if (!module.isReachable) continue;
@@ -7807,7 +7792,6 @@ const detectDeadExports = (graph, config) => {
7807
7792
  if (!config.reportTypes && exportInfo.isTypeOnly) continue;
7808
7793
  const usageKey = `${module.fileId.path}::${exportInfo.name}`;
7809
7794
  if (usageMap.has(usageKey)) continue;
7810
- if (platformSiblingUsageKeys?.has(`${platformStrippedBasePath(module.fileId.path, REACT_NATIVE_PLATFORM_SUFFIXES)}::${exportInfo.name}`)) continue;
7811
7795
  if (module.localIdentifierReferences.includes(exportInfo.name)) continue;
7812
7796
  if (!exportInfo.isDefault && defaultExportLinkedNames.has(exportInfo.name)) continue;
7813
7797
  unusedExports.push({
@@ -7821,18 +7805,6 @@ const detectDeadExports = (graph, config) => {
7821
7805
  }
7822
7806
  return unusedExports;
7823
7807
  };
7824
- const buildPlatformSiblingUsageKeys = (usageMap) => {
7825
- const baseKeys = /* @__PURE__ */ new Set();
7826
- for (const usageKey of usageMap) {
7827
- const separatorIndex = usageKey.lastIndexOf("::");
7828
- if (separatorIndex === -1) continue;
7829
- const filePath = usageKey.slice(0, separatorIndex);
7830
- const exportName = usageKey.slice(separatorIndex + 2);
7831
- const basePath = platformStrippedBasePath(filePath, REACT_NATIVE_PLATFORM_SUFFIXES);
7832
- baseKeys.add(`${basePath}::${exportName}`);
7833
- }
7834
- return baseKeys;
7835
- };
7836
7808
  const buildUsageMap = (graph) => {
7837
7809
  const usedExportKeys = /* @__PURE__ */ new Set();
7838
7810
  const sourceToTargetMap = buildSourceToTargetsMap(graph);
@@ -8909,7 +8881,19 @@ const detectCycles = (graph) => {
8909
8881
 
8910
8882
  //#endregion
8911
8883
  //#region src/report/redundancy.ts
8912
- const isPlatformSpecificModulePath = (modulePath) => platformStrippedBasePath(modulePath) !== modulePath;
8884
+ const isPlatformSpecificModulePath = (modulePath) => {
8885
+ const extensionIndex = modulePath.lastIndexOf(".");
8886
+ if (extensionIndex === -1) return false;
8887
+ const withoutExtension = modulePath.slice(0, extensionIndex);
8888
+ return PLATFORM_SUFFIXES.some((suffix) => withoutExtension.endsWith(suffix));
8889
+ };
8890
+ const platformStrippedBasePath = (modulePath) => {
8891
+ const extensionIndex = modulePath.lastIndexOf(".");
8892
+ if (extensionIndex === -1) return modulePath;
8893
+ const withoutExtension = modulePath.slice(0, extensionIndex);
8894
+ for (const suffix of PLATFORM_SUFFIXES) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
8895
+ return modulePath;
8896
+ };
8913
8897
  const buildPlatformSiblingGroupSizes = (graph) => {
8914
8898
  const baseToCount = /* @__PURE__ */ new Map();
8915
8899
  for (const module of graph.modules) {
@@ -12659,21 +12643,23 @@ const generateReport = (graph, config) => {
12659
12643
  const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
12660
12644
  const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
12661
12645
  const crossFileDuplicateExports = config.reportRedundancy ? safeReportDetector("detectCrossFileDuplicateExports", () => detectCrossFileDuplicateExports(graph), [], errorSink) : [];
12662
- const duplicateBlockResult = safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), {
12646
+ const emptyDuplicateBlockResult = {
12663
12647
  duplicateBlocks: [],
12664
12648
  duplicateBlockClusters: [],
12665
12649
  shadowedDirectoryPairs: []
12666
- }, errorSink);
12667
- const reExportCycles = safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink);
12668
- const featureFlags = safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink);
12669
- const complexFunctions = safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink);
12670
- const privateTypeLeaks = safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink);
12671
- const typeScriptSmellsResult = safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), {
12650
+ };
12651
+ const duplicateBlockResult = config.reportCodeQuality ? safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), emptyDuplicateBlockResult, errorSink) : emptyDuplicateBlockResult;
12652
+ const reExportCycles = config.reportCodeQuality ? safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink) : [];
12653
+ const featureFlags = config.reportCodeQuality ? safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink) : [];
12654
+ const complexFunctions = config.reportCodeQuality ? safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink) : [];
12655
+ const privateTypeLeaks = config.reportCodeQuality ? safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink) : [];
12656
+ const emptyTypeScriptSmellsResult = {
12672
12657
  unnecessaryAssertions: [],
12673
12658
  lazyImportsAtTopLevel: [],
12674
12659
  commonjsInEsm: [],
12675
12660
  typeScriptEscapeHatches: []
12676
- }, errorSink);
12661
+ };
12662
+ const typeScriptSmellsResult = config.reportCodeQuality ? safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), emptyTypeScriptSmellsResult, errorSink) : emptyTypeScriptSmellsResult;
12677
12663
  let semanticResult;
12678
12664
  try {
12679
12665
  semanticResult = runSemanticAnalysis(graph, config);
@@ -12935,6 +12921,7 @@ const defineConfig = (options) => ({
12935
12921
  reportTypes: options.reportTypes ?? false,
12936
12922
  includeEntryExports: options.includeEntryExports ?? false,
12937
12923
  reportRedundancy: options.reportRedundancy ?? true,
12924
+ reportCodeQuality: options.reportCodeQuality ?? true,
12938
12925
  semantic: fillSemanticConfig(options.semantic),
12939
12926
  duplicateBlocks: fillDuplicateBlocksConfig(options.duplicateBlocks),
12940
12927
  featureFlags: fillFeatureFlagsConfig(options.featureFlags),
@@ -13243,7 +13230,6 @@ const analyze = async (config) => {
13243
13230
  }));
13244
13231
  return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
13245
13232
  }
13246
- moduleGraph.hasReactNative = hasReactNative;
13247
13233
  try {
13248
13234
  resolveReExportChains(moduleGraph);
13249
13235
  } catch (reExportError) {
package/dist/index.d.cts CHANGED
@@ -421,6 +421,16 @@ interface DeslopConfig {
421
421
  reportTypes: boolean;
422
422
  includeEntryExports: boolean;
423
423
  reportRedundancy: boolean;
424
+ /**
425
+ * Run the non-dead-code "code quality" detectors — duplicate-block (copy-paste)
426
+ * detection, complexity hotspots, feature flags, TypeScript smells,
427
+ * private-type leaks, and re-export cycles. On by default. These are by far
428
+ * the most expensive detectors (duplicate-block detection alone can dominate
429
+ * a large-repo scan), and they're independent of the dead-code graph findings
430
+ * (unused files/exports/dependencies, circular dependencies) — so a consumer
431
+ * that only wants dead-code can set this `false` to skip the bulk of the work.
432
+ */
433
+ reportCodeQuality: boolean;
424
434
  semantic: SemanticConfig | undefined;
425
435
  duplicateBlocks: DuplicateBlocksConfig | undefined;
426
436
  featureFlags: FeatureFlagsConfig | undefined;
package/dist/index.d.mts CHANGED
@@ -421,6 +421,16 @@ interface DeslopConfig {
421
421
  reportTypes: boolean;
422
422
  includeEntryExports: boolean;
423
423
  reportRedundancy: boolean;
424
+ /**
425
+ * Run the non-dead-code "code quality" detectors — duplicate-block (copy-paste)
426
+ * detection, complexity hotspots, feature flags, TypeScript smells,
427
+ * private-type leaks, and re-export cycles. On by default. These are by far
428
+ * the most expensive detectors (duplicate-block detection alone can dominate
429
+ * a large-repo scan), and they're independent of the dead-code graph findings
430
+ * (unused files/exports/dependencies, circular dependencies) — so a consumer
431
+ * that only wants dead-code can set this `false` to skip the bulk of the work.
432
+ */
433
+ reportCodeQuality: boolean;
424
434
  semantic: SemanticConfig | undefined;
425
435
  duplicateBlocks: DuplicateBlocksConfig | undefined;
426
436
  featureFlags: FeatureFlagsConfig | undefined;
package/dist/index.mjs CHANGED
@@ -64,8 +64,8 @@ const DEFAULT_EXCLUSIONS = [
64
64
  "**/*.min.mjs",
65
65
  "**/mockServiceWorker.js"
66
66
  ];
67
- const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(?:(?:-[\w-]+(?:[=\s][\w./@=-]+)?\s+)|(?:[\w/-]+\s+))*([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:\s|$)/;
68
- const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(?:(?:-[\w-]+(?:[=\s][\w./@=-]+)?\s+))*((?:[./]|[\w@][\w@-]*\/)[\w./@-]+)(?:\s|$)/;
67
+ const SCRIPT_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|tsc|npx|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:\s|$)/;
68
+ const SCRIPT_EXTENSIONLESS_FILE_PATTERN = /(?:^|\s)(?:node|tsx|ts-node|bun|esr|esno|jiti|babel-node|zx)\s+(?:\S+\s+)*?((?:[./]|[\w@][\w@-]*\/)[\w./@-]+)(?:\s|$)/;
69
69
  const SCRIPT_CONFIG_FILE_PATTERN = /--config\s+([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))/;
70
70
  const SCRIPT_ENTRY_PATTERNS = [];
71
71
  const DEFAULT_ENTRY_GLOBS = [
@@ -236,13 +236,6 @@ const PLATFORM_SUFFIXES = [
236
236
  ".server",
237
237
  ".client"
238
238
  ];
239
- const REACT_NATIVE_PLATFORM_SUFFIXES = [
240
- ".web",
241
- ".react-native",
242
- ".native",
243
- ".ios",
244
- ".android"
245
- ];
246
239
  const REACT_NATIVE_PLATFORM_EXTENSIONS = [
247
240
  ".web.ts",
248
241
  ".web.tsx",
@@ -1557,7 +1550,7 @@ const extractMdxImportsExports = (sourceText) => {
1557
1550
  return statements.join("\n");
1558
1551
  };
1559
1552
  const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
1560
- const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
1553
+ const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script\b[^>]*>/gi;
1561
1554
  const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
1562
1555
  const extractAstroSources = (sourceText) => {
1563
1556
  const sections = [];
@@ -1576,7 +1569,7 @@ const extractAstroSources = (sourceText) => {
1576
1569
  }
1577
1570
  return sections.join("\n");
1578
1571
  };
1579
- const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script>/gi;
1572
+ const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script\b[^>]*>/gi;
1580
1573
  const extractVueScriptContent = (sourceText) => {
1581
1574
  const scriptBlocks = [];
1582
1575
  let scriptMatch;
@@ -1584,7 +1577,7 @@ const extractVueScriptContent = (sourceText) => {
1584
1577
  while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
1585
1578
  return scriptBlocks.join("\n");
1586
1579
  };
1587
- const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1580
+ const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script\b[^>]*>/gi;
1588
1581
  const extractSvelteScriptContent = (sourceText) => {
1589
1582
  const scriptBlocks = [];
1590
1583
  let scriptMatch;
@@ -3821,7 +3814,7 @@ const matchCompiledMapping = (specifier, mappings) => {
3821
3814
  matchedWildcard = specifier.slice(mapping.prefix.length, specifier.length - mapping.suffix.length);
3822
3815
  } else if (specifier !== mapping.prefix) continue;
3823
3816
  for (const target of mapping.targets) {
3824
- const resolved = resolveAliasTarget(target.replace("*", matchedWildcard));
3817
+ const resolved = resolveAliasTarget(target.replaceAll("*", matchedWildcard));
3825
3818
  if (resolved) return resolved;
3826
3819
  }
3827
3820
  }
@@ -3871,11 +3864,11 @@ const BABEL_CONFIG_GLOBS = [
3871
3864
  ];
3872
3865
  const JEST_CONFIG_GLOBS = ["jest.config.{js,ts,mjs,cjs,json}", "**/jest.config.{js,ts,mjs,cjs,json}"];
3873
3866
  const ALIAS_BLOCK_PATTERN = /alias\s*:\s*\{([\s\S]*?)\}/g;
3874
- const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s*,?\s*)+)\)|fileURLToPath\(\s*new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*\)|["']([^"']+)["'])/g;
3867
+ const ALIAS_ENTRY_PATTERN = /["']?([@\w$./-]+)["']?\s*:\s*(?:path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)|fileURLToPath\(\s*new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*\)|["']([^"']+)["'])/g;
3875
3868
  const JEST_MODULE_NAME_MAPPER_BLOCK_PATTERN = /moduleNameMapper\s*:\s*\{([\s\S]*?)\}/g;
3876
3869
  const JEST_MODULE_NAME_MAPPER_ENTRY_PATTERN = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
3877
3870
  const WEBPACK_MODULES_BLOCK_PATTERN = /modules\s*:\s*\[([\s\S]*?)\]/g;
3878
- const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["']\s*,?\s*)+)\)/g;
3871
+ const WEBPACK_PATH_CALL_PATTERN = /path\.(?:resolve|join)\(\s*__dirname\s*,\s*((?:["'][^"']+["'][\s,]*)+)\)/g;
3879
3872
  const STRING_LITERAL_PATTERN$1 = /["']([^"']+)["']/g;
3880
3873
  const TSCONFIG_FILENAMES = [
3881
3874
  "tsconfig.json",
@@ -5480,7 +5473,7 @@ const extractBundlerConfigEntryPoints = (directory) => {
5480
5473
  };
5481
5474
  const WEBPACK_ENTRY_BLOCK_PATTERN = /entry\s*:\s*(?:\{[^}]*\}|\[[^\]]*\]|['"][^'"]+['"]|path\.(?:join|resolve)\([^)]*\))/gs;
5482
5475
  const WEBPACK_ENTRY_FILE_PATTERN = /['"]([^'"]+)['"]/g;
5483
- const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"]\s*,?\s*)+)\)/g;
5476
+ const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"][\s,]*)+)\)/g;
5484
5477
  const REQUIRE_RESOLVE_PATTERN = /require\.resolve\(\s*['"]([^'"]+)['"]\s*\)/g;
5485
5478
  const extractWebpackEntryPoints = (directory) => {
5486
5479
  const entries = [];
@@ -7199,10 +7192,13 @@ const discoverToolingEntryPoints = (rootDir, workspacePackages) => {
7199
7192
 
7200
7193
  //#endregion
7201
7194
  //#region src/utils/resolve-available-concurrency.ts
7195
+ const clampParseConcurrency = (value) => Math.max(1, Math.min(Math.floor(value), 16));
7202
7196
  const resolveAvailableConcurrency = () => {
7197
+ const requestedConcurrency = Number(process.env["DESLOP_PARSE_CONCURRENCY"]);
7198
+ if (Number.isFinite(requestedConcurrency) && requestedConcurrency >= 1) return clampParseConcurrency(requestedConcurrency);
7203
7199
  const available = os.availableParallelism();
7204
7200
  if (!Number.isFinite(available) || available < 1) return 1;
7205
- return Math.max(1, Math.min(Math.floor(available), 16));
7201
+ return clampParseConcurrency(available);
7206
7202
  };
7207
7203
 
7208
7204
  //#endregion
@@ -7746,21 +7742,10 @@ const hasReachableDirectImporter = (targetModuleIndex, graph) => {
7746
7742
  return false;
7747
7743
  };
7748
7744
 
7749
- //#endregion
7750
- //#region src/utils/platform-stripped-base-path.ts
7751
- const platformStrippedBasePath = (modulePath, suffixes = PLATFORM_SUFFIXES) => {
7752
- const extensionIndex = modulePath.lastIndexOf(".");
7753
- if (extensionIndex === -1) return modulePath;
7754
- const withoutExtension = modulePath.slice(0, extensionIndex);
7755
- for (const suffix of suffixes) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
7756
- return modulePath;
7757
- };
7758
-
7759
7745
  //#endregion
7760
7746
  //#region src/report/exports.ts
7761
7747
  const detectDeadExports = (graph, config) => {
7762
7748
  const usageMap = buildUsageMap(graph);
7763
- const platformSiblingUsageKeys = graph.hasReactNative ? buildPlatformSiblingUsageKeys(usageMap) : void 0;
7764
7749
  const unusedExports = [];
7765
7750
  for (const module of graph.modules) {
7766
7751
  if (!module.isReachable) continue;
@@ -7775,7 +7760,6 @@ const detectDeadExports = (graph, config) => {
7775
7760
  if (!config.reportTypes && exportInfo.isTypeOnly) continue;
7776
7761
  const usageKey = `${module.fileId.path}::${exportInfo.name}`;
7777
7762
  if (usageMap.has(usageKey)) continue;
7778
- if (platformSiblingUsageKeys?.has(`${platformStrippedBasePath(module.fileId.path, REACT_NATIVE_PLATFORM_SUFFIXES)}::${exportInfo.name}`)) continue;
7779
7763
  if (module.localIdentifierReferences.includes(exportInfo.name)) continue;
7780
7764
  if (!exportInfo.isDefault && defaultExportLinkedNames.has(exportInfo.name)) continue;
7781
7765
  unusedExports.push({
@@ -7789,18 +7773,6 @@ const detectDeadExports = (graph, config) => {
7789
7773
  }
7790
7774
  return unusedExports;
7791
7775
  };
7792
- const buildPlatformSiblingUsageKeys = (usageMap) => {
7793
- const baseKeys = /* @__PURE__ */ new Set();
7794
- for (const usageKey of usageMap) {
7795
- const separatorIndex = usageKey.lastIndexOf("::");
7796
- if (separatorIndex === -1) continue;
7797
- const filePath = usageKey.slice(0, separatorIndex);
7798
- const exportName = usageKey.slice(separatorIndex + 2);
7799
- const basePath = platformStrippedBasePath(filePath, REACT_NATIVE_PLATFORM_SUFFIXES);
7800
- baseKeys.add(`${basePath}::${exportName}`);
7801
- }
7802
- return baseKeys;
7803
- };
7804
7776
  const buildUsageMap = (graph) => {
7805
7777
  const usedExportKeys = /* @__PURE__ */ new Set();
7806
7778
  const sourceToTargetMap = buildSourceToTargetsMap(graph);
@@ -8877,7 +8849,19 @@ const detectCycles = (graph) => {
8877
8849
 
8878
8850
  //#endregion
8879
8851
  //#region src/report/redundancy.ts
8880
- const isPlatformSpecificModulePath = (modulePath) => platformStrippedBasePath(modulePath) !== modulePath;
8852
+ const isPlatformSpecificModulePath = (modulePath) => {
8853
+ const extensionIndex = modulePath.lastIndexOf(".");
8854
+ if (extensionIndex === -1) return false;
8855
+ const withoutExtension = modulePath.slice(0, extensionIndex);
8856
+ return PLATFORM_SUFFIXES.some((suffix) => withoutExtension.endsWith(suffix));
8857
+ };
8858
+ const platformStrippedBasePath = (modulePath) => {
8859
+ const extensionIndex = modulePath.lastIndexOf(".");
8860
+ if (extensionIndex === -1) return modulePath;
8861
+ const withoutExtension = modulePath.slice(0, extensionIndex);
8862
+ for (const suffix of PLATFORM_SUFFIXES) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
8863
+ return modulePath;
8864
+ };
8881
8865
  const buildPlatformSiblingGroupSizes = (graph) => {
8882
8866
  const baseToCount = /* @__PURE__ */ new Map();
8883
8867
  for (const module of graph.modules) {
@@ -12627,21 +12611,23 @@ const generateReport = (graph, config) => {
12627
12611
  const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
12628
12612
  const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
12629
12613
  const crossFileDuplicateExports = config.reportRedundancy ? safeReportDetector("detectCrossFileDuplicateExports", () => detectCrossFileDuplicateExports(graph), [], errorSink) : [];
12630
- const duplicateBlockResult = safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), {
12614
+ const emptyDuplicateBlockResult = {
12631
12615
  duplicateBlocks: [],
12632
12616
  duplicateBlockClusters: [],
12633
12617
  shadowedDirectoryPairs: []
12634
- }, errorSink);
12635
- const reExportCycles = safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink);
12636
- const featureFlags = safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink);
12637
- const complexFunctions = safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink);
12638
- const privateTypeLeaks = safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink);
12639
- const typeScriptSmellsResult = safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), {
12618
+ };
12619
+ const duplicateBlockResult = config.reportCodeQuality ? safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), emptyDuplicateBlockResult, errorSink) : emptyDuplicateBlockResult;
12620
+ const reExportCycles = config.reportCodeQuality ? safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink) : [];
12621
+ const featureFlags = config.reportCodeQuality ? safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink) : [];
12622
+ const complexFunctions = config.reportCodeQuality ? safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink) : [];
12623
+ const privateTypeLeaks = config.reportCodeQuality ? safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink) : [];
12624
+ const emptyTypeScriptSmellsResult = {
12640
12625
  unnecessaryAssertions: [],
12641
12626
  lazyImportsAtTopLevel: [],
12642
12627
  commonjsInEsm: [],
12643
12628
  typeScriptEscapeHatches: []
12644
- }, errorSink);
12629
+ };
12630
+ const typeScriptSmellsResult = config.reportCodeQuality ? safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), emptyTypeScriptSmellsResult, errorSink) : emptyTypeScriptSmellsResult;
12645
12631
  let semanticResult;
12646
12632
  try {
12647
12633
  semanticResult = runSemanticAnalysis(graph, config);
@@ -12903,6 +12889,7 @@ const defineConfig = (options) => ({
12903
12889
  reportTypes: options.reportTypes ?? false,
12904
12890
  includeEntryExports: options.includeEntryExports ?? false,
12905
12891
  reportRedundancy: options.reportRedundancy ?? true,
12892
+ reportCodeQuality: options.reportCodeQuality ?? true,
12906
12893
  semantic: fillSemanticConfig(options.semantic),
12907
12894
  duplicateBlocks: fillDuplicateBlocksConfig(options.duplicateBlocks),
12908
12895
  featureFlags: fillFeatureFlagsConfig(options.featureFlags),
@@ -13211,7 +13198,6 @@ const analyze = async (config) => {
13211
13198
  }));
13212
13199
  return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
13213
13200
  }
13214
- moduleGraph.hasReactNative = hasReactNative;
13215
13201
  try {
13216
13202
  resolveReExportChains(moduleGraph);
13217
13203
  } catch (reExportError) {
@@ -1234,7 +1234,7 @@ const extractMdxImportsExports = (sourceText) => {
1234
1234
  return statements.join("\n");
1235
1235
  };
1236
1236
  const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
1237
- const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
1237
+ const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script\b[^>]*>/gi;
1238
1238
  const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
1239
1239
  const extractAstroSources = (sourceText) => {
1240
1240
  const sections = [];
@@ -1253,7 +1253,7 @@ const extractAstroSources = (sourceText) => {
1253
1253
  }
1254
1254
  return sections.join("\n");
1255
1255
  };
1256
- const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script>/gi;
1256
+ const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script\b[^>]*>/gi;
1257
1257
  const extractVueScriptContent = (sourceText) => {
1258
1258
  const scriptBlocks = [];
1259
1259
  let scriptMatch;
@@ -1261,7 +1261,7 @@ const extractVueScriptContent = (sourceText) => {
1261
1261
  while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
1262
1262
  return scriptBlocks.join("\n");
1263
1263
  };
1264
- const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1264
+ const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script\b[^>]*>/gi;
1265
1265
  const extractSvelteScriptContent = (sourceText) => {
1266
1266
  const scriptBlocks = [];
1267
1267
  let scriptMatch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deslop-js",
3
- "version": "0.0.25-dev.aeeb44a",
3
+ "version": "0.5.6",
4
4
  "description": "Deslop JavaScript code",
5
5
  "keywords": [
6
6
  "dead-code",
@@ -12,9 +12,9 @@
12
12
  "typescript",
13
13
  "unused"
14
14
  ],
15
- "homepage": "https://github.com/millionco/deslop-js#readme",
15
+ "homepage": "https://github.com/millionco/react-doctor#readme",
16
16
  "bugs": {
17
- "url": "https://github.com/millionco/deslop-js/issues"
17
+ "url": "https://github.com/millionco/react-doctor/issues"
18
18
  },
19
19
  "license": "MIT",
20
20
  "author": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "https://github.com/millionco/deslop-js.git",
26
+ "url": "https://github.com/millionco/react-doctor.git",
27
27
  "directory": "packages/deslop-js"
28
28
  },
29
29
  "files": [