@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
1,506 lines (1,282 loc) • 53.1 kB
JavaScript
/**
* Heuristic Analysis Engine Plugin with ts-morph Core Integration
* Following Rule C005: Single responsibility - Pattern-based analysis with ts-morph core
* Following Rule C014: Dependency injection - implements interface
* Following Rule C015: Use domain language - clear heuristic analysis terms
*/
const AnalysisEngineInterface = require('../core/interfaces/analysis-engine.interface');
const ASTModuleRegistry = require('../core/ast-modules/index');
const dependencyChecker = require('../core/dependency-checker');
const SunlintRuleAdapter = require('../core/adapters/sunlint-rule-adapter');
const SemanticEngine = require('../core/semantic-engine');
const SemanticRuleBase = require('../core/semantic-rule-base');
const { getInstance: getUnifiedRegistry } = require('../core/unified-rule-registry');
const AutoPerformanceManager = require('../core/auto-performance-manager');
const fs = require('fs');
const path = require('path');
class HeuristicEngine extends AnalysisEngineInterface {
constructor() {
super('heuristic', '4.0', ['typescript', 'javascript', 'dart', 'swift', 'kotlin', 'java', 'python', 'go', 'rust', 'all']);
this.ruleAnalyzers = new Map();
this.supportedRulesList = [];
this.ruleAdapter = SunlintRuleAdapter.getInstance();
this.astRegistry = ASTModuleRegistry;
// ts-morph as core technology for heuristic engine
// Note: semantic engine will be initialized in initialize() with proper config
this.semanticEngine = null;
this.semanticRules = new Map();
this.symbolTableEnabled = false;
// Unified rule registry
this.unifiedRegistry = getUnifiedRegistry();
// ✅ PERFORMANCE OPTIMIZATIONS (Integrated)
this.performanceManager = new AutoPerformanceManager();
this.performanceConfig = null;
this.metrics = {
startTime: null,
filesProcessed: 0,
rulesProcessed: 0,
violationsFound: 0,
memoryUsage: 0
};
}
/**
* Initialize Heuristic engine with ts-morph core and configuration
* ✅ ENHANCED: Now includes performance optimization
* Following Rule C006: Verb-noun naming
* @param {Object} config - Engine configuration
*/
async initialize(config) {
try {
// ✅ PERFORMANCE: Get optimal settings based on project
this.performanceConfig = this.performanceManager.getOptimalSettings(config, config?.targetFiles || []);
// Store verbosity setting
this.verbose = config?.verbose || false;
if (this.verbose && this.performanceConfig.autoDetected) {
console.log(`🤖 [HeuristicEngine] Auto-detected performance profile: ${this.performanceConfig.name}`);
console.log(` ⚡ Settings: ${this.performanceConfig.timeout/1000}s timeout, ${this.performanceConfig.batchSize || 'auto'} batch size`);
}
// Initialize unified rule registry
await this.unifiedRegistry.initialize({ verbose: this.verbose });
// Check for optional AST dependencies
dependencyChecker.checkAndNotify('ast');
// Initialize ts-morph Symbol Table (core requirement)
await this.initializeSymbolTable(config);
// Initialize rule adapter
await this.ruleAdapter.initialize();
// Load available rules from unified registry (OPTIMIZED: skip for performance)
// Rules will be loaded on-demand in analyze() method
if (config.loadAllRules) {
await this.loadRulesFromRegistry(config);
} else if (this.verbose) {
console.log(`⚡ [HeuristicEngine] Skipping bulk rule loading for performance - will load on-demand`);
}
this.initialized = true;
if (this.verbose) {
console.log(`🔍 Heuristic engine v4.0 initialized:`);
console.log(` 📊 Total rules: ${this.supportedRulesList.length}`);
console.log(` 🧠 Symbol Table: ${this.symbolTableInitialized ? 'enabled' : 'disabled'}`);
console.log(` 🔧 Semantic rules: ${this.semanticRules.size}`);
console.log(` ⚡ Performance: ${this.performanceConfig.name || 'standard'}`);
}
} catch (error) {
console.error('Failed to initialize Heuristic engine:', error.message);
throw error;
}
}
/**
* Initialize ts-morph Symbol Table as core requirement
* OPTIMIZED: Use targeted files instead of entire project for better performance
*/
async initializeSymbolTable(config) {
const projectPath = config?.projectPath || process.cwd();
try {
// Initialize semantic engine with config options including maxSemanticFiles
const semanticOptions = {
maxSemanticFiles: config?.maxSemanticFiles,
verbose: this.verbose,
...config?.semanticOptions
};
this.semanticEngine = new SemanticEngine(semanticOptions);
// Pass verbose option to semantic engine
this.semanticEngine.verbose = this.verbose;
// ts-morph is now a core dependency - but optimized for targeted files
const success = await this.semanticEngine.initialize(projectPath, config?.targetFiles);
if (success) {
this.semanticEnabled = true;
this.symbolTableInitialized = true;
if (this.verbose) {
console.log(`🧠 Symbol Table initialized for: ${projectPath}`);
}
} else {
if (this.verbose) {
console.warn('⚠️ Symbol Table initialization failed, using fallback mode');
}
}
} catch (error) {
if (this.verbose) {
console.warn('⚠️ ts-morph Symbol Table unavailable:', error.message);
console.warn('⚠️ Falling back to traditional AST/regex analysis only');
}
}
}
/**
* Load rules from unified registry instead of scanning directories
* Following Rule C006: Verb-noun naming
* @param {Object} config - Engine configuration
*/
async loadRulesFromRegistry(config = {}) {
try {
// Get rules supported by heuristic engine from unified registry
const supportedRules = this.unifiedRegistry.getRulesForEngine('heuristic');
if (this.verbose) {
console.log(`🔍 [HeuristicEngine] Found ${supportedRules.length} rules from unified registry`);
}
// Load each rule
for (const ruleDefinition of supportedRules) {
await this.loadRuleFromDefinition(ruleDefinition);
}
// Manually load C047 if needed (DEPRECATED - C047 now in enhanced registry)
// if (!this.semanticRules.has('C047') && !this.ruleAnalyzers.has('C047')) {
// await this.manuallyLoadC047();
// }
if (this.verbose) {
console.log(`✅ [HeuristicEngine] Loaded ${this.supportedRulesList.length} rules from unified registry`);
}
} catch (error) {
console.error('Failed to load rules from registry:', error.message);
// Fallback to old scanning method
await this.scanRuleAnalyzers(config);
}
}
/**
* Load a single rule from its definition
* @param {Object} ruleDefinition - Rule definition from unified registry
*/
async loadRuleFromDefinition(ruleDefinition) {
const ruleId = ruleDefinition.id;
try {
// Resolve best analyzer path for this engine
const analyzerPath = this.unifiedRegistry.resolveAnalyzerPath(ruleId, 'heuristic');
if (!analyzerPath) {
if (this.verbose) {
console.warn(`⚠️ [HeuristicEngine] No compatible analyzer found for ${ruleId}`);
}
return;
}
// Determine analyzer type from path and strategy
const strategy = ruleDefinition.strategy.preferred;
const category = ruleDefinition.category;
if (strategy === 'semantic' && this.symbolTableInitialized) {
// Load as semantic rule
await this.loadSemanticRule(ruleId, analyzerPath, { category });
} else {
// Load as traditional rule (ast/regex)
await this.loadTraditionalRule(ruleId, analyzerPath, { category, type: strategy });
}
} catch (error) {
if (this.verbose) {
console.warn(`⚠️ [HeuristicEngine] Failed to load rule ${ruleId}:`, error.message);
}
}
}
/**
* Load semantic rule from analyzer path
* @param {string} ruleId - Rule ID
* @param {string} analyzerPath - Path to analyzer file
* @param {Object} metadata - Rule metadata
*/
async loadSemanticRule(ruleId, analyzerPath, metadata) {
try {
const SemanticRuleClass = require(analyzerPath);
// Verify it extends SemanticRuleBase
if (this.isSemanticRule(SemanticRuleClass)) {
await this.registerSemanticRule(ruleId, SemanticRuleClass, {
path: analyzerPath,
category: metadata.category
});
if (this.verbose) {
console.log(`🧠 [HeuristicEngine] Loaded semantic rule: ${ruleId}`);
}
} else {
// Not a semantic rule, fallback to traditional
await this.loadTraditionalRule(ruleId, analyzerPath, metadata);
}
} catch (error) {
if (this.verbose) {
console.warn(`⚠️ [HeuristicEngine] Failed to load semantic rule ${ruleId}:`, error.message);
}
}
}
/**
* Load traditional rule (ast/regex) from analyzer path
* @param {string} ruleId - Rule ID
* @param {string} analyzerPath - Path to analyzer file
* @param {Object} metadata - Rule metadata
*/
async loadTraditionalRule(ruleId, analyzerPath, metadata) {
try {
const analyzerModule = require(analyzerPath);
const AnalyzerClass = analyzerModule.default || analyzerModule;
this.registerTraditionalRule(ruleId, AnalyzerClass, {
path: analyzerPath,
category: metadata.category,
folder: ruleId, // Add folder name for config loading
type: metadata.type || 'regex'
});
if (this.verbose) {
console.log(`🔧 [HeuristicEngine] Loaded ${metadata.type} rule: ${ruleId}`);
}
} catch (error) {
if (this.verbose) {
console.warn(`⚠️ [HeuristicEngine] Failed to load traditional rule ${ruleId}:`, error.message);
}
}
}
/**
* Scan for available rule analyzers with semantic support
* Priority: semantic > ast > regex
* Following Rule C006: Verb-noun naming
*/
async scanRuleAnalyzers(config = {}) {
const rulesDir = path.resolve(__dirname, '../rules');
if (!fs.existsSync(rulesDir)) {
console.warn('⚠️ Rules directory not found');
return;
}
try {
// Scan category folders (common, security, typescript, etc.)
const categoryFolders = fs.readdirSync(rulesDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.filter(dirent => !['tests', 'docs', 'utils', 'migration'].includes(dirent.name))
.map(dirent => dirent.name);
for (const categoryFolder of categoryFolders) {
const categoryPath = path.join(rulesDir, categoryFolder);
// Scan rule folders within category
const ruleFolders = fs.readdirSync(categoryPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const ruleFolder of ruleFolders) {
const ruleId = ruleFolder; // Use folder name directly as rule ID
const rulePath = path.join(categoryPath, ruleFolder);
await this.loadRuleAnalyzer(ruleId, rulePath, categoryFolder);
}
}
} catch (error) {
console.warn('⚠️ Error scanning rule analyzers:', error.message);
}
}
/**
* Lazy load a single rule on-demand
* @param {string} ruleId - Rule ID to load
* @param {Object} options - Loading options
*/
async lazyLoadRule(ruleId, options = {}) {
try {
const ruleDefinition = this.unifiedRegistry.getRuleDefinition(ruleId);
if (!ruleDefinition) {
if (options.verbose) {
console.warn(`⚠️ [HeuristicEngine] Rule definition not found for ${ruleId}`);
}
return;
}
// Check if rule supports heuristic engine
if (!this.unifiedRegistry.isRuleSupported(ruleId, 'heuristic')) {
if (options.verbose) {
console.warn(`⚠️ [HeuristicEngine] Rule ${ruleId} not supported by heuristic engine`);
}
return;
}
if (options.verbose) {
console.log(`🔄 [HeuristicEngine] Lazy loading rule ${ruleId}...`);
}
await this.loadRuleFromDefinition(ruleDefinition);
} catch (error) {
if (options.verbose) {
console.warn(`⚠️ [HeuristicEngine] Failed to lazy load rule ${ruleId}:`, error.message);
}
}
}
/**
* Manually load C047 semantic rule (special case)
*/
async manuallyLoadC047() {
try {
if (this.verbose) {
console.log(`[DEBUG] 🔬 Manually loading C047 semantic rule...`);
}
const c047Path = path.resolve(__dirname, '../rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js');
if (fs.existsSync(c047Path)) {
const C047SemanticRule = require(c047Path);
const instance = new C047SemanticRule();
// Register as semantic rule
await this.registerSemanticRule('C047', C047SemanticRule, {
path: c047Path,
category: 'common',
type: 'semantic',
description: 'C047 - No Duplicate Retry Logic (Semantic Analysis)'
});
if (this.verbose) {
console.log(`[DEBUG] ✅ C047 semantic rule loaded successfully`);
}
} else {
console.warn(`⚠️ C047 semantic rule not found at: ${c047Path}`);
}
} catch (error) {
console.warn(`⚠️ Failed to manually load C047:`, error.message);
}
}
/**
* Load rule analyzer with semantic priority
*/
async loadRuleAnalyzer(ruleId, rulePath, categoryFolder) {
// Analyzer priority: semantic > ast > regex
const analyzerCandidates = [
{ path: path.join(rulePath, 'semantic-analyzer.js'), type: 'semantic' },
{ path: path.join(rulePath, 'ast-analyzer.js'), type: 'ast' },
{ path: path.join(rulePath, 'regex-analyzer.js'), type: 'regex' },
{ path: path.join(rulePath, 'analyzer.js'), type: 'regex' } // legacy fallback
];
let selectedAnalyzer = null;
let analyzerPath = null;
let analyzerType = null;
// Try semantic analyzer first if Symbol Table available
if (this.symbolTableInitialized) {
const semanticCandidate = analyzerCandidates[0];
if (fs.existsSync(semanticCandidate.path)) {
try {
const analyzerModule = require(semanticCandidate.path);
selectedAnalyzer = analyzerModule.default || analyzerModule;
analyzerPath = semanticCandidate.path;
analyzerType = 'semantic';
// Verify it extends SemanticRuleBase
if (this.isSemanticRule(selectedAnalyzer)) {
await this.registerSemanticRule(ruleId, selectedAnalyzer, {
path: analyzerPath,
category: categoryFolder
});
return; // Successfully registered semantic rule
}
} catch (error) {
console.debug(`Semantic analyzer for ${ruleId} failed to load:`, error.message);
}
}
}
// Fall back to AST analyzer
const astCandidate = analyzerCandidates[1];
if (fs.existsSync(astCandidate.path)) {
try {
const analyzerModule = require(astCandidate.path);
selectedAnalyzer = analyzerModule.default || analyzerModule;
analyzerPath = astCandidate.path;
analyzerType = 'ast';
} catch (error) {
console.debug(`AST analyzer for ${ruleId} failed to load:`, error.message);
}
}
// Fall back to regex analyzer
if (!selectedAnalyzer) {
for (const regexCandidate of analyzerCandidates.slice(2)) {
if (fs.existsSync(regexCandidate.path)) {
try {
const analyzerModule = require(regexCandidate.path);
selectedAnalyzer = analyzerModule.default || analyzerModule;
analyzerPath = regexCandidate.path;
analyzerType = 'regex';
break;
} catch (error) {
console.debug(`Regex analyzer for ${ruleId} failed to load:`, error.message);
}
}
}
}
// Register traditional (non-semantic) analyzer
if (selectedAnalyzer) {
this.registerTraditionalRule(ruleId, selectedAnalyzer, {
path: analyzerPath,
category: categoryFolder,
folder: fullRuleId, // Add folder name for config loading
type: analyzerType
});
}
}
/**
* Check if analyzer is a semantic rule
*/
isSemanticRule(analyzerClass) {
if (typeof analyzerClass !== 'function') return false;
try {
const instance = new analyzerClass(analyzerClass.name);
return instance instanceof SemanticRuleBase;
} catch (error) {
return false;
}
}
/**
* Register semantic rule (lazy initialization)
*/
async registerSemanticRule(ruleId, analyzerClass, metadata) {
try {
// Store rule class and metadata for lazy initialization
this.semanticRules.set(ruleId, {
analyzerClass,
metadata,
type: 'semantic',
initialized: false,
instance: null
});
this.supportedRulesList.push(ruleId);
if (this.verbose) {
console.log(`🧠 Registered semantic rule: ${ruleId} (lazy initialization)`);
}
} catch (error) {
console.warn(`⚠️ Failed to register semantic rule ${ruleId}:`, error.message);
}
}
/**
* Initialize semantic rule on-demand
*/
async initializeSemanticRule(ruleId) {
const ruleEntry = this.semanticRules.get(ruleId);
if (!ruleEntry || ruleEntry.initialized) {
return ruleEntry?.instance;
}
try {
const instance = new ruleEntry.analyzerClass(ruleId);
instance.initialize(this.semanticEngine, { verbose: this.verbose });
// Update entry with initialized instance
ruleEntry.instance = instance;
ruleEntry.initialized = true;
if (this.verbose) {
console.log(`🔧 Rule ${ruleId} initialized with semantic analysis`);
}
return instance;
} catch (error) {
console.warn(`⚠️ Failed to initialize semantic rule ${ruleId}:`, error.message);
return null;
}
}
/**
* Register traditional (heuristic/AST/regex) rule
*/
registerTraditionalRule(ruleId, analyzer, metadata) {
if (typeof analyzer === 'function') {
// Class constructor
this.ruleAnalyzers.set(ruleId, {
...metadata,
class: analyzer,
type: 'class'
});
this.supportedRulesList.push(ruleId);
} else if (analyzer && typeof analyzer === 'object' && analyzer.analyze) {
// Instance with analyze method
this.ruleAnalyzers.set(ruleId, {
...metadata,
instance: analyzer,
type: 'instance'
});
this.supportedRulesList.push(ruleId);
} else {
console.warn(`⚠️ Analyzer for ${ruleId} has unsupported format:`, typeof analyzer);
}
}
/**
* Extract rule ID from folder name
* Following Rule C006: Verb-noun naming
* @param {string} folderName - Rule folder name
* @returns {string} Rule ID
*/
extractRuleIdFromFolder(folderName) {
// Extract rule ID from patterns like "C019_log_level_usage"
const match = folderName.match(/^([CST]\d{3})/);
return match ? match[1] : folderName;
}
/**
* Get full rule ID from short rule ID (C029 -> C029_catch_block_logging)
* @param {string} ruleId - Short rule ID
* @returns {string} Full rule ID or original if not found
*/
getFullRuleId(ruleId) {
// Check exact match first
if (this.ruleAnalyzers.has(ruleId)) {
return ruleId;
}
// Find full rule ID that starts with short rule ID
const shortRulePattern = new RegExp(`^${ruleId}_`);
const fullRuleId = Array.from(this.ruleAnalyzers.keys()).find(fullId => shortRulePattern.test(fullId));
return fullRuleId || ruleId; // Return original if not found
}
/**
* Check if a rule is supported by this engine
* Following Rule C006: Verb-noun naming
* @param {string} ruleId - Rule ID to check
* @returns {boolean} True if rule is supported
*/
isRuleSupported(ruleId) {
// Special case: C047 is always supported (loaded on-demand)
if (ruleId === 'C047') {
return true;
}
// Use unified registry for primary lookup
if (this.unifiedRegistry && this.unifiedRegistry.initialized) {
return this.unifiedRegistry.isRuleSupported(ruleId, 'heuristic');
}
// Fallback to original logic for backward compatibility
return this.supportedRulesList.includes(ruleId) ||
this.semanticRules.has(ruleId) ||
this.ruleAnalyzers.has(ruleId) ||
this.checkShortRuleIdMatch(ruleId);
}
/**
* Check short rule ID matches (backward compatibility)
* @param {string} ruleId - Short rule ID (e.g., C029)
* @returns {boolean} True if matches any full rule ID
*/
checkShortRuleIdMatch(ruleId) {
const shortRulePattern = new RegExp(`^${ruleId}_`);
return this.supportedRulesList.some(fullRuleId => shortRulePattern.test(fullRuleId)) ||
Array.from(this.semanticRules.keys()).some(fullRuleId => shortRulePattern.test(fullRuleId)) ||
Array.from(this.ruleAnalyzers.keys()).some(fullRuleId => shortRulePattern.test(fullRuleId));
}
/**
* Analyze files using heuristic patterns
* ✅ ENHANCED: Now includes performance optimizations and batch processing
* Following Rule C006: Verb-noun naming
* @param {string[]} files - Files to analyze
* @param {Object[]} rules - Rules to apply
* @param {Object} options - Analysis options
* @returns {Promise<Object>} Analysis results
*/
async analyze(files, rules, options) {
if (!this.initialized) {
throw new Error('Heuristic engine not initialized');
}
// ✅ PERFORMANCE: Apply file limits and timeout protection
const startTime = Date.now();
this.metrics.startTime = startTime;
// Apply analysis file limits (different from semantic file limits)
const maxFiles = this.getAnalysisFileLimit(options);
const limitedFiles = files.slice(0, maxFiles);
if (files.length > maxFiles && this.verbose) {
console.warn(`⚠️ [HeuristicEngine] Analysis file limit: ${limitedFiles.length}/${files.length} files`);
console.log(` 💡 Note: Symbol table uses separate limit (--max-semantic-files)`);
}
// Set up timeout if configured
const timeout = this.performanceConfig?.timeout || parseInt(options.timeout) || 0;
let timeoutId = null;
if (timeout > 0) {
timeoutId = setTimeout(() => {
throw new Error(`Analysis timeout after ${timeout}ms`);
}, timeout);
}
if (options.verbose) {
console.log(`🔍 [HeuristicEngine] Analyzing ${limitedFiles.length} files with ${rules.length} rules`);
if (this.performanceConfig?.name) {
console.log(`⚡ [Performance] Using ${this.performanceConfig.name} profile`);
}
if (timeout > 0) {
console.log(`⏰ [Timeout] Analysis will timeout after ${timeout/1000}s`);
}
}
try {
// Check if we should use batch processing
if (this.shouldUseBatchProcessing(limitedFiles, rules)) {
return await this.analyzeBatched(limitedFiles, rules, options);
} else {
return await this.analyzeStandard(limitedFiles, rules, options);
}
} finally {
// Clear timeout
if (timeoutId) {
clearTimeout(timeoutId);
}
// Log performance metrics
const duration = Date.now() - startTime;
this.metrics.filesProcessed = limitedFiles.length;
this.metrics.rulesProcessed = rules.length;
if (options.verbose) {
console.log(`✅ [HeuristicEngine] Analysis completed in ${duration}ms`);
}
}
}
/**
* ✅ NEW: Get analysis file limit (separate from semantic file limit)
*/
getAnalysisFileLimit(options) {
// User-specified limit
if (options.maxFiles && parseInt(options.maxFiles) > 0) {
return parseInt(options.maxFiles);
}
// Performance config limit
if (this.performanceConfig?.maxFiles) {
return this.performanceConfig.maxFiles;
}
// Default based on performance mode
const mode = options.performance || 'auto';
const defaults = {
fast: 500,
auto: 1000,
careful: 1500
};
return defaults[mode] || 1000;
}
/**
* ✅ NEW: Determine if batch processing should be used
*/
shouldUseBatchProcessing(files, rules) {
const batchThreshold = this.performanceConfig?.batchThreshold || 100;
const totalWorkload = files.length * rules.length;
return totalWorkload > batchThreshold ||
files.length > 200 ||
rules.length > 30;
}
/**
* ✅ NEW: Batch processing for large workloads
*/
async analyzeBatched(files, rules, options) {
if (options.verbose) {
console.log(`� [HeuristicEngine] Using batch processing for large workload`);
}
const results = {
results: [],
filesAnalyzed: files.length,
engine: 'heuristic',
metadata: {
rulesAnalyzed: rules.map(r => r.id),
analyzersUsed: [],
batchProcessing: true
}
};
// Create rule batches
const batchSize = this.performanceConfig?.batchSize || 10;
const ruleBatches = [];
for (let i = 0; i < rules.length; i += batchSize) {
ruleBatches.push(rules.slice(i, i + batchSize));
}
if (options.verbose) {
console.log(`📦 [Batch] Processing ${ruleBatches.length} rule batches (${batchSize} rules each)`);
}
// Process each batch
for (let batchIndex = 0; batchIndex < ruleBatches.length; batchIndex++) {
const ruleBatch = ruleBatches[batchIndex];
if (options.verbose) {
console.log(`⚡ [Batch ${batchIndex + 1}/${ruleBatches.length}] Processing ${ruleBatch.length} rules...`);
}
const batchResults = await this.analyzeStandard(files, ruleBatch, options);
// Merge batch results
for (const fileResult of batchResults.results) {
let existingFile = results.results.find(r => r.file === fileResult.file);
if (!existingFile) {
existingFile = { file: fileResult.file, violations: [] };
results.results.push(existingFile);
}
existingFile.violations.push(...fileResult.violations);
}
results.metadata.analyzersUsed.push(...batchResults.metadata.analyzersUsed);
// Memory management
if (batchIndex % 3 === 0 && global.gc) {
global.gc(); // Trigger garbage collection every 3 batches
}
}
return results;
}
/**
* ✅ REFACTORED: Standard analysis method (extracted from original analyze)
*/
async analyzeStandard(files, rules, options) {
const results = {
results: [],
filesAnalyzed: files.length,
engine: 'heuristic',
metadata: {
rulesAnalyzed: rules.map(r => r.id),
analyzersUsed: []
}
};
// Group files by language for efficient processing
const filesByLanguage = this.groupFilesByLanguage(files);
for (const rule of rules) {
// Special case: Load C047 semantic rule on-demand
if (rule.id === 'C047' && !this.semanticRules.has('C047')) {
if (options.verbose) {
console.log(`🔬 [HeuristicEngine] Loading C047 semantic rule on-demand...`);
}
await this.manuallyLoadC047();
}
// Lazy load rule if not already loaded
if (!this.isRuleSupported(rule.id)) {
if (options.verbose) {
console.log(`🔄 [HeuristicEngine] Lazy loading rule ${rule.id}...`);
}
await this.lazyLoadRule(rule.id, options);
}
if (!this.isRuleSupported(rule.id)) {
if (options.verbose) {
console.warn(`⚠️ Rule ${rule.id} not supported by Heuristic engine, skipping...`);
}
continue;
}
try {
let ruleViolations = [];
// Check if this is a semantic rule first (higher priority)
if (this.semanticRules.has(rule.id)) {
if (options.verbose) {
console.log(`🧠 [HeuristicEngine] Running semantic analysis for rule ${rule.id}`);
}
ruleViolations = await this.analyzeSemanticRule(rule, files, options);
} else {
// Fallback to traditional analysis
if (options.verbose) {
console.log(`🔧 [HeuristicEngine] Running traditional analysis for rule ${rule.id}`);
}
ruleViolations = await this.analyzeRule(rule, filesByLanguage, options);
}
if (ruleViolations.length > 0) {
// Group violations by file
const violationsByFile = this.groupViolationsByFile(ruleViolations);
for (const [filePath, violations] of violationsByFile) {
// Find or create file result
let fileResult = results.results.find(r => r.file === filePath);
if (!fileResult) {
fileResult = { file: filePath, violations: [] };
results.results.push(fileResult);
}
fileResult.violations.push(...violations);
}
}
results.metadata.analyzersUsed.push(rule.id);
} catch (error) {
console.error(`❌ Failed to analyze rule ${rule.id}:`, error.message);
// Continue with other rules
}
}
return results;
}
/**
* Analyze semantic rule across files
* Following Rule C006: Verb-noun naming
* @param {Object} rule - Rule to analyze
* @param {string[]} files - Files to analyze
* @param {Object} options - Analysis options
* @returns {Promise<Object[]>} Rule violations
*/
async analyzeSemanticRule(rule, files, options) {
const semanticRuleInfo = this.semanticRules.get(rule.id);
if (!semanticRuleInfo) {
console.warn(`⚠️ Semantic rule ${rule.id} not found`);
return [];
}
try {
// Initialize rule on-demand (lazy initialization)
const ruleInstance = await this.initializeSemanticRule(rule.id);
if (!ruleInstance) {
console.warn(`⚠️ Failed to initialize semantic rule ${rule.id}`);
return [];
}
const allViolations = [];
// Run semantic analysis for each file
for (const filePath of files) {
try {
if (options.verbose) {
console.log(`🧠 [SemanticRule] Analyzing ${path.basename(filePath)} with ${rule.id}`);
}
// Call semantic rule's analyzeFile method
await ruleInstance.analyzeFile(filePath, options);
// Get violations from the rule instance
const fileViolations = ruleInstance.getViolations();
allViolations.push(...fileViolations);
// Clear violations for next file
ruleInstance.clearViolations();
} catch (fileError) {
console.warn(`⚠️ Semantic rule ${rule.id} failed for ${filePath}:`, fileError.message);
}
}
if (options.verbose && allViolations.length > 0) {
console.log(`🧠 [SemanticRule] Found ${allViolations.length} violations for ${rule.id}`);
}
return allViolations;
} catch (error) {
console.error(`❌ Failed to run semantic rule ${rule.id}:`, error.message);
return [];
}
}
/**
* Analyze a specific rule across files
* Following Rule C006: Verb-noun naming
* @param {Object} rule - Rule to analyze
* @param {Object} filesByLanguage - Files grouped by language
* @param {Object} options - Analysis options
* @returns {Promise<Object[]>} Rule violations
*/
async analyzeRule(rule, filesByLanguage, options) {
// Get full rule ID (C029 -> C029_catch_block_logging)
const fullRuleId = this.getFullRuleId(rule.id);
// Lazy load rule if not already loaded
if (!this.ruleAnalyzers.has(fullRuleId)) {
if (options.verbose) {
console.log(`🔄 [HeuristicEngine] Lazy loading rule ${rule.id} for analysis...`);
}
await this.lazyLoadRule(rule.id, options);
}
const analyzerInfo = this.ruleAnalyzers.get(fullRuleId);
if (!analyzerInfo) {
return [];
}
try {
// Get analyzer - handle both class and instance types
let analyzer;
if (analyzerInfo.type === 'class') {
// Create analyzer instance from class
const AnalyzerClass = analyzerInfo.class;
try {
analyzer = new AnalyzerClass({
verbose: options.verbose,
semanticEngine: this.semanticEngine
});
// Initialize with semantic engine if method exists
if (analyzer.initialize && typeof analyzer.initialize === 'function') {
await analyzer.initialize(this.semanticEngine);
}
} catch (constructorError) {
throw new Error(`Failed to instantiate analyzer class: ${constructorError.message}`);
}
} else if (analyzerInfo.type === 'instance') {
// Use existing analyzer instance
analyzer = analyzerInfo.instance;
// Initialize existing instance with semantic engine if method exists
if (analyzer.initialize && typeof analyzer.initialize === 'function') {
await analyzer.initialize(this.semanticEngine);
}
} else {
throw new Error(`Unknown analyzer type: ${analyzerInfo.type}`);
}
// Verify analyzer has required methods
if (!analyzer.analyze || typeof analyzer.analyze !== 'function') {
console.warn(`⚠️ Analyzer for ${rule.id} missing analyze method`);
return [];
}
const allViolations = [];
// Run analyzer for each supported language
const ruleLanguages = this.getRuleLanguages(rule);
for (const language of ruleLanguages) {
const languageFiles = filesByLanguage[language] || [];
if (languageFiles.length === 0) continue;
try {
// Load rule config
const ruleConfig = await this.loadRuleConfig(rule.id, analyzerInfo.folder, analyzerInfo.category, options.verbose);
// Run analysis with AST enhancement
if (options.verbose) {
console.log(`🔧 [DEBUG] About to call runEnhancedAnalysis for rule ${rule.id}, language ${language}`);
}
const languageViolations = await this.runEnhancedAnalysis(
analyzer,
rule.id,
languageFiles,
language,
{ ...ruleConfig, ...options, semanticEngine: this.semanticEngine }
);
allViolations.push(...languageViolations);
} catch (error) {
console.error(`❌ Rule ${rule.id} failed for ${language}:`, error.message);
}
}
return allViolations;
} catch (error) {
console.error(`❌ Failed to create analyzer for rule ${rule.id}:`, error.message);
return [];
}
}
/**
* Get supported languages for a rule
* Following Rule C006: Verb-noun naming
* @param {Object} rule - Rule object
* @returns {string[]} Supported languages
*/
getRuleLanguages(rule) {
// Get from rule adapter
const adapterRule = this.ruleAdapter.getRuleById(rule.id);
if (adapterRule?.languages) {
// If rule supports 'All languages', return all available languages
if (adapterRule.languages.includes('All languages')) {
return ['typescript', 'javascript', 'java', 'python', 'ruby', 'php', 'dart', 'kotlin', 'swift'];
}
return adapterRule.languages;
}
// Fallback to rule object
if (rule.languages) {
// If rule supports 'All languages', return all available languages
if (rule.languages.includes('All languages')) {
return ['typescript', 'javascript', 'java', 'python', 'ruby', 'php', 'dart', 'kotlin', 'swift'];
}
return rule.languages;
}
// Default to common languages
return ['typescript', 'javascript'];
}
/**
* Load rule configuration
* Following Rule C006: Verb-noun naming
* @param {string} ruleId - Rule ID
* @param {string} ruleFolder - Rule folder name
* @param {string} category - Rule category (common, security, etc)
* @param {boolean} verbose - Enable verbose logging
* @returns {Promise<Object>} Rule configuration
*/
async loadRuleConfig(ruleId, ruleFolder, category = 'common', verbose = false) {
try {
const configPath = path.resolve(__dirname, '../rules', category, ruleFolder, 'config.json');
if (fs.existsSync(configPath)) {
return require(configPath);
}
} catch (error) {
if (verbose) {
console.warn(`[DEBUG] ⚠️ Failed to load config for ${ruleId}:`, error.message);
}
}
// Return minimal config
return {
ruleId,
name: `Rule ${ruleId}`,
description: `Analysis for rule ${ruleId}`,
severity: 'warning'
};
}
/**
* Group files by programming language
* Following Rule C006: Verb-noun naming
* @param {string[]} files - Files to group
* @returns {Object} Files grouped by language
*/
groupFilesByLanguage(files) {
const groups = {};
for (const file of files) {
const language = this.detectLanguage(file);
if (!groups[language]) {
groups[language] = [];
}
groups[language].push(file);
}
return groups;
}
/**
* Detect programming language from file extension
* Following Rule C006: Verb-noun naming
* @param {string} filePath - File path
* @returns {string} Detected language
*/
detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.ts': case '.tsx': return 'typescript';
case '.js': case '.jsx': return 'javascript';
case '.dart': return 'dart';
case '.swift': return 'swift';
case '.kt': case '.kts': return 'kotlin';
default: return 'unknown';
}
}
/**
* Group violations by file path
* Following Rule C006: Verb-noun naming
* @param {Object[]} violations - Array of violations
* @returns {Map} Violations grouped by file
*/
groupViolationsByFile(violations) {
const groups = new Map();
for (const violation of violations) {
const filePath = violation.file || violation.filePath;
if (!filePath) continue;
if (!groups.has(filePath)) {
groups.set(filePath, []);
}
groups.get(filePath).push(violation);
}
return groups;
}
/**
* Get supported rules
* Following Rule C006: Verb-noun naming
* @returns {string[]} Supported rule IDs
*/
getSupportedRules() {
return this.supportedRulesList;
}
/**
* Run enhanced analysis with multiple strategies
* Automatically selects optimal analysis method per rule
* Following Rule C006: Verb-noun naming
*/
async runEnhancedAnalysis(analyzer, ruleId, files, language, options) {
// Create debug config from options
const debugConfig = {
enabled: options.debug || options.verbose || false,
logger: (component, message) => console.log(`🔧 [${component}] ${message}`)
};
// Debug logging based on debug flag
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `runEnhancedAnalysis called: rule=${ruleId}, language=${language}, files=${files.length}`);
}
if (options.verbose) {
console.log(`🔧 [DEBUG] runEnhancedAnalysis called: rule=${ruleId}, language=${language}, files=${files.length}`);
}
// Load rule analysis strategy
const strategy = await this.getRuleAnalysisStrategy(ruleId, language);
// Debug logging based on debug flag
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `Rule ${ruleId}: ${strategy.primary} (approach: ${strategy.approach})`);
}
if (options.verbose) {
console.log(`🔧 [Strategy] Rule ${ruleId}: ${strategy.primary} (fallback: ${strategy.fallback || 'none'})`);
}
let violations = [];
let analysisResults = {
methods: [],
totalViolations: 0,
accuracy: 'unknown'
};
// Execute analysis based on strategy
switch (strategy.approach) {
case 'ast-primary':
violations = await this.runASTPrimaryAnalysis(analyzer, ruleId, files, language, options, strategy, debugConfig);
break;
case 'regex-optimal':
violations = await this.runRegexOptimalAnalysis(analyzer, ruleId, files, language, options);
break;
case 'hybrid-combined':
violations = await this.runHybridAnalysis(analyzer, ruleId, files, language, options, strategy);
break;
case 'progressive-enhancement':
violations = await this.runProgressiveAnalysis(analyzer, ruleId, files, language, options, strategy);
break;
default:
// Fallback to existing logic
violations = await this.runLegacyAnalysis(analyzer, ruleId, files, language, options);
}
if (options.verbose && violations.length > 0) {
console.log(`📊 [Analysis] Found ${violations.length} violations using ${strategy.approach}`);
}
return violations;
}
/**
* Get optimal analysis strategy for a rule
*/
async getRuleAnalysisStrategy(ruleId, language) {
try {
const strategies = require('../config/rule-analysis-strategies');
// Check AST-preferred rules
if (strategies.astPreferred[ruleId]) {
const astAvailable = this.astRegistry.isASTSupportAvailable(language);
return {
approach: 'ast-primary',
primary: 'ast',
fallback: 'regex',
astAvailable,
config: strategies.astPreferred[ruleId]
};
}
// Check regex-optimal rules
if (strategies.regexOptimal[ruleId]) {
return {
approach: 'regex-optimal',
primary: 'regex',
config: strategies.regexOptimal[ruleId]
};
}
// Check hybrid rules
if (strategies.hybridOptimal[ruleId]) {
return {
approach: 'hybrid-combined',
primary: strategies.hybridOptimal[ruleId].strategy.split('-')[0],
config: strategies.hybridOptimal[ruleId]
};
}
// Check experimental rules
if (strategies.experimental[ruleId]) {
return {
approach: 'progressive-enhancement',
primary: 'regex',
fallback: 'ast',
config: strategies.experimental[ruleId]
};
}
// Default strategy
return {
approach: 'ast-primary',
primary: 'ast',
fallback: 'regex',
astAvailable: this.astRegistry.isASTSupportAvailable(language)
};
} catch (error) {
// Fallback if strategy config is not available
return {
approach: 'ast-primary',
primary: 'ast',
fallback: 'regex',
astAvailable: this.astRegistry.isASTSupportAvailable(language)
};
}
}
/**
* AST-primary analysis: Try AST first, fallback to regex
*/
async runASTPrimaryAnalysis(analyzer, ruleId, files, language, options, strategy, debugConfig) {
const violations = [];
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `Starting AST-primary analysis for ${ruleId}, AST available: ${strategy.astAvailable}`);
}
for (const filePath of files) {
try {
const code = fs.readFileSync(filePath, 'utf8');
let analysisResult = null;
// Try AST analysis first if available
if (strategy.astAvailable) {
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `Attempting AST for file: ${filePath}`);
}
try {
const astResult = await this.astRegistry.analyzeRule(ruleId, code, language, filePath);
if (astResult && astResult.length > 0) {
analysisResult = astResult.map(violation => ({
...violation,
filePath,
analysisMethod: 'ast',
confidence: strategy.config?.accuracy?.ast || 90
}));
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `AST found ${astResult.length} violations in ${filePath}`);
}
}
} catch (astError) {
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `AST failed for ${filePath}: ${astError.message}`);
}
if (options.verbose) {
console.warn(`⚠️ AST analysis failed for ${filePath}, falling back to regex`);
}
}
} else {
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `AST not available, skipping to fallback for ${filePath}`);
}
}
// Fallback to regex if AST failed or not available
if (!analysisResult) {
if (debugConfig.enabled) {
debugConfig.logger(this.constructor.name, `Using regex fallback for ${filePath}`);
}
const regexResult = await analyzer.analyze([filePath], language, options);
if (regexResult && regexResult.length > 0) {
analysisResult = regexResult.map(violation => ({
...violation,
analysisMethod: 'regex',
confidence: strategy.config?.accuracy?.regex || 75
}));
}
}
if (analysisResult) {
violations.push(...analysisResult);
}
} catch (error) {
if (options.verbose) {
console.error(`❌ Analysis failed for ${filePath}:`, error.message);
}
}
}
return violations;
}
/**
* Regex-optimal analysis: Use regex as primary method
*/
async runRegexOptimalAnalysis(analyzer, ruleId, files, language, options) {
const violations = [];
const regexResult = await analyzer.analyze(files, language, options);
if (regexResult && regexResult.length > 0) {
violations.push(...regexResult.map(violation => ({
...violation,
analysisMethod: 'regex',
confidence: 95 // High confidence for regex-optimal rules
})));
}
return violations;
}
/**
* Hybrid analysis: Combine multiple methods for best results
*/
async runHybridAnalysis(analyzer, ruleId, files, language, options, strategy) {
const violations = [];
const astViolations = [];
const regexViolations = [];
for (const filePath of files) {
try {
const code = fs.readFileSync(filePath, 'utf8');
// Run both AST and regex analysis
const analysisPromises = [];
// AST analysis
if (this.astRegistry.isASTSupportAvailable(language)) {
analysisPromises.push(
this.astRegistry.analyzeRule(ruleId, code, language, filePath)
.then(result => ({ type: 'ast', result, filePath }))
.catch(() => ({ type: 'ast', result: null, filePath }))
);
}
// Regex analysis
analysisPromises.push(
analyzer.analyze([filePath], language, options)
.then(result => ({ type: 'regex', result, filePath }))
.catch(() => ({ type: 'regex', result: null, filePath }))
);
const results = await Promise.all(analysisPromises);
// Process results based on strategy
for (const { type, result, filePath } of results) {
if (result && result.length > 0) {
const violations = result.map(violation => ({
...violation,
filePath,
analysisMethod: type,
confidence: strategy.config?.accuracy?.[type] || 85
}));
if (type === 'ast') {
astViolations.push(...violations);
} else {
regexViolations.push(...violations);
}
}
}
} catch (error) {
if (options.verbose) {
console.error(`❌ Hybrid analysis failed for ${filePath}:`, error.message);
}
}
}
// Combine results intelligently
const combinedViolations = this.combineHybridResults(
astViolations,
regexViolations,
strategy.config?.strategy || 'ast-primary-regex-fallback'
);
return combinedViolations;
}
/**
* Progressive enhancement: Start simple, enhance with advanced methods
*/
async runProgressiveAnalysis(analyzer, ruleId, files, language, options, strategy) {
const violations = [];
// Start with basic regex analysis
const regexResult = await analyzer.analyze(files, language, options);
if (regexResult) {
violations.push(...regexResult.map(violation => ({
...violation,
analysisMethod: 'regex',
confidence: 75
})));
}
// Enhance with AST if available and beneficial
if (this.astRegistry.isASTSupportAvailable(language) && violations.length > 0) {
// TODO: Implement AST enhancement for specific violation types
// This could involve re-analyzing files with violations for better precision
}
return violations;
}
/**
* Legacy analysis method (for backward compatibility)
*/
async runLegacyAna