UNPKG

@sun-asterisk/sunlint

Version:

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

288 lines (245 loc) 8.31 kB
/** * C024 Symbol-based Analyzer - Advanced Do not scatter hardcoded constants throughout the logic * Purpose: The rule prevents scattering hardcoded constants throughout the logic. Instead, constants should be defined in a single place to improve maintainability and readability. */ const { SyntaxKind } = require('ts-morph'); class C024SymbolBasedAnalyzer { constructor(semanticEngine = null) { this.ruleId = 'C024'; this.ruleName = 'Error Scatter hardcoded constants throughout the logic (Symbol-Based)'; this.semanticEngine = semanticEngine; this.verbose = false; this.safeStrings = ["UNKNOWN", "N/A"]; // allowlist of special fallback values } async initialize(semanticEngine = null) { if (semanticEngine) { this.semanticEngine = semanticEngine; } this.verbose = semanticEngine?.verbose || false; if (process.env.SUNLINT_DEBUG) { console.log(`🔧 [C024 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`); } } async analyzeFileBasic(filePath, options = {}) { // This is the main entry point called by the hybrid analyzer return await this.analyzeFileWithSymbols(filePath, options); } async analyzeFileWithSymbols(filePath, options = {}) { const violations = []; // Enable verbose mode if requested const verbose = options.verbose || this.verbose; if (!this.semanticEngine?.project) { if (verbose) { console.warn('[C024 Symbol-Based] No semantic engine available, skipping analysis'); } return violations; } if (verbose) { console.log(`🔍 [C024 Symbol-Based] Starting analysis for ${filePath}`); } try { // skip ignored files if (this.isIgnoredFile(filePath)) { if (verbose) { console.log(`🔍 [C024 Symbol-Based] Skipping ignored file: ${filePath}`); } return violations; } const sourceFile = this.semanticEngine.project.getSourceFile(filePath); if (!sourceFile) { return violations; } // skip constants files if (this.isConstantsFile(filePath)) return violations; // Detect hardcoded constants sourceFile.forEachDescendant((node) => { this.checkLiterals(node, sourceFile, violations); this.checkConstDeclaration(node, sourceFile, violations); this.checkStaticReadonly(node, sourceFile, violations); }); if (verbose) { console.log(`🔍 [C024 Symbol-Based] Total violations found: ${violations.length}`); } return violations; } catch (error) { if (verbose) { console.warn(`[C024 Symbol-Based] Analysis failed for ${filePath}:`, error.message); } return violations; } } // --- push violation object --- pushViolation(violations, node, filePath, text, message) { violations.push({ ruleId: this.ruleId, severity: "warning", message: message || `Hardcoded constant found: "${text}"`, source: this.ruleId, file: filePath, line: node.getStartLineNumber(), column: node.getStart() - node.getStartLinePos(), description: "[SYMBOL-BASED] Hardcoded constants should be defined in a single place to improve maintainability.", suggestion: "Define constants in a dedicated file or section", category: "constants", }); } // --- check literals like "ADMIN", 123, true --- checkLiterals(node, sourceFile, violations) { const kind = node.getKind(); if ( kind === SyntaxKind.StringLiteral || kind === SyntaxKind.NumericLiteral ) { const text = node.getText().replace(/['"`]/g, ""); // strip quotes if (this.isAllowedLiteral(node, text)) return; this.pushViolation( violations, node, sourceFile.getFilePath(), node.getText() ); } } // --- check const declarations outside constants.ts --- checkConstDeclaration(node, sourceFile, violations) { const kind = node.getKind(); if (kind === SyntaxKind.VariableDeclaration) { const parentKind = node.getParent()?.getKind(); // Skip detection for `for ... of` loop variable const loopAncestor = node.getFirstAncestor((ancestor) => { const kind = ancestor.getKind?.(); return ( kind === SyntaxKind.ForOfStatement || kind === SyntaxKind.ForInStatement || kind === SyntaxKind.ForStatement || kind === SyntaxKind.WhileStatement || kind === SyntaxKind.DoStatement || kind === SyntaxKind.SwitchStatement ); }); if (loopAncestor) { return; // skip for all loop/switch contexts, no matter how nested } if ( parentKind === SyntaxKind.VariableDeclarationList && node.getParent().getDeclarationKind() === "const" && !node.getInitializer() ) { this.pushViolation( violations, node, sourceFile.getFilePath(), node.getName(), `Const declaration "${node.getName()}" should be moved into constants file` ); } } } // --- check static readonly properties inside classes --- checkStaticReadonly(node, sourceFile, violations) { const kind = node.getKind(); if (kind === SyntaxKind.PropertyDeclaration) { const modifiers = node.getModifiers().map((m) => m.getText()); if (modifiers.includes("static") && modifiers.includes("readonly")) { this.pushViolation( violations, node, sourceFile.getFilePath(), node.getName(), `Static readonly property "${node.getName()}" should be moved into constants file` ); } } } // --- helper: allow safe literals --- isAllowedLiteral(node, text) { const parent = node.getParent(); // 1 Skip imports/exports if (parent?.getKind() === SyntaxKind.ImportDeclaration) return true; if (parent?.getKind() === SyntaxKind.ExportDeclaration) return true; // 2 Skip literals that are inside call expressions (direct or nested) if ( parent?.getKind() === SyntaxKind.CallExpression || parent?.getFirstAncestorByKind(SyntaxKind.CallExpression) ) { return true; } if ( parent?.getKind() === SyntaxKind.ElementAccessExpression && parent.getArgumentExpression?.() === node ) { return true; // skip array/object key } // 3 Allow short strings if (typeof text === "string" && text.length <= 1) return true; // 4 Allow sentinel numbers if (text === "0" || text === "1" || text === "-1") return true; // 5 Allow known safe strings (like "UNKNOWN") if (this.safeStrings.includes(text)) return true; // 6 Allow SQL-style placeholders (:variable) inside string/template if (typeof text === "string" && /:\w+/.test(text)) { return true; } return false; } // helper to check if file is a constants file isConstantsFile(filePath) { const lower = filePath.toLowerCase(); // common suffixes/patterns for utility or structural files const ignoredSuffixes = [ ".constants.ts", ".const.ts", ".enum.ts", ".interface.ts", ".response.ts", ".request.ts", ".res.ts", ".req.ts", ]; // 1 direct suffix match if (ignoredSuffixes.some(suffix => lower.endsWith(suffix))) { return true; } // 2 matches dto.xxx.ts (multi-dot dto files) if (/\.dto\.[^.]+\.ts$/.test(lower)) { return true; } // 3 matches folder-based conventions if ( lower.includes("/constants/") || lower.includes("/enums/") || lower.includes("/interfaces/") ) { return true; } return false; } isIgnoredFile(filePath) { const ignoredPatterns = [ /\.test\./i, /\.tests\./i, /\.spec\./i, /\.mock\./i, /\.css$/i, /\.scss$/i, /\.html$/i, /\.json$/i, /\.md$/i, /\.svg$/i, /\.png$/i, /\.jpg$/i, /\.jpeg$/i, /\.gif$/i, /\.bmp$/i, /\.ico$/i, /\.lock$/i, /\.log$/i, /\/test\//i, /\/tests\//i, /\/spec\//i ]; return ignoredPatterns.some((regex) => regex.test(filePath)); } } module.exports = C024SymbolBasedAnalyzer;