Add analyzer modules
Some checks failed
CI / test (push) Failing after 6s

This commit is contained in:
2026-01-30 00:58:58 +00:00
parent d7bf2f0a0a
commit 55b2e0801e

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