deslop-cli 0.0.10 → 0.0.12

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.
Files changed (3) hide show
  1. package/README.md +42 -8
  2. package/dist/cli.mjs +113 -2
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # deslop-cli
2
2
 
3
- CLI for [deslop-js](https://github.com/aidenybai/deslop-js) — find unused files, exports, dependencies, and circular imports.
3
+ Deslop JavaScript code.
4
+
5
+ CLI for [deslop-js](https://github.com/aidenybai/deslop-js). Finds unused files, dead exports, dead dependencies, circular imports, redundant aliases, duplicate types, and other DRY violations.
4
6
 
5
7
  ## Install
6
8
 
@@ -25,13 +27,13 @@ Analyze the current directory:
25
27
  deslop
26
28
  ```
27
29
 
28
- Output JSON:
30
+ Output JSON for programmatic consumption:
29
31
 
30
32
  ```bash
31
33
  deslop ./my-app --json
32
34
  ```
33
35
 
34
- Fail CI when unused code is found (files, exports, or dependencies not circular imports):
36
+ Fail CI when unused code is found (files, exports, or dependencies, not circular imports):
35
37
 
36
38
  ```bash
37
39
  deslop ./my-app --fail-on-issues
@@ -43,6 +45,30 @@ Fail CI when circular imports are found:
43
45
  deslop ./my-app --fail-on-cycles
44
46
  ```
45
47
 
48
+ ## What `deslop` reports
49
+
50
+ The default scan emits the following finding categories (each grouped in human output, fully detailed in `--json`):
51
+
52
+ | Category | What it catches |
53
+ | ------------------------- | -------------------------------------------------------------------------------- |
54
+ | `unusedFiles` | Files unreachable from any entry point |
55
+ | `unusedExports` | Exported symbols never imported anywhere |
56
+ | `unusedDependencies` | `package.json` deps not imported |
57
+ | `circularDependencies` | Import cycles |
58
+ | `redundantAliases` | `import { x as x }`, useless re-export renames |
59
+ | `duplicateExports` | Same name exported twice from one module |
60
+ | `duplicateImports` | Same specifier imported on multiple lines (merge them) |
61
+ | `redundantTypePatterns` | `T & {}`, `Partial<Partial<T>>`, `Pick<T, keyof T>`, empty `extends` |
62
+ | `identityWrappers` | `const wrap = (x) => fn(x)`, calls without transforming |
63
+ | `duplicateTypeDefinitions`| Same structural type declared in multiple files |
64
+ | `duplicateInlineTypes` | Anonymous `{ a, b, c }` shapes repeated across modules |
65
+ | `simplifiableFunctions` | `(x) => { return f(x) }`, `await x; return x;`, useless `async` |
66
+ | `simplifiableExpressions` | `!!x`, `x ? x : y`, `cond ? true : false`, `x !== null && x !== undefined` |
67
+ | `duplicateConstants` | Same literal value used in N files under different names |
68
+ | `analysisErrors` | Structured info / warning / error notes (parse failures, skipped files, etc.) |
69
+
70
+ Type-aware findings (`unusedTypes`, `unusedClassMembers`, `misclassifiedDependencies`, etc.) require enabling the semantic layer programmatically. See the [`deslop-js` README](https://github.com/aidenybai/deslop-js#semantic-type-aware-analysis). They are not exposed via CLI flags yet.
71
+
46
72
  ### Options
47
73
 
48
74
  | Option | Description |
@@ -50,7 +76,7 @@ deslop ./my-app --fail-on-cycles
50
76
  | `[root]` | Project root directory (default: `.`; must exist) |
51
77
  | `-e, --entry <pattern>` | Entry point glob patterns |
52
78
  | `-i, --ignore <pattern>` | Glob patterns to exclude |
53
- | `--extensions <ext>` | File extensions to scan (e.g. `.ts` `.vue`) |
79
+ | `--extensions <ext>` | File extensions to scan (e.g. `.ts` `.vue`) |
54
80
  | `--tsconfig <path>` | Path to tsconfig.json for alias resolution |
55
81
  | `--report-types` | Include type-only exports in results |
56
82
  | `--include-entry-exports` | Report unused exports from entry files |
@@ -60,8 +86,16 @@ deslop ./my-app --fail-on-cycles
60
86
 
61
87
  ### Exit codes
62
88
 
63
- | Code | Meaning |
64
- | ---- | -------------------------------------------- |
65
- | `0` | Success (no failure flags triggered) |
89
+ | Code | Meaning |
90
+ | ---- | ------------------------------------------------------- |
91
+ | `0` | Success (no failure flags triggered) |
66
92
  | `1` | Issues found (per `--fail-on-*` flags) or runtime error |
67
- | `2` | Invalid project root |
93
+ | `2` | Invalid project root |
94
+
95
+ ### Confidence tiers
96
+
97
+ Every redundancy finding carries a confidence tier (`high` / `medium` / `low`) visible in human and JSON output. Use `high` for CI gates; `medium` and `low` are best as code-review prompts. Some patterns flagged at `medium` (`x ?? null`, single-name `duplicateConstants` across packages) have legitimate intent ripgrep alone can't disambiguate.
98
+
99
+ ### Skipped files
100
+
101
+ Files identified as empty, binary, or minified bundles are skipped with an `info`-severity `analysisErrors` note. This isn't an error. It means the file looked machine-generated or non-source and was excluded from analysis to avoid producing irrelevant findings.
package/dist/cli.mjs CHANGED
@@ -42,10 +42,121 @@ const formatHumanReadableResult = (result) => {
42
42
  for (const circularDependency of result.circularDependencies) lines.push(` ${circularDependency.files.join(" → ")}`);
43
43
  lines.push("");
44
44
  }
45
- if (result.unusedFiles.length + result.unusedExports.length + result.unusedDependencies.length + result.circularDependencies.length === 0) lines.push("No unused files, exports, dependencies, or circular imports found.");
45
+ if (result.unusedTypes.length > 0) {
46
+ const typeLabel = result.unusedTypes.length === 1 ? "type" : "types";
47
+ lines.push(`${result.unusedTypes.length} unused ${typeLabel}`);
48
+ for (const unusedType of result.unusedTypes) lines.push(` ${unusedType.path}:${unusedType.line} ${unusedType.name} (${unusedType.kind}, ${unusedType.confidence})`);
49
+ lines.push("");
50
+ }
51
+ if (result.unusedEnumMembers.length > 0) {
52
+ const memberLabel = result.unusedEnumMembers.length === 1 ? "enum member" : "enum members";
53
+ lines.push(`${result.unusedEnumMembers.length} unused ${memberLabel}`);
54
+ for (const unusedMember of result.unusedEnumMembers) lines.push(` ${unusedMember.path}:${unusedMember.line} ${unusedMember.enumName}.${unusedMember.memberName} (${unusedMember.confidence})`);
55
+ lines.push("");
56
+ }
57
+ if (result.unusedClassMembers.length > 0) {
58
+ const classLabel = result.unusedClassMembers.length === 1 ? "class member" : "class members";
59
+ lines.push(`${result.unusedClassMembers.length} unused ${classLabel}`);
60
+ for (const unusedMember of result.unusedClassMembers) lines.push(` ${unusedMember.path}:${unusedMember.line} ${unusedMember.className}.${unusedMember.memberName} (${unusedMember.memberKind}, ${unusedMember.confidence})`);
61
+ lines.push("");
62
+ }
63
+ if (result.misclassifiedDependencies.length > 0) {
64
+ const depLabel = result.misclassifiedDependencies.length === 1 ? "dependency" : "dependencies";
65
+ lines.push(`${result.misclassifiedDependencies.length} misclassified ${depLabel}`);
66
+ for (const finding of result.misclassifiedDependencies) lines.push(` ${finding.name} ${finding.declaredAs} → ${finding.suggestedAs} (${finding.confidence})`);
67
+ lines.push("");
68
+ }
69
+ if (result.redundantAliases.length > 0) {
70
+ const aliasLabel = result.redundantAliases.length === 1 ? "alias" : "aliases";
71
+ lines.push(`${result.redundantAliases.length} redundant ${aliasLabel}`);
72
+ for (const finding of result.redundantAliases) lines.push(` ${finding.path}:${finding.line} [${finding.kind}] ${finding.name}`);
73
+ lines.push("");
74
+ }
75
+ if (result.duplicateExports.length > 0) {
76
+ const exportLabel = result.duplicateExports.length === 1 ? "export" : "exports";
77
+ lines.push(`${result.duplicateExports.length} duplicate ${exportLabel}`);
78
+ for (const finding of result.duplicateExports) lines.push(` ${finding.path} ${finding.name} (${finding.occurrences.length}x)`);
79
+ lines.push("");
80
+ }
81
+ if (result.duplicateImports.length > 0) {
82
+ const importLabel = result.duplicateImports.length === 1 ? "import" : "imports";
83
+ lines.push(`${result.duplicateImports.length} duplicate ${importLabel}`);
84
+ for (const finding of result.duplicateImports) lines.push(` ${finding.path} ${finding.specifier} (${finding.occurrences.length}x)`);
85
+ lines.push("");
86
+ }
87
+ if (result.redundantTypePatterns.length > 0) {
88
+ const patternLabel = result.redundantTypePatterns.length === 1 ? "type pattern" : "type patterns";
89
+ lines.push(`${result.redundantTypePatterns.length} redundant ${patternLabel}`);
90
+ for (const finding of result.redundantTypePatterns) lines.push(` ${finding.path}:${finding.line} ${finding.typeName} [${finding.kind}] → ${finding.suggestion}`);
91
+ lines.push("");
92
+ }
93
+ if (result.identityWrappers.length > 0) {
94
+ const wrapperLabel = result.identityWrappers.length === 1 ? "wrapper" : "wrappers";
95
+ lines.push(`${result.identityWrappers.length} identity ${wrapperLabel}`);
96
+ for (const finding of result.identityWrappers) lines.push(` ${finding.path}:${finding.line} ${finding.wrapperName} → ${finding.wrappedExpression}`);
97
+ lines.push("");
98
+ }
99
+ if (result.duplicateTypeDefinitions.length > 0) {
100
+ const defLabel = result.duplicateTypeDefinitions.length === 1 ? "type definition group" : "type definition groups";
101
+ lines.push(`${result.duplicateTypeDefinitions.length} duplicate ${defLabel}`);
102
+ for (const finding of result.duplicateTypeDefinitions) {
103
+ const instanceLabels = finding.instances.map((instance) => `${instance.typeName}@${instance.path}:${instance.line}`).join(", ");
104
+ lines.push(` [${finding.confidence}] ${instanceLabels}`);
105
+ }
106
+ lines.push("");
107
+ }
108
+ if (result.analysisErrors.length > 0) {
109
+ const fatalCount = result.analysisErrors.filter((error) => error.severity === "fatal").length;
110
+ const warningCount = result.analysisErrors.filter((error) => error.severity === "warning").length;
111
+ const infoCount = result.analysisErrors.filter((error) => error.severity === "info").length;
112
+ const counts = [
113
+ fatalCount > 0 ? `${fatalCount} fatal` : null,
114
+ warningCount > 0 ? `${warningCount} warning` : null,
115
+ infoCount > 0 ? `${infoCount} info` : null
116
+ ].filter((entry) => entry !== null).join(", ");
117
+ lines.push(`${result.analysisErrors.length} analysis ${result.analysisErrors.length === 1 ? "error" : "errors"} (${counts})`);
118
+ for (const error of result.analysisErrors.slice(0, 20)) {
119
+ const location = error.path ? ` ${error.path}` : "";
120
+ lines.push(` [${error.severity}/${error.module}/${error.code}]${location} ${error.message}`);
121
+ }
122
+ if (result.analysisErrors.length > 20) lines.push(` … and ${result.analysisErrors.length - 20} more`);
123
+ lines.push("");
124
+ }
125
+ if (result.duplicateInlineTypes.length > 0) {
126
+ const inlineLabel = result.duplicateInlineTypes.length === 1 ? "inline type group" : "inline type groups";
127
+ lines.push(`${result.duplicateInlineTypes.length} duplicate ${inlineLabel}`);
128
+ for (const finding of result.duplicateInlineTypes) {
129
+ lines.push(` [${finding.confidence}] ${finding.preview} (${finding.occurrences.length} sites)`);
130
+ for (const occurrence of finding.occurrences) lines.push(` ${occurrence.path}:${occurrence.line} ${occurrence.context}${occurrence.nearestName ? ` ${occurrence.nearestName}` : ""}`);
131
+ }
132
+ lines.push("");
133
+ }
134
+ if (result.simplifiableFunctions.length > 0) {
135
+ const fnLabel = result.simplifiableFunctions.length === 1 ? "function" : "functions";
136
+ lines.push(`${result.simplifiableFunctions.length} simplifiable ${fnLabel}`);
137
+ for (const finding of result.simplifiableFunctions) lines.push(` ${finding.path}:${finding.line} [${finding.kind}, ${finding.confidence}] ${finding.functionName ?? "?"} → ${finding.suggestion}`);
138
+ lines.push("");
139
+ }
140
+ if (result.simplifiableExpressions.length > 0) {
141
+ const exprLabel = result.simplifiableExpressions.length === 1 ? "expression" : "expressions";
142
+ lines.push(`${result.simplifiableExpressions.length} simplifiable ${exprLabel}`);
143
+ for (const finding of result.simplifiableExpressions) lines.push(` ${finding.path}:${finding.line} [${finding.kind}, ${finding.confidence}] ${finding.snippet} → ${finding.suggestion}`);
144
+ lines.push("");
145
+ }
146
+ if (result.duplicateConstants.length > 0) {
147
+ const constLabel = result.duplicateConstants.length === 1 ? "constant group" : "constant groups";
148
+ lines.push(`${result.duplicateConstants.length} duplicate ${constLabel}`);
149
+ for (const finding of result.duplicateConstants) {
150
+ lines.push(` [${finding.confidence}] ${finding.literalPreview} (${finding.occurrences.length} copies)`);
151
+ for (const occurrence of finding.occurrences.slice(0, 3)) lines.push(` ${occurrence.path}:${occurrence.line} const ${occurrence.constantName}`);
152
+ if (finding.occurrences.length > 3) lines.push(` … and ${finding.occurrences.length - 3} more`);
153
+ }
154
+ lines.push("");
155
+ }
156
+ if (result.unusedFiles.length + result.unusedExports.length + result.unusedDependencies.length + result.circularDependencies.length + result.unusedTypes.length + result.unusedEnumMembers.length + result.unusedClassMembers.length + result.misclassifiedDependencies.length + result.redundantAliases.length + result.duplicateExports.length + result.duplicateImports.length + result.redundantTypePatterns.length + result.identityWrappers.length + result.duplicateTypeDefinitions.length + result.duplicateInlineTypes.length + result.simplifiableFunctions.length + result.simplifiableExpressions.length + result.duplicateConstants.length === 0) lines.push("No unused files, exports, dependencies, or circular imports found.");
46
157
  return lines.join("\n").trimEnd() + "\n";
47
158
  };
48
- const hasUnusedIssues = (result) => result.unusedFiles.length > 0 || result.unusedExports.length > 0 || result.unusedDependencies.length > 0;
159
+ const hasUnusedIssues = (result) => result.unusedFiles.length > 0 || result.unusedExports.length > 0 || result.unusedDependencies.length > 0 || result.unusedTypes.length > 0 || result.unusedEnumMembers.length > 0 || result.unusedClassMembers.length > 0 || result.misclassifiedDependencies.length > 0 || result.redundantAliases.length > 0 || result.duplicateExports.length > 0 || result.duplicateImports.length > 0 || result.redundantTypePatterns.length > 0 || result.identityWrappers.length > 0 || result.duplicateTypeDefinitions.length > 0 || result.duplicateInlineTypes.length > 0 || result.simplifiableFunctions.length > 0 || result.simplifiableExpressions.length > 0 || result.duplicateConstants.length > 0;
49
160
  const hasCircularIssues = (result) => result.circularDependencies.length > 0;
50
161
 
51
162
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deslop-cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "CLI for deslop-js — find unused files, exports, and dependencies",
5
5
  "keywords": [
6
6
  "cli",
@@ -16,7 +16,6 @@
16
16
  "name": "Aiden Bai",
17
17
  "email": "aiden@million.dev"
18
18
  },
19
- "type": "module",
20
19
  "bin": {
21
20
  "deslop": "./dist/cli.mjs"
22
21
  },
@@ -26,18 +25,19 @@
26
25
  "README.md",
27
26
  "LICENSE"
28
27
  ],
29
- "engines": {
30
- "node": ">=22"
28
+ "type": "module",
29
+ "publishConfig": {
30
+ "access": "public"
31
31
  },
32
32
  "dependencies": {
33
33
  "commander": "^14.0.3",
34
- "deslop-js": "0.0.10"
34
+ "deslop-js": "0.0.12"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.6.0"
38
38
  },
39
- "publishConfig": {
40
- "access": "public"
39
+ "engines": {
40
+ "node": ">=22"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "vp pack",