This commit is contained in:
212
src/analyzers/typeNarrowing.ts
Normal file
212
src/analyzers/typeNarrowing.ts
Normal file
@@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user