Add analyzer modules
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-30 00:58:58 +00:00
parent 832d62f25c
commit d7bf2f0a0a

View 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();
}