diff --git a/src/analyzers/typeNarrowing.ts b/src/analyzers/typeNarrowing.ts new file mode 100644 index 0000000..e0c7f55 --- /dev/null +++ b/src/analyzers/typeNarrowing.ts @@ -0,0 +1,212 @@ +import { DependencyGraph, NarrowingIssue, GraphNode } from '../types'; + +export interface NarrowingAnalysisOptions { + maxNestingDepth?: number; + warnOnNever?: boolean; + strictConditional?: boolean; +} + +export class TypeNarrowingAnalyzer { + private graph: DependencyGraph; + private options: NarrowingAnalysisOptions; + private issues: NarrowingIssue[]; + + constructor(graph: DependencyGraph, options: NarrowingAnalysisOptions = {}) { + this.graph = graph; + this.options = { + maxNestingDepth: options.maxNestingDepth ?? 3, + warnOnNever: options.warnOnNever ?? true, + strictConditional: options.strictConditional ?? false + }; + this.issues = []; + } + + analyze(): NarrowingIssue[] { + this.issues = []; + + for (const node of this.graph.nodes.values()) { + this.analyzeNode(node); + } + + return this.issues; + } + + private analyzeNode(node: GraphNode): void { + const metadata = node.metadata as Record; + const typeAnnotation = metadata.typeAnnotation as string | undefined; + + if (typeAnnotation) { + this.checkExcessiveNesting(node, typeAnnotation); + this.checkNeverType(node, typeAnnotation); + this.checkConditionalNarrowing(node, typeAnnotation); + this.checkTypeAssertionNarrowing(node, typeAnnotation); + } + + this.checkOptionalNarrowing(node); + } + + private checkExcessiveNesting(node: GraphNode, typeAnnotation: string): void { + let depth = 0; + let maxDepth = 0; + + for (const char of typeAnnotation) { + if (char === '(' || char === '[' || char === '{') { + depth++; + if (depth > maxDepth) { + maxDepth = depth; + } + } else if (char === ')' || char === ']' || char === '}') { + depth--; + } + } + + if (maxDepth >= this.options.maxNestingDepth!) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: `Type has excessive nesting depth of ${maxDepth}`, + originalType: typeAnnotation, + narrowedType: `Depth: ${maxDepth}`, + suggestion: 'Consider breaking into smaller, named types' + }); + } + } + + private checkNeverType(node: GraphNode, typeAnnotation: string): void { + if (typeAnnotation.includes('never')) { + if (this.options.warnOnNever) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: 'Type contains "never" which indicates no possible values', + originalType: typeAnnotation, + suggestion: 'Verify that this type is intentionally unreachable' + }); + } + } + } + + private checkConditionalNarrowing(node: GraphNode, typeAnnotation: string): void { + if (typeAnnotation.includes('extends') && typeAnnotation.includes('?')) { + const ternaryCount = (typeAnnotation.match(/\?/g) || []).length; + + if (ternaryCount > 2) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: `Type has ${ternaryCount} nested conditional expressions`, + originalType: typeAnnotation, + suggestion: 'Consider extracting conditional logic into helper types or functions' + }); + } + + if (this.options.strictConditional && ternaryCount > 0) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: 'Conditional type detected in strict mode', + originalType: typeAnnotation, + suggestion: 'Review conditional type for potential simplification' + }); + } + } + } + + private checkTypeAssertionNarrowing(node: GraphNode, typeAnnotation: string): void { + if (typeAnnotation.includes('as ')) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: 'Type contains type assertion which bypasses type checking', + originalType: typeAnnotation, + suggestion: 'Consider using type guards or overloads instead of assertions' + }); + } + } + + private checkOptionalNarrowing(node: GraphNode): void { + const metadata = node.metadata as Record; + const properties = metadata.properties as unknown[] | undefined; + + if (properties && Array.isArray(properties)) { + let optionalCount = 0; + let requiredCount = 0; + + for (const prop of properties) { + if (prop && typeof prop === 'object') { + const propObj = prop as { optional?: boolean }; + if (propObj.optional) { + optionalCount++; + } else { + requiredCount++; + } + } + } + + if (optionalCount > 0 && requiredCount > 0) { + const ratio = optionalCount / (optionalCount + requiredCount); + if (ratio > 0.7) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: `${optionalCount} of ${optionalCount + requiredCount} properties are optional (${Math.round(ratio * 100)}%)`, + originalType: `${requiredCount} required, ${optionalCount} optional`, + suggestion: 'Consider if the type has too many optional properties' + }); + } + } + } + } + + getIssues(): NarrowingIssue[] { + return this.issues; + } + + getIssueCount(): number { + return this.issues.length; + } + + filterByFilePath(filePath: string): NarrowingIssue[] { + return this.issues.filter((issue) => issue.filePath === filePath); + } + + filterByTypeName(typeName: string): NarrowingIssue[] { + return this.issues.filter((issue) => issue.typeName === typeName); + } + + getComplexityScore(): number { + let score = 0; + + for (const issue of this.issues) { + if (issue.description.includes('nesting')) { + score += 2; + } else if (issue.description.includes('conditional')) { + score += 1; + } else { + score += 0.5; + } + } + + return score; + } +} + +export function analyzeTypeNarrowing( + graph: DependencyGraph, + options?: NarrowingAnalysisOptions +): NarrowingIssue[] { + const analyzer = new TypeNarrowingAnalyzer(graph, options); + return analyzer.analyze(); +}