44 * Detects state management patterns, architectural layers, and Angular-specific patterns
55 */
66
7- /* eslint-disable @typescript-eslint/no-explicit-any */
87import { promises as fs } from 'fs' ;
98import path from 'path' ;
9+ import { parse } from '@typescript-eslint/typescript-estree' ;
10+ import type { TSESTree } from '@typescript-eslint/typescript-estree' ;
1011import {
1112 FrameworkAnalyzer ,
1213 AnalysisResult ,
@@ -15,16 +16,29 @@ import {
1516 CodeComponent ,
1617 ImportStatement ,
1718 ExportStatement ,
18- ArchitecturalLayer
19+ ArchitecturalLayer ,
20+ DependencyCategory
1921} from '../../types/index.js' ;
20- import { parse } from '@typescript-eslint/typescript-estree' ;
2122import { createChunksFromCode } from '../../utils/chunking.js' ;
2223import {
2324 CODEBASE_CONTEXT_DIRNAME ,
2425 KEYWORD_INDEX_FILENAME
2526} from '../../constants/codebase-context.js' ;
2627import { registerComplementaryPatterns } from '../../patterns/semantics.js' ;
2728
29+ interface AngularInput {
30+ name : string ;
31+ type : string ;
32+ style : 'decorator' | 'signal' ;
33+ required ?: boolean ;
34+ }
35+
36+ interface AngularOutput {
37+ name : string ;
38+ type : string ;
39+ style : 'decorator' | 'signal' ;
40+ }
41+
2842export class AngularAnalyzer implements FrameworkAnalyzer {
2943 readonly name = 'angular' ;
3044 readonly version = '1.0.0' ;
@@ -144,12 +158,13 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
144158 const source = node . source . value as string ;
145159 imports . push ( {
146160 source,
147- imports : node . specifiers . map ( ( s : any ) => {
161+ imports : node . specifiers . map ( ( s : TSESTree . ImportClause ) => {
148162 if ( s . type === 'ImportDefaultSpecifier' ) return 'default' ;
149163 if ( s . type === 'ImportNamespaceSpecifier' ) return '*' ;
150- return s . imported ?. name || s . local . name ;
164+ const specifier = s as TSESTree . ImportSpecifier ;
165+ return specifier . imported . name || specifier . local . name ;
151166 } ) ,
152- isDefault : node . specifiers . some ( ( s : any ) => s . type === 'ImportDefaultSpecifier' ) ,
167+ isDefault : node . specifiers . some ( ( s : TSESTree . ImportClause ) => s . type === 'ImportDefaultSpecifier' ) ,
153168 isDynamic : false ,
154169 line : node . loc ?. start . line
155170 } ) ;
@@ -352,15 +367,21 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
352367 }
353368
354369 private async extractAngularComponent (
355- classNode : any ,
370+ classNode : TSESTree . ClassDeclaration ,
356371 content : string
357372 ) : Promise < CodeComponent | null > {
358- if ( ! classNode . decorators || classNode . decorators . length === 0 ) {
373+ if ( ! classNode . id || ! classNode . decorators || classNode . decorators . length === 0 ) {
359374 return null ;
360375 }
361376
362377 const decorator = classNode . decorators [ 0 ] ;
363- const decoratorName = decorator . expression . callee ?. name || decorator . expression . name ;
378+ const expr = decorator . expression ;
379+ const decoratorName : string =
380+ expr . type === 'CallExpression' && expr . callee . type === 'Identifier'
381+ ? expr . callee . name
382+ : expr . type === 'Identifier'
383+ ? expr . name
384+ : '' ;
364385
365386 let componentType : string | undefined ;
366387 let angularType : string | undefined ;
@@ -466,27 +487,28 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
466487 } ;
467488 }
468489
469- private extractDecoratorMetadata ( decorator : any ) : Record < string , any > {
470- const metadata : Record < string , any > = { } ;
490+ private extractDecoratorMetadata ( decorator : TSESTree . Decorator ) : Record < string , unknown > {
491+ const metadata : Record < string , unknown > = { } ;
471492
472493 try {
473- if ( decorator . expression . arguments && decorator . expression . arguments [ 0 ] ) {
494+ if ( decorator . expression . type === 'CallExpression' && decorator . expression . arguments [ 0 ] ) {
474495 const arg = decorator . expression . arguments [ 0 ] ;
475496
476497 if ( arg . type === 'ObjectExpression' ) {
477498 for ( const prop of arg . properties ) {
478- if ( prop . key && prop . value ) {
479- const key = prop . key . name || prop . key . value ;
480-
481- if ( prop . value . type === 'Literal' ) {
482- metadata [ key ] = prop . value . value ;
483- } else if ( prop . value . type === 'ArrayExpression' ) {
484- metadata [ key ] = prop . value . elements
485- . map ( ( el : any ) => ( el . type === 'Literal' ? el . value : null ) )
486- . filter ( Boolean ) ;
487- } else if ( prop . value . type === 'Identifier' ) {
488- metadata [ key ] = prop . value . name ;
489- }
499+ if ( prop . type !== 'Property' ) continue ;
500+ const keyNode = prop . key as { name ?: string ; value ?: unknown } ;
501+ const key = keyNode . name ?? String ( keyNode . value ?? '' ) ;
502+ if ( ! key ) continue ;
503+
504+ if ( prop . value . type === 'Literal' ) {
505+ metadata [ key ] = prop . value . value ;
506+ } else if ( prop . value . type === 'ArrayExpression' ) {
507+ metadata [ key ] = prop . value . elements
508+ . map ( ( el ) => ( el && el . type === 'Literal' ? el . value : null ) )
509+ . filter ( Boolean ) ;
510+ } else if ( prop . value . type === 'Identifier' ) {
511+ metadata [ key ] = prop . value . name ;
490512 }
491513 }
492514 }
@@ -498,7 +520,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
498520 return metadata ;
499521 }
500522
501- private extractLifecycleHooks ( classNode : any ) : string [ ] {
523+ private extractLifecycleHooks ( classNode : TSESTree . ClassDeclaration ) : string [ ] {
502524 const hooks : string [ ] = [ ] ;
503525 const lifecycleHooks = [
504526 'ngOnChanges' ,
@@ -513,7 +535,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
513535
514536 if ( classNode . body && classNode . body . body ) {
515537 for ( const member of classNode . body . body ) {
516- if ( member . type === 'MethodDefinition' && member . key ) {
538+ if ( member . type === 'MethodDefinition' && member . key && member . key . type === 'Identifier' ) {
517539 const methodName = member . key . name ;
518540 if ( lifecycleHooks . includes ( methodName ) ) {
519541 hooks . push ( methodName ) ;
@@ -525,7 +547,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
525547 return hooks ;
526548 }
527549
528- private extractInjectedServices ( classNode : any ) : string [ ] {
550+ private extractInjectedServices ( classNode : TSESTree . ClassDeclaration ) : string [ ] {
529551 const services : string [ ] = [ ] ;
530552
531553 // Look for constructor parameters
@@ -534,8 +556,12 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
534556 if ( member . type === 'MethodDefinition' && member . kind === 'constructor' ) {
535557 if ( member . value . params ) {
536558 for ( const param of member . value . params ) {
537- if ( param . typeAnnotation ?. typeAnnotation ?. typeName ) {
538- services . push ( param . typeAnnotation . typeAnnotation . typeName . name ) ;
559+ const typedParam = param as TSESTree . Identifier ;
560+ if ( typedParam . typeAnnotation ?. typeAnnotation ?. type === 'TSTypeReference' ) {
561+ const typeRef = typedParam . typeAnnotation . typeAnnotation as TSESTree . TSTypeReference ;
562+ if ( typeRef . typeName . type === 'Identifier' ) {
563+ services . push ( typeRef . typeName . name ) ;
564+ }
539565 }
540566 }
541567 }
@@ -546,40 +572,46 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
546572 return services ;
547573 }
548574
549- private extractInputs ( classNode : any ) : any [ ] {
550- const inputs : any [ ] = [ ] ;
575+ private extractInputs ( classNode : TSESTree . ClassDeclaration ) : AngularInput [ ] {
576+ const inputs : AngularInput [ ] = [ ] ;
551577
552578 if ( classNode . body && classNode . body . body ) {
553579 for ( const member of classNode . body . body ) {
554580 if ( member . type === 'PropertyDefinition' ) {
555581 // Check for decorator-based @Input()
556582 if ( member . decorators ) {
557- const hasInput = member . decorators . some (
558- ( d : any ) => d . expression ?. callee ?. name === 'Input' || d . expression ?. name === 'Input'
559- ) ;
560-
561- if ( hasInput && member . key ) {
583+ const hasInput = member . decorators . some ( ( d : TSESTree . Decorator ) => {
584+ const expr = d . expression ;
585+ return (
586+ ( expr . type === 'CallExpression' &&
587+ expr . callee . type === 'Identifier' &&
588+ expr . callee . name === 'Input' ) ||
589+ ( expr . type === 'Identifier' && expr . name === 'Input' )
590+ ) ;
591+ } ) ;
592+
593+ if ( hasInput && member . key && 'name' in member . key ) {
562594 inputs . push ( {
563595 name : member . key . name ,
564- type : member . typeAnnotation ?. typeAnnotation ?. type || 'any ',
596+ type : ( member . typeAnnotation ?. typeAnnotation ?. type as string | undefined ) || 'unknown ',
565597 style : 'decorator'
566598 } ) ;
567599 }
568600 }
569601
570602 // Check for signal-based input() (Angular v17.1+)
571- if ( member . value && member . key ) {
572- const valueStr =
573- member . value . type === 'CallExpression'
574- ? member . value . callee ?. name || member . value . callee ?. object ?. name
575- : null ;
603+ if ( member . value && member . key && 'name' in member . key ) {
604+ const callee = member . value . type === 'CallExpression'
605+ ? ( member . value . callee as { type : string ; name ?: string ; object ?: { name ?: string } ; property ?: { name ?: string } } )
606+ : null ;
607+ const valueStr = callee ?. name ?? callee ?. object ?. name ?? null ;
576608
577609 if ( valueStr === 'input' ) {
578610 inputs . push ( {
579611 name : member . key . name ,
580612 type : 'InputSignal' ,
581613 style : 'signal' ,
582- required : member . value . callee ?. property ?. name === 'required'
614+ required : callee ?. property ?. name === 'required'
583615 } ) ;
584616 }
585617 }
@@ -590,19 +622,25 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
590622 return inputs ;
591623 }
592624
593- private extractOutputs ( classNode : any ) : any [ ] {
594- const outputs : any [ ] = [ ] ;
625+ private extractOutputs ( classNode : TSESTree . ClassDeclaration ) : AngularOutput [ ] {
626+ const outputs : AngularOutput [ ] = [ ] ;
595627
596628 if ( classNode . body && classNode . body . body ) {
597629 for ( const member of classNode . body . body ) {
598630 if ( member . type === 'PropertyDefinition' ) {
599631 // Check for decorator-based @Output()
600632 if ( member . decorators ) {
601- const hasOutput = member . decorators . some (
602- ( d : any ) => d . expression ?. callee ?. name === 'Output' || d . expression ?. name === 'Output'
603- ) ;
604-
605- if ( hasOutput && member . key ) {
633+ const hasOutput = member . decorators . some ( ( d : TSESTree . Decorator ) => {
634+ const expr = d . expression ;
635+ return (
636+ ( expr . type === 'CallExpression' &&
637+ expr . callee . type === 'Identifier' &&
638+ expr . callee . name === 'Output' ) ||
639+ ( expr . type === 'Identifier' && expr . name === 'Output' )
640+ ) ;
641+ } ) ;
642+
643+ if ( hasOutput && member . key && 'name' in member . key ) {
606644 outputs . push ( {
607645 name : member . key . name ,
608646 type : 'EventEmitter' ,
@@ -612,9 +650,11 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
612650 }
613651
614652 // Check for signal-based output() (Angular v17.1+)
615- if ( member . value && member . key ) {
616- const valueStr =
617- member . value . type === 'CallExpression' ? member . value . callee ?. name : null ;
653+ if ( member . value && member . key && 'name' in member . key ) {
654+ const callee = member . value . type === 'CallExpression'
655+ ? ( member . value . callee as { type : string ; name ?: string } )
656+ : null ;
657+ const valueStr = callee ?. name ?? null ;
618658
619659 if ( valueStr === 'output' ) {
620660 outputs . push ( {
@@ -755,7 +795,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
755795 return 'unknown' ;
756796 }
757797
758- private categorizeDependency ( name : string ) : any {
798+ private categorizeDependency ( name : string ) : DependencyCategory {
759799 if ( name . startsWith ( '@angular/' ) ) {
760800 return 'framework' ;
761801 }
@@ -885,20 +925,21 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
885925 try {
886926 const indexPath = path . join ( rootPath , CODEBASE_CONTEXT_DIRNAME , KEYWORD_INDEX_FILENAME ) ;
887927 const indexContent = await fs . readFile ( indexPath , 'utf-8' ) ;
888- const parsed = JSON . parse ( indexContent ) as any ;
928+ const parsed = JSON . parse ( indexContent ) as unknown ;
889929
890930 // Legacy index.json is an array — do not consume it (missing version/meta headers).
891931 if ( Array . isArray ( parsed ) ) {
892932 return metadata ;
893933 }
894934
895- const chunks = parsed && Array . isArray ( parsed . chunks ) ? parsed . chunks : null ;
935+ const parsedObj = parsed as { chunks ?: unknown } ;
936+ const chunks = parsedObj && Array . isArray ( parsedObj . chunks ) ? ( parsedObj . chunks as Array < { filePath ?: string ; startLine ?: number ; endLine ?: number ; componentType ?: string ; layer ?: string } > ) : null ;
896937 if ( Array . isArray ( chunks ) && chunks . length > 0 ) {
897938 console . error ( `Loading statistics from ${ indexPath } : ${ chunks . length } chunks` ) ;
898939
899- metadata . statistics . totalFiles = new Set ( chunks . map ( ( c : any ) => c . filePath ) ) . size ;
940+ metadata . statistics . totalFiles = new Set ( chunks . map ( ( c ) => c . filePath ) ) . size ;
900941 metadata . statistics . totalLines = chunks . reduce (
901- ( sum : number , c : any ) => sum + ( c . endLine - c . startLine + 1 ) ,
942+ ( sum , c ) => sum + ( ( c . endLine ?? 0 ) - ( c . startLine ?? 0 ) + 1 ) ,
902943 0
903944 ) ;
904945
@@ -954,8 +995,8 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
954995 switch ( componentType ) {
955996 case 'component' : {
956997 const selector = metadata ?. selector || 'unknown' ;
957- const inputs = metadata ?. inputs ?. length || 0 ;
958- const outputs = metadata ?. outputs ?. length || 0 ;
998+ const inputs = Array . isArray ( metadata ?. inputs ) ? metadata . inputs . length : 0 ;
999+ const outputs = Array . isArray ( metadata ?. outputs ) ? metadata . outputs . length : 0 ;
9591000 const lifecycle = this . extractLifecycleMethods ( content ) ;
9601001 return `Angular component '${ className } ' (selector: ${ selector } )${
9611002 lifecycle ? ` with ${ lifecycle } ` : ''
@@ -986,8 +1027,8 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
9861027 }
9871028
9881029 case 'module' : {
989- const imports = metadata ?. imports ?. length || 0 ;
990- const declarations = metadata ?. declarations ?. length || 0 ;
1030+ const imports = Array . isArray ( metadata ?. imports ) ? metadata . imports . length : 0 ;
1031+ const declarations = Array . isArray ( metadata ?. declarations ) ? metadata . declarations . length : 0 ;
9911032 return `Angular module '${ className } ' with ${ declarations } declarations and ${ imports } imports.` ;
9921033 }
9931034
0 commit comments