UNPKG

@sun-asterisk/sunlint

Version:

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

432 lines (360 loc) 13.5 kB
/** * AST-based C043 Analyzer * Mirrors ESLint's sophisticated implementation for perfect accuracy * Leverages SunLint's existing AST infrastructure */ const astRegistry = require('../../../core/ast-modules'); const fs = require('fs'); class C043NoConsoleOrPrintAnalyzer { constructor() { this.ruleId = 'C043'; this.ruleName = 'No Console Or Print'; this.description = 'Do not use console.log or print in production code'; this.severity = 'warning'; this.astRegistry = astRegistry; // Configuration mirroring ESLint's C043 rule this.config = { allowedMethods: new Set(['error', 'warn']), allowInDevelopment: true, allowInTests: true, testFilePatterns: [ '.test.', '.spec.', '__tests__', '/test/', '/tests/', '.test.ts', '.test.js', '.spec.ts', '.spec.js', 'test.tsx', 'spec.tsx', '.stories.', '.story.' // Include stories files like ESLint ], developmentPatterns: [ '.dev.', '.development.', '.debug.', '/dev/', '/development/' ], developmentFlags: new Set([ '__DEV__', 'DEBUG', 'process.env.NODE_ENV', 'process.env.ENVIRONMENT', 'process.env.ENABLE_LOGGING', 'FEATURES.debug', 'BUILD_TYPE' ]), consoleMethods: new Set([ 'log', 'info', 'debug', 'trace', 'dir', 'dirxml', 'table', 'count', 'countReset', 'time', 'timeEnd', 'timeLog', 'assert', 'clear', 'group', 'groupCollapsed', 'groupEnd', 'profile', 'profileEnd', 'timeStamp' ]), forbiddenFunctions: ['print', 'alert', 'confirm', 'prompt'] }; } async analyze(files, language, config) { const violations = []; // Batch processing to avoid memory issues const batchSize = 100; const totalFiles = files.length; for (let i = 0; i < totalFiles; i += batchSize) { const batch = files.slice(i, i + batchSize); for (const filePath of batch) { try { // Skip test files if configured if (this.config.allowInTests && this.isTestFile(filePath)) { continue; } // Skip development files if configured if (this.config.allowInDevelopment && this.isDevelopmentFile(filePath)) { continue; } const fileContent = fs.readFileSync(filePath, 'utf8'); // Skip empty files or very large files (>1MB) if (!fileContent.trim() || fileContent.length > 1024 * 1024) { continue; } const fileLanguage = this.getLanguageFromPath(filePath); // Use regex-based analysis for now (AST can be added later) // AST parsing can be slow on large files, so use fast regex approach const regexViolations = await this.analyzeFileWithRegex(filePath, fileContent, language, config); violations.push(...regexViolations); } catch (error) { // Skip problematic files silently to avoid stopping entire analysis console.warn(`C043 skipping ${filePath}: ${error.message}`); } } // Give Node.js a chance to breathe between batches if (i + batchSize < totalFiles) { await new Promise(resolve => setImmediate(resolve)); } } return violations; } getLanguageFromPath(filePath) { const ext = filePath.split('.').pop().toLowerCase(); const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'mjs': 'javascript', 'cjs': 'javascript' }; return languageMap[ext] || 'javascript'; } analyzeAST(ast, filePath, fileContent) { const violations = []; const lines = fileContent.split('\n'); // Define visitor for AST traversal const visitor = { CallExpression: (node) => { // Check for console method calls if (this.isConsoleCall(node)) { const methodName = this.getConsoleMethodName(node); // Skip allowed methods (error, warn) if (this.config.allowedMethods.has(methodName)) { return; } // Check if in development context if (this.config.allowInDevelopment && this.isInDevelopmentContext(node, ast)) { return; } // Create violation const location = this.getNodeLocation(node); if (location && location.line <= lines.length) { violations.push({ ruleId: this.ruleId, severity: this.severity, message: `Do not use console.${methodName}() in production code. Use proper logging instead.`, filePath: filePath, line: location.line, column: location.column, source: lines[location.line - 1]?.trim() || '', suggestion: `Consider using a proper logging library (logger.${methodName}())` }); } } // Check for forbidden functions (print, alert, etc.) if (this.isForbiddenFunctionCall(node)) { const functionName = this.getFunctionName(node); const location = this.getNodeLocation(node); if (location && location.line <= lines.length) { violations.push({ ruleId: this.ruleId, severity: this.severity, message: `Do not use ${functionName}() in production code. Use proper logging or UI notifications instead.`, filePath: filePath, line: location.line, column: location.column, source: lines[location.line - 1]?.trim() || '', suggestion: `Consider using a logging library or proper UI notification system` }); } } } }; // Traverse AST this.traverseAST(ast, visitor); return violations; } // AST Helper Methods isConsoleCall(node) { return node.type === 'CallExpression' && node.callee && node.callee.type === 'MemberExpression' && node.callee.object && node.callee.object.type === 'Identifier' && node.callee.object.name === 'console' && node.callee.property && this.config.consoleMethods.has(node.callee.property.name); } getConsoleMethodName(node) { if (node.callee && node.callee.property) { return node.callee.property.name; } return 'log'; } isForbiddenFunctionCall(node) { return node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && this.config.forbiddenFunctions.includes(node.callee.name); } getFunctionName(node) { if (node.callee && node.callee.name) { return node.callee.name; } return 'unknown'; } isInDevelopmentContext(node, ast) { // Check if node is within an if statement checking development flags let parent = node.parent; while (parent) { if (parent.type === 'IfStatement') { const test = parent.test; if (this.isDevelopmentCondition(test)) { return true; } } parent = parent.parent; } return false; } isDevelopmentCondition(node) { if (!node) return false; // Check for various development condition patterns if (node.type === 'Identifier' && this.config.developmentFlags.has(node.name)) { return true; } if (node.type === 'MemberExpression') { const source = this.nodeToString(node); return Array.from(this.config.developmentFlags).some(flag => source.includes(flag)); } if (node.type === 'BinaryExpression') { return this.isDevelopmentCondition(node.left) || this.isDevelopmentCondition(node.right); } return false; } nodeToString(node) { // Simple node to string conversion for pattern matching if (node.type === 'Identifier') { return node.name; } if (node.type === 'MemberExpression') { return `${this.nodeToString(node.object)}.${node.property.name}`; } if (node.type === 'Literal') { return String(node.value); } return ''; } getNodeLocation(node) { if (node.loc) { return { line: node.loc.start.line, column: node.loc.start.column + 1 }; } return null; } traverseAST(node, visitor) { if (!node || typeof node !== 'object') return; // Prevent infinite recursion if (node._visited) return; node._visited = true; try { // Visit current node if (visitor[node.type]) { visitor[node.type](node); } // Traverse children with depth limit const maxDepth = 100; if ((node._depth || 0) > maxDepth) return; for (const key in node) { if (key === 'parent' || key === '_visited' || key === '_depth') continue; // Avoid circular references const child = node[key]; if (Array.isArray(child)) { for (const item of child) { if (item && typeof item === 'object') { item.parent = node; // Set parent reference item._depth = (node._depth || 0) + 1; this.traverseAST(item, visitor); } } } else if (child && typeof child === 'object') { child.parent = node; // Set parent reference child._depth = (node._depth || 0) + 1; this.traverseAST(child, visitor); } } } finally { // Clean up to prevent memory leaks delete node._visited; } } // File Classification Methods isTestFile(filePath) { return this.config.testFilePatterns.some(pattern => filePath.includes(pattern) ); } isDevelopmentFile(filePath) { return this.config.developmentPatterns.some(pattern => filePath.includes(pattern) ); } // Regex Fallback Methods (for compatibility) async analyzeWithRegexFallback(files, language, config) { const violations = []; for (const filePath of files) { try { const fileContent = fs.readFileSync(filePath, 'utf8'); const fileViolations = await this.analyzeFileWithRegex(filePath, fileContent, language, config); violations.push(...fileViolations); } catch (error) { console.warn(`C043 regex fallback analysis error for ${filePath}:`, error.message); } } return violations; } async analyzeFileWithRegex(filePath, fileContent, language, config) { const violations = []; // Skip test files and development files if (this.isTestFile(filePath) || this.isDevelopmentFile(filePath)) { return violations; } const lines = fileContent.split('\n'); // Simple regex patterns for fallback const consolePattern = /\bconsole\.(log|debug|info|trace|dir|table|count|time|clear|group|assert|profile)\s*\(/g; const forbiddenPattern = /\b(print|alert|confirm|prompt)\s*\(/g; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = i + 1; // Skip comments and strings (basic check) if (this.isLineCommentOrString(line)) { continue; } // Check console calls consolePattern.lastIndex = 0; // Reset regex state let match; while ((match = consolePattern.exec(line)) !== null) { const method = match[1]; if (!this.config.allowedMethods.has(method)) { violations.push({ ruleId: this.ruleId, severity: this.severity, message: `Do not use console.${method}() in production code. Use proper logging instead.`, filePath: filePath, line: lineNumber, column: match.index + 1, source: line.trim(), suggestion: `Consider using a proper logging library (logger.${method}())` }); } // Prevent infinite loop - limit to first match per line break; } // Check forbidden functions forbiddenPattern.lastIndex = 0; // Reset regex state while ((match = forbiddenPattern.exec(line)) !== null) { const functionName = match[1]; violations.push({ ruleId: this.ruleId, severity: this.severity, message: `Do not use ${functionName}() in production code. Use proper logging or UI notifications instead.`, filePath: filePath, line: lineNumber, column: match.index + 1, source: line.trim(), suggestion: `Consider using a logging library or proper UI notification system` }); // Prevent infinite loop - limit to first match per line break; } } return violations; } isLineCommentOrString(line) { const trimmed = line.trim(); // Single line comment if (trimmed.startsWith('//') || trimmed.startsWith('*')) { return true; } // Simple string check - if more quotes before console than after, likely in string const beforeConsole = line.split(/console\.|print\(|alert\(/)[0]; if (beforeConsole) { const quotes = (beforeConsole.match(/['"]/g) || []).length; return quotes % 2 === 1; } return false; } } module.exports = C043NoConsoleOrPrintAnalyzer;