diff --git a/src/analyzers/typeWidening.ts b/src/analyzers/typeWidening.ts new file mode 100644 index 0000000..34f59c7 --- /dev/null +++ b/src/analyzers/typeWidening.ts @@ -0,0 +1,180 @@ +import { DependencyGraph, WideningIssue, GraphNode } from '../types'; + +export interface WideningAnalysisOptions { + strictMode?: boolean; + warnOnAny?: boolean; + maxUnionMembers?: number; +} + +export class TypeWideningAnalyzer { + private graph: DependencyGraph; + private options: WideningAnalysisOptions; + private issues: WideningIssue[]; + + constructor(graph: DependencyGraph, options: WideningAnalysisOptions = {}) { + this.graph = graph; + this.options = { + strictMode: options.strictMode ?? false, + warnOnAny: options.warnOnAny ?? true, + maxUnionMembers: options.maxUnionMembers ?? 5 + }; + this.issues = []; + } + + analyze(): WideningIssue[] { + 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.checkUnionWidening(node, typeAnnotation); + this.checkIntersectionWidening(node, typeAnnotation); + this.checkAnyWidening(node, typeAnnotation); + this.checkUnknownWidening(node, typeAnnotation); + } + + this.checkPropertyWidening(node); + } + + private checkUnionWidening(node: GraphNode, typeAnnotation: string): void { + const unionParts = typeAnnotation.split(' | '); + + if (unionParts.length > this.options.maxUnionMembers!) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: `Union type has ${unionParts.length} members, which may indicate excessive type widening`, + originalType: unionParts.slice(0, 3).join(' | ') + (unionParts.length > 3 ? ' ...' : ''), + widenedType: typeAnnotation, + suggestion: 'Consider using discriminated unions or breaking into separate types' + }); + } + + if (this.options.strictMode && unionParts.length > 3) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: 'Large union type detected in strict mode', + originalType: typeAnnotation, + suggestion: 'Consider refactoring into more specific types' + }); + } + } + + private checkIntersectionWidening(node: GraphNode, typeAnnotation: string): void { + const intersectionParts = typeAnnotation.split(' & '); + + if (intersectionParts.length > 4) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: `Intersection type has ${intersectionParts.length} members`, + originalType: intersectionParts.slice(0, 3).join(' & ') + (intersectionParts.length > 3 ? ' ...' : ''), + suggestion: 'Consider extracting common properties into a base type' + }); + } + } + + private checkAnyWidening(node: GraphNode, typeAnnotation: string): void { + if (typeAnnotation.includes('any')) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: 'Type uses "any" which disables type checking', + originalType: typeAnnotation, + suggestion: 'Replace with specific types or unknown if type is truly dynamic' + }); + } + } + + private checkUnknownWidening(node: GraphNode, typeAnnotation: string): void { + if (typeAnnotation.includes('unknown')) { + this.issues.push({ + nodeId: node.id, + typeName: node.label, + filePath: node.filePath, + lineNumber: node.lineNumber, + description: 'Type uses "unknown" which requires type guards', + originalType: typeAnnotation, + suggestion: 'Ensure proper type guards are used when narrowing' + }); + } + } + + private checkPropertyWidening(node: GraphNode): void { + const metadata = node.metadata as Record; + const properties = metadata.properties as unknown[] | undefined; + + if (properties && Array.isArray(properties)) { + for (const prop of properties) { + if (prop && typeof prop === 'object') { + const propObj = prop as { type?: string }; + if (propObj.type && propObj.type.includes('undefined')) { + const nodeId = node.id; + const lineNumber = node.lineNumber; + + this.issues.push({ + nodeId, + typeName: `${node.label}.property`, + filePath: node.filePath, + lineNumber, + description: 'Property type includes undefined which may cause widening', + originalType: propObj.type, + suggestion: 'Consider using optional properties (?) instead of union with undefined' + }); + } + } + } + } + } + + getIssues(): WideningIssue[] { + return this.issues; + } + + getIssueCount(): number { + return this.issues.length; + } + + getIssueCountBySeverity(): Record { + const counts: Record = {}; + for (const issue of this.issues) { + const severity = issue.description.includes('any') ? 'high' : 'medium'; + counts[severity] = (counts[severity] || 0) + 1; + } + return counts; + } + + filterByFilePath(filePath: string): WideningIssue[] { + return this.issues.filter((issue) => issue.filePath === filePath); + } + + filterByTypeName(typeName: string): WideningIssue[] { + return this.issues.filter((issue) => issue.typeName === typeName); + } +} + +export function analyzeTypeWidening( + graph: DependencyGraph, + options?: WideningAnalysisOptions +): WideningIssue[] { + const analyzer = new TypeWideningAnalyzer(graph, options); + return analyzer.analyze(); +}