Skip to content

Commit 1b507bf

Browse files
committed
refactor: eliminate all any types and consolidate type definitions
- Remove 68 `any` occurrences across 15 files with proper TypeScript types - Replace `Record<string, any>` with `Record<string, unknown>` for JSON - Add type guards and narrowing for unsafe external data - Promote @typescript-eslint/no-explicit-any from warn to error - Consolidate duplicate types: PatternTrend, PatternCandidateBase, UsageLocation - Make GoldenFile extend IntelligenceGoldenFile to eliminate field duplication - Define PatternCandidateBase for shared pattern candidate fields - Create UsageLocation base for ImportUsage and SymbolUsage - All 234 tests passing, type-check clean, 0 lint errors
1 parent d28460c commit 1b507bf

File tree

20 files changed

+423
-234
lines changed

20 files changed

+423
-234
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@
3939
### Added
4040

4141
- **Definition-first ranking**: Exact-name searches now show the file that *defines* a symbol before files that use it. For example, searching `parseConfig` shows the function definition first, then callers.
42+
43+
### Refactored
44+
45+
- **Eliminated all `any` types**: 68 occurrences across 15 files now use proper TypeScript types. Replaced unsafe `Record<string, any>` with `Record<string, unknown>` and narrowed types using proper type guards. Promoted `@typescript-eslint/no-explicit-any` from `warn` to `error` to enforce strict typing.
46+
- **Consolidated duplicate type definitions**: Single source of truth for shared types:
47+
- `PatternTrend` canonical location in `types/index.ts` (imported by `usage-tracker.ts`)
48+
- New `PatternCandidateBase` for shared pattern fields; `PatternCandidate extends PatternCandidateBase`; runtime adds optional internal fields
49+
- New `UsageLocation` base for both `ImportUsage` and `SymbolUsage` (extends with `preview` field)
50+
- `GoldenFile extends IntelligenceGoldenFile` to eliminate field duplication (`file`, `score`)
51+
- Introduced `RuntimePatternPrimary` and `DecisionCard` types for tool-specific outputs
4252
- **Scope headers in code snippets**: When requesting snippets (`includeSnippets: true`), each code block now starts with a comment like `// UserService.login()` so agents know where the code lives without extra file reads.
4353
- **Edit decision card**: When searching with `intent="edit"`, `intent="refactor"`, or `intent="migrate"`, results now include a decision card telling you whether there's enough evidence to proceed safely. The card shows: whether you're ready (`ready: true/false`), what to do next if not (`nextAction`), relevant team patterns to follow, a top example file, how many callers appear in results (`impact.coverage`), and what searches would help close gaps (`whatWouldHelp`).
4454
- **Caller coverage tracking**: The decision card shows how many of a symbol's callers are in your search results. Low coverage (less than 40% when there are lots of callers) triggers an alert so you know to search more before editing.

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default tseslint.config(
1616
// import plugin is handled via recommended usually, but kept simple for now
1717
},
1818
rules: {
19-
'@typescript-eslint/no-explicit-any': 'warn',
19+
'@typescript-eslint/no-explicit-any': 'error',
2020
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' }],
2121
'no-console': ['warn', { 'allow': ['warn', 'error'] }],
2222
},

src/analyzers/angular/index.ts

Lines changed: 103 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* Detects state management patterns, architectural layers, and Angular-specific patterns
55
*/
66

7-
/* eslint-disable @typescript-eslint/no-explicit-any */
87
import { promises as fs } from 'fs';
98
import path from 'path';
9+
import { parse } from '@typescript-eslint/typescript-estree';
10+
import type { TSESTree } from '@typescript-eslint/typescript-estree';
1011
import {
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';
2122
import { createChunksFromCode } from '../../utils/chunking.js';
2223
import {
2324
CODEBASE_CONTEXT_DIRNAME,
2425
KEYWORD_INDEX_FILENAME
2526
} from '../../constants/codebase-context.js';
2627
import { 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+
2842
export 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

Comments
 (0)