UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

417 lines (350 loc) 13.5 kB
/** * Symbol-based analyzer for C040 - Centralized Validation Logic Analysis * Purpose: Use AST + Data Flow to detect scattered validation logic across layers */ const { SyntaxKind } = require('ts-morph'); class C040SymbolBasedAnalyzer { constructor(semanticEngine = null) { this.ruleId = 'C040'; this.ruleName = 'Centralized Validation Logic (Symbol-Based)'; this.semanticEngine = semanticEngine; this.verbose = false; // Validation patterns to detect this.validationPatterns = { functionNames: [ 'validate', 'check', 'ensure', 'verify', 'isValid', 'hasValid', 'validateInput', 'checkInput', 'validateData', 'sanitize' ], frameworks: ['zod', 'joi', 'yup', 'class-validator', 'ajv'], errorTypes: ['ValidationError', 'BadRequest', 'InvalidInput', 'TypeError'] }; // Layer detection patterns this.layerPatterns = { controller: /controller|route|handler/i, service: /service|business|logic/i, repository: /repository|dao|data/i, validator: /validator|validation|schema/i }; } async initialize(semanticEngine = null) { if (semanticEngine) { this.semanticEngine = semanticEngine; } this.verbose = semanticEngine?.verbose || false; if (this.verbose) { console.log(`[DEBUG] 🔧 C040 Symbol-Based: Analyzer initialized`); } } async analyze(files, language, options = {}) { const violations = []; if (!this.semanticEngine?.project) { if (this.verbose) { console.warn('[C040 Symbol-Based] No semantic engine available, skipping analysis'); } return violations; } for (const filePath of files) { try { const fileViolations = await this.analyzeFile(filePath); violations.push(...fileViolations); } catch (error) { if (this.verbose) { console.warn(`[C040] Analysis failed for ${filePath}:`, error.message); } } } return violations; } async analyzeFile(filePath) { const violations = []; try { const sourceFile = this.semanticEngine.project.getSourceFile(filePath); if (!sourceFile) { if (this.verbose) { console.log(`[C040] Source file not found in project: ${filePath}`); } return violations; } if (this.verbose) { console.log(`[C040] Analyzing file: ${filePath}`); } // Detect layer from file path const layer = this.detectLayer(filePath); // Find validation patterns const validationScents = this.findValidationPatterns(sourceFile); if (validationScents.length > 0) { // Check if validation is in wrong layer if (layer === 'controller' || layer === 'service') { const complexValidations = validationScents.filter(v => v.isComplex); const businessValidations = complexValidations.filter(v => v.context === 'business-validation' ); if (businessValidations.length > 0) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: `Found ${businessValidations.length} complex business validation pattern(s) in ${layer} layer. Consider moving to validators.`, file: filePath, line: businessValidations[0].line, column: businessValidations[0].column, details: { layer, validationCount: businessValidations.length, patterns: businessValidations.map(v => v.pattern), suggestion: 'Move validation logic to dedicated validator classes', ruleName: this.ruleName } }); } } } } catch (error) { if (this.verbose) { console.warn(`[C040] Error analyzing ${filePath}:`, error.message); } } return violations; } detectLayer(filePath) { const path = filePath.toLowerCase(); for (const [layer, pattern] of Object.entries(this.layerPatterns)) { if (pattern.test(path)) { return layer; } } return 'unknown'; } findValidationPatterns(sourceFile) { const patterns = []; // 1. Find business validation functions (semantic analysis) sourceFile.getFunctions().forEach(func => { const validationInfo = this.analyzeValidationFunction(func); if (validationInfo) { patterns.push(validationInfo); } }); // 2. Find class methods with validation logic sourceFile.getClasses().forEach(cls => { cls.getMethods().forEach(method => { const validationInfo = this.analyzeValidationMethod(method, cls); if (validationInfo) { patterns.push(validationInfo); } }); }); // 3. Find validation decorators and frameworks usage sourceFile.getClasses().forEach(cls => { cls.getMethods().forEach(method => { const decoratorInfo = this.analyzeValidationDecorators(method); if (decoratorInfo) { patterns.push(decoratorInfo); } }); }); return patterns; } /** * Analyze if a function contains actual business validation logic */ analyzeValidationFunction(func) { const name = func.getName(); const body = func.getBodyText(); // Skip if it's infrastructure/utility function if (this.isInfrastructureFunction(func, name, body)) { return null; } // Check for actual validation patterns in body const validationSignals = this.countValidationSignals(body); if (validationSignals.score > 2) { // Threshold for real validation return { type: 'function', pattern: name, line: func.getStartLineNumber(), column: func.getStart(), isComplex: validationSignals.score > 5, signals: validationSignals.patterns, context: 'business-validation' }; } return null; } /** * Analyze if a method contains business validation logic */ analyzeValidationMethod(method, parentClass) { const methodName = method.getName(); const className = parentClass.getName(); const body = method.getBodyText(); // Skip infrastructure methods if (this.isInfrastructureMethod(method, methodName, body, className)) { return null; } // Check for validation patterns const validationSignals = this.countValidationSignals(body); // Balanced threshold to catch meaningful validation if (validationSignals.score > 3) { return { type: 'method', pattern: `${className}.${methodName}`, line: method.getStartLineNumber(), column: method.getStart(), isComplex: validationSignals.score > 6, signals: validationSignals.patterns, context: 'business-validation' }; } return null; } /** * Check for validation framework decorators */ analyzeValidationDecorators(method) { const decorators = method.getDecorators(); const validationDecorators = decorators.filter(dec => { const name = dec.getName(); return ['IsEmail', 'IsString', 'IsNumber', 'Min', 'Max', 'Length', 'Matches'].includes(name); }); if (validationDecorators.length > 0) { return { type: 'decorator', pattern: validationDecorators.map(d => d.getName()).join(', '), line: method.getStartLineNumber(), column: method.getStart(), isComplex: true, context: 'framework-validation' }; } return null; } /** * Check if function/method is infrastructure/utility rather than business validation */ isInfrastructureFunction(func, name, body) { // Health check patterns if (name === 'check' && body.includes('health')) return true; if (name.includes('health') || name.includes('Health')) return true; // Crypto/hash utilities if (name === 'check' && (body.includes('bcrypt') || body.includes('hash') || body.includes('crypto'))) return true; if (name.includes('hash') && body.includes('bcrypt')) return true; // Authentication utilities (not business validation) if (name.includes('verify') && body.includes('jwt')) return true; if (name.includes('verify') && body.includes('token')) return true; // Simple getters/setters if (name.startsWith('get') || name.startsWith('set')) return true; return false; } /** * Check if method is infrastructure/utility */ isInfrastructureMethod(method, methodName, body, className) { // Infrastructure keywords in method/class names const infraKeywords = [ 'health', 'hash', 'auth', 'login', 'register', 'encrypt', 'decrypt', 'token', 'session', 'cookie', 'cache', 'log', 'monitor', 'metric', 's3', 'aws', 'storage', 'upload', 'download', 'file', 'config', 'connection', 'database', 'db', 'migration', 'seed', 'error', 'exception' ]; const lowerMethodName = methodName.toLowerCase(); const lowerClassName = className.toLowerCase(); // Check method/class names if (infraKeywords.some(keyword => lowerMethodName.includes(keyword) || lowerClassName.includes(keyword) )) { return true; } // Controller methods that just delegate (not business validation) if (className.toLowerCase().includes('controller')) { // Simple delegation patterns const delegationPatterns = [ /return\s+await\s+this\.\w+\.\w+\(/, /return\s+this\.\w+\.\w+\(/, /^\s*return\s+await/m ]; if (delegationPatterns.some(pattern => pattern.test(body))) { return true; } } // Check method body for infrastructure patterns const infraPatterns = [ /bcrypt|crypto|jwt/i, /\.hash\(|\.compare\(/i, /redis|cache/i, /aws|s3|bucket/i, /winston|logger/i, /fileExist|checkExist/i, /uploadFile|downloadFile/i, /HttpStatus\./i // Pure HTTP status handling ]; if (infraPatterns.some(pattern => pattern.test(body))) { return true; } // Call parent function check return this.isInfrastructureFunction(method, methodName, body); } /** * Count actual validation signals in code body using semantic analysis */ countValidationSignals(body) { let score = 0; const patterns = []; // Business validation patterns (weighted scoring) const validationIndicators = [ // High weight - clear business validation { pattern: /throw new.*ValidationError/gi, weight: 5, name: 'ValidationError' }, { pattern: /throw new.*BadRequest/gi, weight: 4, name: 'BadRequestError' }, { pattern: /throw new.*InvalidInput/gi, weight: 4, name: 'InvalidInput' }, // Medium weight - input validation { pattern: /if\s*\(\s*!.*\)\s*{[^}]*throw/gi, weight: 3, name: 'conditional-validation' }, { pattern: /\.length\s*[<>]=?\s*\d+.*throw/gi, weight: 3, name: 'length-validation' }, { pattern: /typeof.*!==.*throw/gi, weight: 3, name: 'type-validation' }, // Framework validation { pattern: /zod\.|joi\.|yup\./gi, weight: 4, name: 'validation-framework' }, { pattern: /class-validator/gi, weight: 4, name: 'class-validator' }, // Business rule validation { pattern: /validate[A-Z][a-zA-Z]*\(/gi, weight: 3, name: 'validate-method' }, { pattern: /check[A-Z][a-zA-Z]*\(/gi, weight: 2, name: 'check-method' }, { pattern: /ensure[A-Z][a-zA-Z]*\(/gi, weight: 3, name: 'ensure-method' }, // Low weight - might be utility { pattern: /\.test\(/gi, weight: 1, name: 'regex-test' }, { pattern: /\.match\(/gi, weight: 1, name: 'string-match' } ]; validationIndicators.forEach(indicator => { const matches = body.match(indicator.pattern); if (matches) { score += matches.length * indicator.weight; patterns.push(`${indicator.name} (${matches.length})`); } }); // Penalty for infrastructure patterns (increased penalties) const infrastructurePatterns = [ { pattern: /bcrypt|crypto|jwt/gi, penalty: -4 }, { pattern: /health|Health/gi, penalty: -6 }, { pattern: /\.hash\(|\.compare\(/gi, penalty: -3 }, { pattern: /HttpStatus\.|\.status\(/gi, penalty: -2 }, { pattern: /aws|s3|bucket/gi, penalty: -4 }, { pattern: /fileExist|checkExist/gi, penalty: -3 }, { pattern: /return\s+await\s+this\./gi, penalty: -2 }, // Delegation pattern { pattern: /BadRequest.*S3/gi, penalty: -5 }, // S3 error handling { pattern: /error.*message/gi, penalty: -1 } // Generic error handling ]; infrastructurePatterns.forEach(infra => { const matches = body.match(infra.pattern); if (matches) { score += matches.length * infra.penalty; patterns.push(`infrastructure-penalty (${matches.length})`); } }); return { score: Math.max(0, score), patterns }; } isValidationFunction(name) { return this.validationPatterns.functionNames.some(pattern => name.toLowerCase().includes(pattern.toLowerCase()) ); } isValidationError(text) { return this.validationPatterns.errorTypes.some(errorType => text.includes(errorType) ); } } module.exports = C040SymbolBasedAnalyzer;