This commit is contained in:
180
src/analyzers/typeWidening.ts
Normal file
180
src/analyzers/typeWidening.ts
Normal file
@@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, number> {
|
||||
const counts: Record<string, number> = {};
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user