Deslop JavaScript code.
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.
npm install deslop-jsThe deslop-cli package provides a command-line interface:
npm install -g deslop-cli# scan the current directory
deslop
# scan a specific project
deslop ./my-project
# use the explicit analyze sub-command (equivalent to the above)
deslop analyze ./my-projectdeslop [root] [options]
# custom entry points
deslop --entry src/main.ts --entry src/worker.ts
# ignore test files
deslop --ignore "**/*.test.ts" --ignore "**/__mocks__/**"
# only scan specific extensions
deslop --extensions .ts .tsx
# resolve path aliases via tsconfig
deslop --tsconfig ./tsconfig.json
# include type-only exports in results
deslop --report-types
# report unused exports from entry files too
deslop --include-entry-exports
# output results as JSON (useful for CI or piping to other tools)
deslop --json
# exit with code 1 when unused code is found (for CI gates)
deslop --fail-on-issues
# exit with code 1 when circular imports are found
deslop --fail-on-cycles# fail the build if there are unused exports or circular imports
deslop ./src --fail-on-issues --fail-on-cycles --ignore "**/*.test.ts"import { analyze, defineConfig } from "deslop-js";
const config = defineConfig({ rootDir: "./my-project" });
const result = await analyze(config);
// unused-code findings (syntactic)
result.unusedFiles; // files unreachable from any entry point
result.unusedExports; // exported symbols never imported
result.unusedDependencies; // package.json deps not imported anywhere
result.circularDependencies; // import cycles
// redundancy / DRY findings (syntactic, on by default)
result.redundantAliases; // `import { x as x }`, useless re-export renames
result.duplicateExports; // same name exported twice from one module
result.duplicateImports; // same specifier imported multiple times
result.redundantTypePatterns; // `T & {}`, `Partial<Partial<T>>`, etc.
result.identityWrappers; // `const wrap = (x) => fn(x)`
result.duplicateTypeDefinitions; // same-shape type declared in N files
result.duplicateInlineTypes; // anonymous `{ a, b, c }` repeated across modules
result.simplifiableFunctions; // `() => { return x }`, `await x; return x`
result.simplifiableExpressions; // `!!x`, `x ? x : y`, `cond ? true : false`
result.duplicateConstants; // same literal value across files
result.crossFileDuplicateExports; // same export name shipped by 2+ files that share an importer
result.reExportCycles; // `export * from "./a"` cycles (self-loop or multi-node)
result.privateTypeLeaks; // exported signature references a non-exported local type
// duplicate-block detection (token-based copy-paste; on by default, disable via `duplicateBlocks.enabled: false`)
result.duplicateBlocks; // suffix-array + LCP detected duplicate code blocks
result.duplicateBlockClusters; // clones grouped by file set + refactoring suggestions
result.shadowedDirectoryPairs; // directory pairs with many identical files
// feature flag inventory (on by default, disable via `featureFlags.enabled: false`)
result.featureFlags; // LaunchDarkly/Statsig/Unleash/PostHog/Vercel Flags/process.env.* uses
// function complexity hotspots (on by default, disable via `complexity.enabled: false`)
result.complexFunctions; // McCabe cyclomatic + SonarSource cognitive per function
// TypeScript-specific smells (on by default)
result.unnecessaryAssertions; // `x as unknown as T`, `x as any`, `x!!`, `<T>x`, `"foo"!`
result.lazyImportsAtTopLevel; // top-level `await import(...)` / `.then(...)` that should be static
result.commonjsInEsm; // `require()`, `module.exports`, `exports.x` inside ESM modules
result.typeScriptEscapeHatches; // `// @ts-ignore`, `// @ts-nocheck`, undocumented `@ts-expect-error`
// semantic findings (type-aware; on by default, disable via `semantic: { enabled: false }`)
result.unusedTypes; // type aliases / interfaces never referenced
result.unusedEnumMembers; // enum members no reference site uses
result.unusedClassMembers; // class members no caller invokes (skips React/Angular lifecycle methods)
result.misclassifiedDependencies; // `dependencies` entries used only as types
// diagnostics
result.analysisErrors; // structured errors from any pipeline stage
result.totalFiles;
result.totalExports;
result.analysisTimeMs;defineConfig accepts a required rootDir and optional overrides:
const config = defineConfig({
rootDir: "./my-project",
entryPatterns: ["src/main.ts"],
ignorePatterns: ["**/*.test.ts"],
tsConfigPath: "./tsconfig.json",
reportTypes: true,
includeEntryExports: true,
reportRedundancy: true,
semantic: { enabled: true },
});| Option | Type | Default | Description |
|---|---|---|---|
rootDir |
string |
required | Project root directory |
entryPatterns |
string[] |
auto-detected | Entry point glob patterns |
ignorePatterns |
string[] |
[] |
Glob patterns to exclude from analysis |
includeExtensions |
string[] |
[".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts"] |
File extensions to scan |
tsConfigPath |
string | undefined |
undefined |
Path to tsconfig.json for path alias resolution |
reportTypes |
boolean |
false |
Include type-only exports in unusedExports |
includeEntryExports |
boolean |
false |
Report unused exports from entry files |
reportRedundancy |
boolean |
true |
Emit the redundancy / DRY findings listed above |
semantic |
SemanticConfig |
undefined |
Opt-in TypeScript type-aware analysis (see below) |
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.
const config = defineConfig({
rootDir: "./my-project",
semantic: {
enabled: true,
reportUnusedTypes: true,
reportUnusedEnumMembers: true,
reportUnusedClassMembers: false, // off by default, noisy on framework code
reportMisclassifiedDependencies: true,
reportRedundantVariableAliases: true,
reportRoundTripAliases: true,
},
});| Option | Default | Notes |
|---|---|---|
enabled |
false |
Master switch; semantic analysis loads the TS program and adds ~1–3s per scan |
reportUnusedTypes |
true |
Type aliases / interfaces / type-only exports never referenced |
reportUnusedEnumMembers |
true |
Enum members no reference site reads or writes |
reportUnusedClassMembers |
false |
Subclass overrides, framework method-by-name invocation (@HttpGet, lifecycle hooks) produce too many stylistic FPs to enable by default. Opt in selectively. |
reportMisclassifiedDependencies |
true |
dependencies packages used only via import type |
reportRedundantVariableAliases |
true |
Local aliases like const X = Y; export { X } |
reportRoundTripAliases |
true |
import { X as Y } from "./a"; export { Y as X } |
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:
const config = defineConfig({
rootDir: "./my-project",
duplicateBlocks: {
enabled: true, // default
mode: "semantic", // "strict" preserves identifiers/literals; "semantic" (default) blinds them
minTokens: 50,
minLines: 5,
minOccurrences: 2,
skipLocal: false, // true => only report cross-directory duplicates
},
});Surfaces in three result fields:
result.duplicateBlocks— every duplicated block group with all its occurrencesresult.duplicateBlockClusters— duplicate blocks sharing the same file set, plus anextract-function/extract-modulerefactoring hintresult.shadowedDirectoryPairs— directory pairs (e.g.src/anddeno/lib/) whose files mirror each other
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:
- Env-var flags —
process.env.Xwhose name starts with one of the built-in prefixesFEATURE_,NEXT_PUBLIC_FEATURE_,REACT_APP_FEATURE_,VITE_FEATURE_,NUXT_PUBLIC_FEATURE_,ENABLE_,FF_,FLAG_,TOGGLE_(extend withextraEnvPrefixes). - 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()fromflagsor@vercel/flags). - Config-object access —
config.features.Xstyle; off by default because it's heuristic. Opt in withdetectConfigObjects: true.
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.
The actionable angle: cross-reference result.featureFlags with your live flag dashboard.
- Flags in the dashboard but missing from
result.featureFlags→ no longer read by the codebase, safe to retire from the platform. - Flags in
result.featureFlagswithguardsDeadCode: true→ the guarded code is unreachable, delete both the flag and its body.
const config = defineConfig({
rootDir: "./my-project",
featureFlags: {
enabled: true, // default
extraEnvPrefixes: ["MYAPP_FF_"],
extraSdkFunctionNames: ["myCustomFlag"],
detectConfigObjects: false, // heuristic config.features.x — opt in if you use that pattern
},
});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:
const config = defineConfig({
rootDir: "./my-project",
complexity: {
enabled: true, // default
cyclomaticThreshold: 10,
cognitiveThreshold: 15,
paramCountThreshold: 5,
functionLineThreshold: 80,
},
});On by default. Four families of TypeScript-specific patterns surfaced at high or medium confidence — no extra config required.
result.unnecessaryAssertions; // type assertions that drop type-safety or do nothing
result.lazyImportsAtTopLevel; // dynamic imports at the module top level
result.commonjsInEsm; // CommonJS forms inside ESM modules
result.typeScriptEscapeHatches; // @ts-ignore / @ts-nocheck / undocumented @ts-expect-error| Finding | Kinds |
|---|---|
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) |
result.lazyImportsAtTopLevel |
top-level-await-import, top-level-then-import |
result.commonjsInEsm |
require, module-exports, exports-assignment |
result.typeScriptEscapeHatches |
ts-ignore, ts-nocheck, ts-expect-error-without-explanation |
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.
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).
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.
pnpm install
pnpm build
pnpm test
pnpm lint
pnpm formatMIT