@@ -4,7 +4,7 @@ import fg from 'fast-glob';
44import ignore from 'ignore' ;
55import { execSync } from 'node:child_process' ;
66import chalk from 'chalk' ;
7-
7+ import { findUnusedModules } from './unusedModuleDetector.js' ;
88
99const DEFAULT_CONFIG_FILES = [ '.codeguardianrc.json' , 'codeguardian.config.json' ] ;
1010
@@ -101,6 +101,11 @@ async function run({ configPath = null, staged = false, verbose = false } = {})
101101
102102 const findings = [ ] ;
103103 let filesScanned = 0 ;
104+ // For unused module detection
105+ const jsTsFiles = [ ] ;
106+ const importMap = new Map ( ) ; // file -> [imported files]
107+ const allFilesSet = new Set ( ) ;
108+
104109 for ( const file of files ) {
105110 // small optimization: skip binary-ish files by extension
106111 const ext = path . extname ( file ) . toLowerCase ( ) ;
@@ -118,6 +123,76 @@ async function run({ configPath = null, staged = false, verbose = false } = {})
118123 if ( fileFindings . length > 0 ) {
119124 findings . push ( { file, matches : fileFindings } ) ;
120125 }
126+
127+ // Collect JS/TS files for unused module detection and unused import detection
128+ if ( [ ".js" , ".ts" ] . includes ( ext ) && ! file . includes ( ".test" ) && ! file . includes ( "spec" ) && ! file . includes ( "config" ) && ! file . includes ( "setup" ) ) {
129+ jsTsFiles . push ( file ) ;
130+ allFilesSet . add ( path . resolve ( file ) ) ;
131+ // Parse imports/requires
132+ const imports = [ ] ;
133+ // ES imports (capture imported identifiers)
134+ const esImportRegex = / i m p o r t \s + ( (?: [ \w * { } , \s ] + ) ? ) \s * f r o m \s * [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / g;
135+ let match ;
136+ const importDetails = [ ] ;
137+ while ( ( match = esImportRegex . exec ( content ) ) ) {
138+ const imported = match [ 1 ] . trim ( ) ;
139+ const source = match [ 2 ] ;
140+ // Parse imported identifiers
141+ let identifiers = [ ] ;
142+ if ( imported . startsWith ( "* as " ) ) {
143+ identifiers . push ( imported . replace ( "* as " , "" ) . trim ( ) ) ;
144+ } else if ( imported . startsWith ( "{" ) ) {
145+ // Named imports
146+ identifiers = imported . replace ( / [ { } ] / g, "" ) . split ( "," ) . map ( s => s . trim ( ) . split ( " as " ) [ 0 ] ) . filter ( Boolean ) ;
147+ } else if ( imported ) {
148+ identifiers . push ( imported . split ( "," ) [ 0 ] . trim ( ) ) ;
149+ }
150+ importDetails . push ( { source, identifiers } ) ;
151+ imports . push ( source ) ;
152+ }
153+ // CommonJS requires (variable assignment)
154+ const requireVarRegex = / (?: c o n s t | l e t | v a r ) \s + ( [ \w { } * , \s ] + ) \s * = \s * r e q u i r e \( [ " ' ] ( [ ^ " ' ] + ) [ " ' ] \) / g;
155+ while ( ( match = requireVarRegex . exec ( content ) ) ) {
156+ const imported = match [ 1 ] . trim ( ) ;
157+ const source = match [ 2 ] ;
158+ let identifiers = [ ] ;
159+ if ( imported . startsWith ( "{" ) ) {
160+ identifiers = imported . replace ( / [ { } ] / g, "" ) . split ( "," ) . map ( s => s . trim ( ) ) ;
161+ } else if ( imported ) {
162+ identifiers . push ( imported . split ( "," ) [ 0 ] . trim ( ) ) ;
163+ }
164+ importDetails . push ( { source, identifiers } ) ;
165+ imports . push ( source ) ;
166+ }
167+ // Bare require (no variable assignment)
168+ const requireRegex = / r e q u i r e \( [ " ' ] ( [ ^ " ' ] + ) [ " ' ] \) / g;
169+ while ( ( match = requireRegex . exec ( content ) ) ) {
170+ imports . push ( match [ 1 ] ) ;
171+ }
172+ importMap . set ( path . resolve ( file ) , imports ) ;
173+ // Unused import detection
174+ // For each imported identifier, check if it's used in the file
175+ const unusedImports = [ ] ;
176+ for ( const imp of importDetails ) {
177+ for ( const id of imp . identifiers ) {
178+ // Simple usage check: look for identifier in code (excluding import line)
179+ const usageRegex = new RegExp ( `\\b${ id . replace ( / [ $ ( ) * + . ? ^ { } | \\ ] / g, "\\$&" ) } \\b` , "g" ) ;
180+ // Remove import lines
181+ const codeWithoutImports = content . replace ( esImportRegex , "" ) . replace ( requireVarRegex , "" ) ;
182+ const usageCount = ( codeWithoutImports . match ( usageRegex ) || [ ] ) . length ;
183+ if ( usageCount === 0 ) {
184+ unusedImports . push ( id ) ;
185+ }
186+ }
187+ }
188+ if ( unusedImports . length > 0 ) {
189+ console . log ( chalk . yellowBright ( `\nWarning: Unused imports in ${ file } :` ) ) ;
190+ for ( const id of unusedImports ) {
191+ console . log ( chalk . yellow ( ` ${ id } ` ) ) ;
192+ }
193+ console . log ( chalk . gray ( 'These imports are present but never used in this file.' ) ) ;
194+ }
195+ }
121196 }
122197
123198 // Print nice output
@@ -133,6 +208,16 @@ async function run({ configPath = null, staged = false, verbose = false } = {})
133208 }
134209 }
135210
211+ // Unused JS/TS module detection (warn only)
212+ const unused = findUnusedModules ( jsTsFiles , importMap ) ;
213+ if ( unused . length > 0 ) {
214+ console . log ( chalk . yellowBright ( `\nWarning: Unused modules detected (not imported by any other file):` ) ) ;
215+ for ( const f of unused ) {
216+ console . log ( chalk . yellow ( ` ${ f } ` ) ) ;
217+ }
218+ console . log ( chalk . gray ( 'These files are not blocking CI, but consider cleaning up unused modules.' ) ) ;
219+ }
220+
136221 const endTime = process . hrtime . bigint ( ) ;
137222 const endMem = process . memoryUsage ( ) . heapUsed ;
138223 const durationMs = Number ( endTime - startTime ) / 1e6 ;
0 commit comments