Skip to content

Commit dd714b5

Browse files
committed
feat: detect and report unused package.json dependencies
This commit introduces the ability to detect and report unused dependencies listed in the `package.json` file. It enhances TreeTracr's analysis capabilities by identifying dependencies that are not actually imported or required in the project's source code. - Implemented `checkUnusedPackageDependencies` function in `src/analyzer.js` to analyze `package.json` and identify unused dependencies. - Added a new `--fail-on-unused-deps` CLI flag to `src/cli.js` to allow failing the CI check if unused package dependencies are found. - Updated `index.js` to include a new section in the output for unused package dependencies and to handle the `--fail-on-unused-deps` flag, setting appropriate exit codes. - Updated `README.md` with documentation for the new feature and CLI flag. - Added a new test case to `test/cli.test.js` to verify the functionality of the `--fail-on-unused-deps` flag.
1 parent 4da793b commit dd714b5

File tree

6 files changed

+242
-11
lines changed

6 files changed

+242
-11
lines changed

README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A JavaScript/TypeScript dependency analyzer that helps you visualize and underst
88
- Visualizes dependency trees from entry points
99
- Identifies unused modules
1010
- Detects and highlights circular dependencies
11+
- Identifies unused package.json dependencies
1112
- Automatically detects and analyzes test files
1213
- Supports custom test directories
1314

@@ -30,6 +31,7 @@ treetracr [options] [directory] [entryPoint]
3031
- `--ci`: Enable CI mode (exits with error if issues found)
3132
- `--fail-on-circular`: Exit with error code if circular dependencies found
3233
- `--fail-on-unused`: Exit with error code if unused modules found
34+
- `--fail-on-unused-deps`: Exit with error code if unused package.json dependencies found
3335

3436
### Examples
3537

@@ -74,7 +76,9 @@ jobs:
7476
# - Code 0: No issues found
7577
# - Code 1: Circular dependencies found
7678
# - Code 2: Unused modules found
77-
# - Code 3: Both issues found
79+
# - Code 3: Both circular dependencies and unused modules found
80+
# - Code 4: Unused package dependencies found
81+
# - Code 7: All issues found
7882
```
7983

8084
## Test File Detection
@@ -183,6 +187,16 @@ Found 2 unused local modules:
183187

184188
No circular dependencies detected!
185189

190+
╭──────────────────────────────╮
191+
│ │
192+
│ UNUSED PACKAGE DEPENDENCIES │
193+
│ │
194+
╰──────────────────────────────╯
195+
196+
Found 2 unused package.json dependencies:
197+
- some-unused-package
198+
- another-unused-package
199+
186200
╭────────────────╮
187201
│ │
188202
│ TEST FILES │
@@ -222,6 +236,7 @@ CI mode with failures would look like:
222236
❌ CI checks failed!
223237
- Found 3 circular dependencies
224238
- Found 2 unused modules
239+
- Found 2 unused package dependencies
225240
```
226241

227242
## Why TreeTracr?

index.js

+45-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
buildDependencyMaps,
1111
traceDependenciesFromEntry,
1212
isTestFile,
13-
detectCircularDependencies
13+
detectCircularDependencies,
14+
checkUnusedPackageDependencies
1415
} from './src/analyzer.js'
1516
import {
1617
formatPath,
@@ -131,6 +132,29 @@ async function main() {
131132
}
132133
}
133134

135+
// Add section for unused package dependencies
136+
output.section('UNUSED PACKAGE DEPENDENCIES')
137+
138+
// Check for unused dependencies in package.json
139+
const unusedDeps = await checkUnusedPackageDependencies(sourceDir)
140+
141+
if (unusedDeps.size === 0) {
142+
output.success('No unused package.json dependencies found!')
143+
} else {
144+
output.warning(`Found ${unusedDeps.size} unused package.json dependencies:`)
145+
Array.from(unusedDeps).forEach((dep) => {
146+
output.warning(`- ${dep}`)
147+
})
148+
149+
// Mark CI check as failed for unused package dependencies
150+
if (options.failOnUnusedPackageDeps) {
151+
output.error('CI check failed: Unused package dependencies detected')
152+
if (options.ci) {
153+
exitCode = Math.max(exitCode, 4); // Use exit code 4 for unused package dependencies
154+
}
155+
}
156+
}
157+
134158
// Add new section for test files
135159
output.section('TEST FILES')
136160

@@ -153,15 +177,17 @@ async function main() {
153177
}
154178

155179
// Add summary section for CI mode
156-
if (options.ci || options.failOnCircular || options.failOnUnused) {
180+
if (options.ci || options.failOnCircular || options.failOnUnused || options.failOnUnusedPackageDeps) {
157181
output.section('CI CHECK SUMMARY')
158182

159183
const hasCircularDeps = circularDeps.length > 0
160184
const hasUnusedModules = unusedModules.length > 0
185+
const hasUnusedPackageDeps = unusedDeps.size > 0
161186
const shouldFailCircular = options.failOnCircular && hasCircularDeps
162187
const shouldFailUnused = options.failOnUnused && hasUnusedModules
188+
const shouldFailUnusedPackageDeps = options.failOnUnusedPackageDeps && hasUnusedPackageDeps
163189

164-
if (!shouldFailCircular && !shouldFailUnused) {
190+
if (!shouldFailCircular && !shouldFailUnused && !shouldFailUnusedPackageDeps) {
165191
output.success('✅ All checks passed!')
166192

167193
// Show details in regular mode but be concise in CI mode
@@ -172,6 +198,9 @@ async function main() {
172198
if (options.failOnUnused) {
173199
output.success('- No unused modules')
174200
}
201+
if (options.failOnUnusedPackageDeps) {
202+
output.success('- No unused package dependencies')
203+
}
175204
}
176205
} else {
177206
output.error('❌ CI checks failed!')
@@ -184,9 +213,19 @@ async function main() {
184213
output.error(`- Found ${unusedModules.length} unused modules`)
185214
}
186215

187-
// Set combined status code if both issues are present
188-
if (shouldFailCircular && shouldFailUnused) {
189-
exitCode = 3; // Exit code 3 for both issues
216+
if (shouldFailUnusedPackageDeps) {
217+
output.error(`- Found ${unusedDeps.size} unused package dependencies`)
218+
}
219+
220+
// Determine exit code based on combination of failures
221+
if (shouldFailUnusedPackageDeps) {
222+
if (shouldFailCircular || shouldFailUnused) {
223+
exitCode = 7; // Exit code 7 for all issues
224+
} else {
225+
exitCode = 4; // Exit code 4 for unused package dependencies
226+
}
227+
} else if (shouldFailCircular && shouldFailUnused) {
228+
exitCode = 3; // Exit code 3 for both circular and unused modules
190229
} else if (shouldFailCircular) {
191230
exitCode = 1; // Exit code 1 for circular dependencies
192231
} else if (shouldFailUnused) {

src/analyzer.js

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs'
22
import path from 'path'
33
import { IMPORT_PATTERNS, TEST_PATTERNS, JS_EXTENSIONS } from './constants.js'
4-
import { readFile } from './fileSystem.js'
4+
import { readFile, getPackageJson } from './fileSystem.js'
55
import { output } from './output.js'
66

77
// Store file dependencies
@@ -10,6 +10,8 @@ export const moduleDependencies = new Map()
1010
export const moduleReferences = new Map()
1111
// Store circular dependencies
1212
export const circularDependencies = new Map()
13+
// Store unused package.json dependencies
14+
export const unusedPackageDependencies = new Set()
1315

1416
/**
1517
* Normalize a path to handle different import styles
@@ -164,3 +166,66 @@ export async function traceDependenciesFromEntry(entryPath, sourceDir, visited =
164166
export function isTestFile(filePath) {
165167
return TEST_PATTERNS.some(pattern => pattern.test(filePath));
166168
}
169+
170+
/**
171+
* Check if dependencies in package.json are actually used in the source files
172+
* @param {string} sourceDir - Directory to analyze
173+
* @returns {Promise<Set<string>>} - Set of unused dependencies
174+
*/
175+
export async function checkUnusedPackageDependencies(sourceDir) {
176+
// Clear the set of unused dependencies
177+
unusedPackageDependencies.clear()
178+
179+
// Get package.json
180+
const packageJson = await getPackageJson(sourceDir)
181+
if (!packageJson || !packageJson.dependencies) {
182+
return unusedPackageDependencies
183+
}
184+
185+
// Get all dependencies from package.json
186+
const dependencies = Object.keys(packageJson.dependencies)
187+
188+
// Get all content from source files to check for imports
189+
const files = Array.from(moduleDependencies.keys())
190+
const allContent = await Promise.all(
191+
files.map(async file => {
192+
try {
193+
return await readFile(file, 'utf8')
194+
} catch (error) {
195+
output.error(`Error reading file ${file}: ${error.message}`)
196+
return ''
197+
}
198+
})
199+
)
200+
201+
// Join all content for simpler checking
202+
const combinedContent = allContent.join('\n')
203+
204+
// Check each dependency
205+
for (const dependency of dependencies) {
206+
// Different patterns to match dependencies
207+
const importPatterns = [
208+
// ES6 named imports
209+
new RegExp(`import\\s+(?:[\\w\\s{},*]+from\\s+)?['"]${dependency}(?:[\\w\\s-./]*)['"]`, 'g'),
210+
// ES6 default imports
211+
new RegExp(`import\\s+[\\w\\s]+\\s+from\\s+['"]${dependency}(?:[\\w\\s-./]*)['"]`, 'g'),
212+
// ES6 namespace imports
213+
new RegExp(`import\\s+\\*\\s+as\\s+[\\w\\s]+\\s+from\\s+['"]${dependency}(?:[\\w\\s-./]*)['"]`, 'g'),
214+
// CommonJS require
215+
new RegExp(`require\\s*\\(\\s*['"]${dependency}(?:[\\w\\s-./]*)['"]\\s*\\)`, 'g'),
216+
// Dynamic imports
217+
new RegExp(`import\\s*\\(['"]${dependency}(?:[\\w\\s-./]*)['"]\\)`, 'g'),
218+
// Package references in comments or strings
219+
new RegExp(`['"]${dependency}(?:[\\w\\s-./]*)['"]`, 'g')
220+
]
221+
222+
// Check if dependency is used in any file
223+
const isUsed = importPatterns.some(pattern => pattern.test(combinedContent))
224+
225+
if (!isUsed) {
226+
unusedPackageDependencies.add(dependency)
227+
}
228+
}
229+
230+
return unusedPackageDependencies
231+
}

src/cli.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function parseCommandLine() {
1414
ci: false,
1515
failOnCircular: false,
1616
failOnUnused: false,
17+
failOnUnusedPackageDeps: false,
1718
remainingArgs: []
1819
};
1920

@@ -31,6 +32,8 @@ export function parseCommandLine() {
3132
result.failOnCircular = true;
3233
} else if (arg === '--fail-on-unused') {
3334
result.failOnUnused = true;
35+
} else if (arg === '--fail-on-unused-deps') {
36+
result.failOnUnusedPackageDeps = true;
3437
} else if (!arg.startsWith('-')) {
3538
// Process positional arguments
3639
if (!result.sourceDir || result.sourceDir === '.') {
@@ -46,10 +49,11 @@ export function parseCommandLine() {
4649
}
4750
}
4851

49-
// If ci is true but neither failure mode specified, enable both
50-
if (result.ci && !result.failOnCircular && !result.failOnUnused) {
52+
// If ci is true but no failure modes specified, enable all failure modes
53+
if (result.ci && !result.failOnCircular && !result.failOnUnused && !result.failOnUnusedPackageDeps) {
5154
result.failOnCircular = true;
5255
result.failOnUnused = true;
56+
result.failOnUnusedPackageDeps = true;
5357
}
5458

5559
return result;
@@ -71,6 +75,7 @@ OPTIONS:
7175
--ci Enable CI mode (exits with error if issues found)
7276
--fail-on-circular Exit with error code if circular dependencies found
7377
--fail-on-unused Exit with error code if unused modules found
78+
--fail-on-unused-deps Exit with error code if unused package.json dependencies found
7479
7580
ARGUMENTS:
7681
directory Target directory to analyze (default: current directory)

test/cli.test.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ test('parseCommandLine should handle empty arguments', () => {
1717
ci: false,
1818
failOnCircular: false,
1919
failOnUnused: false,
20+
failOnUnusedPackageDeps: false,
2021
remainingArgs: []
2122
});
2223
});
@@ -59,6 +60,7 @@ test('parseCommandLine should handle CI mode flag', () => {
5960
assert.strictEqual(result.ci, true);
6061
assert.strictEqual(result.failOnCircular, true);
6162
assert.strictEqual(result.failOnUnused, true);
63+
assert.strictEqual(result.failOnUnusedPackageDeps, true);
6264
});
6365

6466
// Test for fail-on-circular flag
@@ -79,13 +81,24 @@ test('parseCommandLine should handle fail-on-unused flag', () => {
7981
assert.strictEqual(result.failOnUnused, true);
8082
});
8183

84+
// Test for fail-on-unused-deps flag
85+
test('parseCommandLine should handle fail-on-unused-deps flag', () => {
86+
process.argv = ['node', 'index.js', '--fail-on-unused-deps'];
87+
const result = parseCommandLine();
88+
assert.strictEqual(result.ci, false);
89+
assert.strictEqual(result.failOnCircular, false);
90+
assert.strictEqual(result.failOnUnused, false);
91+
assert.strictEqual(result.failOnUnusedPackageDeps, true);
92+
});
93+
8294
// Test for combined CI flags
8395
test('parseCommandLine should handle combined CI flags', () => {
84-
process.argv = ['node', 'index.js', '--ci', '--fail-on-circular', '--fail-on-unused'];
96+
process.argv = ['node', 'index.js', '--ci', '--fail-on-circular', '--fail-on-unused', '--fail-on-unused-deps'];
8597
const result = parseCommandLine();
8698
assert.strictEqual(result.ci, true);
8799
assert.strictEqual(result.failOnCircular, true);
88100
assert.strictEqual(result.failOnUnused, true);
101+
assert.strictEqual(result.failOnUnusedPackageDeps, true);
89102
});
90103

91104
// Restore original argv after tests

0 commit comments

Comments
 (0)