/** * @angular-modernizer/plugin-angular - Type Safety Analysis Rule * * Detects four categories of type safety violations across all TypeScript code: * 1. Explicit `any` types on properties, parameters, and return types * 2. Untyped function/method parameters (no type annotation, no default, not rest) * 3. Missing explicit return type annotations on functions and class methods * 4. Class properties with no type annotation and no primitive literal initializer * * Scope: * - Analyzes standalone `FunctionDeclaration` nodes and class `MethodDeclaration` nodes * - Analyzes all `Parameter` and `PropertyDeclaration` descendants * - Skips `.d.ts` declaration files entirely * * Known Limitations: * - Arrow functions (`const fn = () => {}`) and function expressions are NOT scanned * for missing return types. Only `FunctionDeclaration` and `MethodDeclaration` are * analyzed. This is intentional to keep the rule focused on explicit declarations. * * Overlap with MissingReturnTypeRule: * - Both rules flag missing return types on class methods. * - `MissingReturnTypeRule` is Angular-specific: it skips Angular lifecycle hooks * (ngOnInit, ngOnDestroy, etc.) and reports under the Angular framework lens. * - `TypeSafetyAnalysisRule` is general TypeScript type safety: no lifecycle hook * skipping, broader scope including standalone functions. * - Double-reporting is intentional; users can disable one rule if preferred. * * @example * ```typescript * const rule = new TypeSafetyAnalysisRule(); * const results = await rule.analyze({ sourceFile }); * ``` */ import { SyntaxKind, type SourceFile } from 'ts-morph'; import type { AnalysisRule, AnalysisContext, AnalysisResult, } from '@angular-modernizer/plugin-system'; export class TypeSafetyAnalysisRule implements AnalysisRule { public readonly id = 'angular:type-safety'; public readonly name = 'Type Safety'; public readonly description = 'Detects type safety violations including explicit any, untyped parameters, untyped return types, and untyped class properties'; public readonly severity = 'warning'; public readonly category = 'angular-typescript'; public readonly tags = ['angular', 'typescript', 'type-safety', 'any']; async analyze(context: AnalysisContext): Promise { const { sourceFile } = context; if (sourceFile.getFilePath().endsWith('.d.ts')) { return []; } return [ ...this.detectExplicitAny(sourceFile, sourceFile.getFilePath()), ...this.detectUntypedParameters(sourceFile, sourceFile.getFilePath()), ...this.detectUntypedReturnTypes(sourceFile, sourceFile.getFilePath()), ...this.detectUntypedProperties(sourceFile, sourceFile.getFilePath()), ]; } private detectExplicitAny( sourceFile: SourceFile, filePath: string, ): AnalysisResult[] { const violations: AnalysisResult[] = []; const anyNodes = sourceFile.getDescendantsOfKind(SyntaxKind.AnyKeyword); for (const node of anyNodes) { const parent = node.getParent(); let message: string; const asProp = parent?.asKind(SyntaxKind.PropertyDeclaration); const asParam = parent?.asKind(SyntaxKind.Parameter); const asMethod = parent?.asKind(SyntaxKind.MethodDeclaration); const asFn = parent?.asKind(SyntaxKind.FunctionDeclaration); if (asProp) { message = `Property '${asProp.getName()}' uses explicit 'any' type`; } else if (asParam) { message = `Parameter '${asParam.getName()}' uses explicit 'any' type`; } else if (asMethod) { message = `Return type of '${asMethod.getName() ?? ''}' uses explicit 'any'`; } else if (asFn) { message = `Return type of '${asFn.getName() ?? ''}' uses explicit 'any'`; } else { message = "Explicit 'any' type usage detected"; } violations.push({ ruleId: this.id, filePath, message, line: node.getStartLineNumber(), column: node.getStart() - node.getStartLinePos(), suggestedFix: 'Replace any with a specific type or unknown', metadata: { violationType: 'explicit-any', confidence: 0.95, suggestedFix: 'Replace any with a specific type or unknown', }, }); } return violations; } private detectUntypedParameters( sourceFile: SourceFile, filePath: string, ): AnalysisResult[] { const violations: AnalysisResult[] = []; const params = sourceFile.getDescendantsOfKind(SyntaxKind.Parameter); for (const param of params) { if (param.getTypeNode()) { continue; } if (param.isRestParameter()) { continue; } if (param.getInitializer()) { continue; } violations.push({ ruleId: this.id, filePath, message: `Parameter '${param.getName()}' is missing a type annotation`, line: param.getStartLineNumber(), column: param.getStart() - param.getStartLinePos(), suggestedFix: 'Replace any with a specific type or unknown', metadata: { violationType: 'untyped-parameter', confidence: 0.85, suggestedFix: 'Replace any with a specific type or unknown', }, }); } return violations; } private detectUntypedReturnTypes( sourceFile: SourceFile, filePath: string, ): AnalysisResult[] { const violations: AnalysisResult[] = []; const functions = sourceFile.getDescendantsOfKind( SyntaxKind.FunctionDeclaration, ); for (const fn of functions) { if (fn.getReturnTypeNode()) { continue; } violations.push({ ruleId: this.id, filePath, message: `Function '${fn.getName() ?? ''}' is missing an explicit return type annotation`, line: fn.getStartLineNumber(), column: fn.getStart() - fn.getStartLinePos(), suggestedFix: 'Replace any with a specific type or unknown', metadata: { violationType: 'untyped-return', confidence: 0.8, suggestedFix: 'Replace any with a specific type or unknown', }, }); } const methods = sourceFile.getDescendantsOfKind( SyntaxKind.MethodDeclaration, ); for (const method of methods) { if (method.getReturnTypeNode()) { continue; } violations.push({ ruleId: this.id, filePath, message: `Method '${method.getName() ?? ''}' is missing an explicit return type annotation`, line: method.getStartLineNumber(), column: method.getStart() - method.getStartLinePos(), suggestedFix: 'Replace any with a specific type or unknown', metadata: { violationType: 'untyped-return', confidence: 0.8, suggestedFix: 'Replace any with a specific type or unknown', }, }); } return violations; } private detectUntypedProperties( sourceFile: SourceFile, filePath: string, ): AnalysisResult[] { const violations: AnalysisResult[] = []; const primitiveLiteralKinds = new Set([ SyntaxKind.StringLiteral, SyntaxKind.NumericLiteral, SyntaxKind.TrueKeyword, SyntaxKind.FalseKeyword, ]); const properties = sourceFile.getDescendantsOfKind( SyntaxKind.PropertyDeclaration, ); for (const prop of properties) { if (prop.getTypeNode()) { continue; } const initializer = prop.getInitializer(); if (initializer && primitiveLiteralKinds.has(initializer.getKind())) { continue; } violations.push({ ruleId: this.id, filePath, message: `Property '${prop.getName()}' is missing a type annotation`, line: prop.getStartLineNumber(), column: prop.getStart() - prop.getStartLinePos(), suggestedFix: 'Replace any with a specific type or unknown', metadata: { violationType: 'untyped-property', confidence: 0.85, suggestedFix: 'Replace any with a specific type or unknown', }, }); } return violations; } }