Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ When run with `--ci` the CLI exits with a non-zero code if any findings are dete
- Staged-file scanning: run only what will be committed (fast pre-commit checks).
- Husky integration: optional pre-commit hooks to block commits locally.
- CI-ready: `--ci` mode for failing pipelines on findings.
- Unused JS/TS module detection: Each scan, CodeGuardian will warn about JavaScript and TypeScript files that are not imported or required by any other file (excluding entry points like `index.js`, `main.ts`, etc.). These warnings help you clean up unused code, but do not block CI or fail the scan.


## CLI options
Expand Down
87 changes: 86 additions & 1 deletion src/scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fg from 'fast-glob';
import ignore from 'ignore';
import { execSync } from 'node:child_process';
import chalk from 'chalk';
import { findUnusedModules } from './unusedModuleDetector.js';

const DEFAULT_CONFIG_FILES = ['.codeguardianrc.json', 'codeguardian.config.json'];

Expand Down Expand Up @@ -101,6 +101,11 @@ async function run({ configPath = null, staged = false, verbose = false } = {})

const findings = [];
let filesScanned = 0;
// For unused module detection
const jsTsFiles = [];
const importMap = new Map(); // file -> [imported files]
const allFilesSet = new Set();

for (const file of files) {
// small optimization: skip binary-ish files by extension
const ext = path.extname(file).toLowerCase();
Expand All @@ -118,6 +123,76 @@ async function run({ configPath = null, staged = false, verbose = false } = {})
if (fileFindings.length > 0) {
findings.push({ file, matches: fileFindings });
}

// Collect JS/TS files for unused module detection and unused import detection
if ([".js", ".ts"].includes(ext) && !file.includes(".test") && !file.includes("spec") && !file.includes("config") && !file.includes("setup")) {
jsTsFiles.push(file);
allFilesSet.add(path.resolve(file));
// Parse imports/requires
const imports = [];
// ES imports (capture imported identifiers)
const esImportRegex = /import\s+((?:[\w*{},\s]+)?)\s*from\s*["']([^"']+)["']/g;
let match;
const importDetails = [];
while ((match = esImportRegex.exec(content))) {
const imported = match[1].trim();
const source = match[2];
// Parse imported identifiers
let identifiers = [];
if (imported.startsWith("* as ")) {
identifiers.push(imported.replace("* as ", "").trim());
} else if (imported.startsWith("{")) {
// Named imports
identifiers = imported.replace(/[{}]/g, "").split(",").map(s => s.trim().split(" as ")[0]).filter(Boolean);
} else if (imported) {
identifiers.push(imported.split(",")[0].trim());
}
importDetails.push({ source, identifiers });
imports.push(source);
}
// CommonJS requires (variable assignment)
const requireVarRegex = /(?:const|let|var)\s+([\w{}*,\s]+)\s*=\s*require\(["']([^"']+)["']\)/g;
while ((match = requireVarRegex.exec(content))) {
const imported = match[1].trim();
const source = match[2];
let identifiers = [];
if (imported.startsWith("{")) {
identifiers = imported.replace(/[{}]/g, "").split(",").map(s => s.trim());
} else if (imported) {
identifiers.push(imported.split(",")[0].trim());
}
importDetails.push({ source, identifiers });
imports.push(source);
}
// Bare require (no variable assignment)
const requireRegex = /require\(["']([^"']+)["']\)/g;
while ((match = requireRegex.exec(content))) {
imports.push(match[1]);
}
importMap.set(path.resolve(file), imports);
// Unused import detection
// For each imported identifier, check if it's used in the file
const unusedImports = [];
for (const imp of importDetails) {
for (const id of imp.identifiers) {
// Simple usage check: look for identifier in code (excluding import line)
const usageRegex = new RegExp(`\\b${id.replace(/[$()*+.?^{}|\\]/g, "\\$&")}\\b`, "g");
// Remove import lines
const codeWithoutImports = content.replace(esImportRegex, "").replace(requireVarRegex, "");
const usageCount = (codeWithoutImports.match(usageRegex) || []).length;
if (usageCount === 0) {
unusedImports.push(id);
}
}
}
if (unusedImports.length > 0) {
console.log(chalk.yellowBright(`\nWarning: Unused imports in ${file}:`));
for (const id of unusedImports) {
console.log(chalk.yellow(` ${id}`));
}
console.log(chalk.gray('These imports are present but never used in this file.'));
}
}
}

// Print nice output
Expand All @@ -133,6 +208,16 @@ async function run({ configPath = null, staged = false, verbose = false } = {})
}
}

// Unused JS/TS module detection (warn only)
const unused = findUnusedModules(jsTsFiles, importMap);
if (unused.length > 0) {
console.log(chalk.yellowBright(`\nWarning: Unused modules detected (not imported by any other file):`));
for (const f of unused) {
console.log(chalk.yellow(` ${f}`));
}
console.log(chalk.gray('These files are not blocking CI, but consider cleaning up unused modules.'));
}

const endTime = process.hrtime.bigint();
const endMem = process.memoryUsage().heapUsed;
const durationMs = Number(endTime - startTime) / 1e6;
Expand Down
41 changes: 41 additions & 0 deletions src/unusedModuleDetector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Unused module detection logic for CodeGuardian
// Scans for JS/TS files not imported by any other file

import { access } from 'node:fs/promises';
import path from 'node:path';


export async function findUnusedModules (jsTsFiles, importMap) {
// Build set of all imported files (resolved to absolute)
const importedSet = new Set();
for (const [file, imports] of importMap.entries()) {
for (const imp of imports) {
if (imp.startsWith("./") || imp.startsWith("../")) {
let resolved;
const candidates = [
path.resolve(path.dirname(file), imp),
path.resolve(path.dirname(file), imp + ".js"),
path.resolve(path.dirname(file), imp + ".ts")
];
for (const candidate of candidates) {
try {
await access(candidate);
resolved = candidate;
break;
} catch {}
}
if (resolved) importedSet.add(resolved);
}
}
}
// Entry points: index.js/ts, cli.js/ts, main.js/ts
const entryRegex = /\b(index|cli|main)\.(js|ts)\b/i;
const unused = [];
for (const file of jsTsFiles) {
const abs = path.resolve(file);
if (!importedSet.has(abs) && !entryRegex.test(path.basename(file))) {
unused.push(file);
}
}
return unused;
}