@sun-asterisk/sunlint 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/CONTRIBUTING.md +533 -70
- package/README.md +16 -2
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/rule-analysis-strategies.js +18 -2
- package/config/rules/enhanced-rules-registry.json +2503 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +32 -5
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +61 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +560 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +180 -30
- package/engines/heuristic-engine.js +513 -56
- package/integrations/eslint/plugin/index.js +27 -27
- package/package.json +11 -6
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/common/C006_function_naming/analyzer.js +504 -0
- package/rules/common/C006_function_naming/config.json +86 -0
- package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C012_command_query_separation/analyzer.js +481 -0
- package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +362 -0
- package/rules/common/C019_log_level_usage/config.json +121 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +141 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C031_validation_separation/analyzer.js +186 -0
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/docs/C031_validation_separation.md +72 -0
- package/rules/index.js +162 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/parser/constants.js +31 -0
- package/rules/parser/file-config.js +80 -0
- package/rules/parser/rule-parser-simple.js +305 -0
- package/rules/parser/rule-parser.js +527 -0
- package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
- package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
- package/rules/security/S023_no_json_injection/analyzer.js +278 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +330 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/generate_insights.js +188 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/testing/test-s005-working.ts +0 -22
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/scripts/merge-reports.js +0 -424
- package/scripts/test-scripts/README.md +0 -22
- package/scripts/test-scripts/test-c041-comparison.js +0 -114
- package/scripts/test-scripts/test-c041-eslint.js +0 -67
- package/scripts/test-scripts/test-eslint-rules.js +0 -146
- package/scripts/test-scripts/test-real-world.js +0 -44
- package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
- /package/{config/schemas/sunlint-schema.json → rules/universal/C010/generic.js} +0 -0
- /package/{core/multi-rule-runner.js → rules/universal/C010/tree-sitter-analyzer.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Heuristic Analysis Engine Plugin
|
|
3
|
-
* Following Rule C005: Single responsibility - Pattern-based analysis with
|
|
2
|
+
* Heuristic Analysis Engine Plugin with ts-morph Core Integration
|
|
3
|
+
* Following Rule C005: Single responsibility - Pattern-based analysis with ts-morph core
|
|
4
4
|
* Following Rule C014: Dependency injection - implements interface
|
|
5
5
|
* Following Rule C015: Use domain language - clear heuristic analysis terms
|
|
6
6
|
*/
|
|
@@ -9,21 +9,32 @@ const AnalysisEngineInterface = require('../core/interfaces/analysis-engine.inte
|
|
|
9
9
|
const ASTModuleRegistry = require('../core/ast-modules/index');
|
|
10
10
|
const dependencyChecker = require('../core/dependency-checker');
|
|
11
11
|
const SunlintRuleAdapter = require('../core/adapters/sunlint-rule-adapter');
|
|
12
|
+
const SemanticEngine = require('../core/semantic-engine');
|
|
13
|
+
const SemanticRuleBase = require('../core/semantic-rule-base');
|
|
14
|
+
const { getInstance: getUnifiedRegistry } = require('../core/unified-rule-registry');
|
|
12
15
|
const fs = require('fs');
|
|
13
16
|
const path = require('path');
|
|
14
17
|
|
|
15
18
|
class HeuristicEngine extends AnalysisEngineInterface {
|
|
16
19
|
constructor() {
|
|
17
|
-
super('heuristic', '
|
|
20
|
+
super('heuristic', '3.0', ['typescript', 'javascript', 'dart', 'swift', 'kotlin', 'java', 'python', 'go', 'rust', 'all']);
|
|
18
21
|
|
|
19
22
|
this.ruleAnalyzers = new Map();
|
|
20
23
|
this.supportedRulesList = [];
|
|
21
24
|
this.ruleAdapter = SunlintRuleAdapter.getInstance();
|
|
22
25
|
this.astRegistry = ASTModuleRegistry;
|
|
26
|
+
|
|
27
|
+
// ts-morph as core technology for heuristic engine
|
|
28
|
+
this.semanticEngine = new SemanticEngine();
|
|
29
|
+
this.semanticRules = new Map();
|
|
30
|
+
this.symbolTableEnabled = false;
|
|
31
|
+
|
|
32
|
+
// Unified rule registry
|
|
33
|
+
this.unifiedRegistry = getUnifiedRegistry();
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
/**
|
|
26
|
-
* Initialize Heuristic engine with configuration
|
|
37
|
+
* Initialize Heuristic engine with ts-morph core and configuration
|
|
27
38
|
* Following Rule C006: Verb-noun naming
|
|
28
39
|
* @param {Object} config - Engine configuration
|
|
29
40
|
*/
|
|
@@ -32,18 +43,27 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
32
43
|
// Store verbosity setting
|
|
33
44
|
this.verbose = config?.verbose || false;
|
|
34
45
|
|
|
46
|
+
// Initialize unified rule registry
|
|
47
|
+
await this.unifiedRegistry.initialize({ verbose: this.verbose });
|
|
48
|
+
|
|
35
49
|
// Check for optional AST dependencies
|
|
36
50
|
dependencyChecker.checkAndNotify('ast');
|
|
37
51
|
|
|
52
|
+
// Initialize ts-morph Symbol Table (core requirement)
|
|
53
|
+
await this.initializeSymbolTable(config);
|
|
54
|
+
|
|
38
55
|
// Initialize rule adapter
|
|
39
56
|
await this.ruleAdapter.initialize();
|
|
40
57
|
|
|
41
|
-
//
|
|
42
|
-
await this.
|
|
58
|
+
// Load available rules from unified registry
|
|
59
|
+
await this.loadRulesFromRegistry(config);
|
|
43
60
|
|
|
44
61
|
this.initialized = true;
|
|
45
62
|
if (this.verbose) {
|
|
46
|
-
console.log(`🔍 Heuristic engine
|
|
63
|
+
console.log(`🔍 Heuristic engine v3.0 initialized:`);
|
|
64
|
+
console.log(` 📊 Total rules: ${this.supportedRulesList.length}`);
|
|
65
|
+
console.log(` 🧠 Symbol Table: ${this.symbolTableInitialized ? 'enabled' : 'disabled'}`);
|
|
66
|
+
console.log(` 🔧 Semantic rules: ${this.semanticRules.size}`);
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
} catch (error) {
|
|
@@ -53,7 +73,171 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
/**
|
|
56
|
-
*
|
|
76
|
+
* Initialize ts-morph Symbol Table as core requirement
|
|
77
|
+
*/
|
|
78
|
+
async initializeSymbolTable(config) {
|
|
79
|
+
const projectPath = config?.projectPath || process.cwd();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// ts-morph is now a core dependency
|
|
83
|
+
const success = await this.semanticEngine.initialize(projectPath);
|
|
84
|
+
|
|
85
|
+
if (success) {
|
|
86
|
+
this.semanticEnabled = true;
|
|
87
|
+
this.symbolTableInitialized = true;
|
|
88
|
+
if (this.verbose) {
|
|
89
|
+
console.log(`🧠 Symbol Table initialized for: ${projectPath}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
if (this.verbose) {
|
|
93
|
+
console.warn('⚠️ Symbol Table initialization failed, using fallback mode');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (this.verbose) {
|
|
99
|
+
console.warn('⚠️ ts-morph Symbol Table unavailable:', error.message);
|
|
100
|
+
console.warn('⚠️ Falling back to traditional AST/regex analysis only');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Load rules from unified registry instead of scanning directories
|
|
107
|
+
* Following Rule C006: Verb-noun naming
|
|
108
|
+
* @param {Object} config - Engine configuration
|
|
109
|
+
*/
|
|
110
|
+
async loadRulesFromRegistry(config = {}) {
|
|
111
|
+
try {
|
|
112
|
+
// Get rules supported by heuristic engine from unified registry
|
|
113
|
+
const supportedRules = this.unifiedRegistry.getRulesForEngine('heuristic');
|
|
114
|
+
|
|
115
|
+
if (this.verbose) {
|
|
116
|
+
console.log(`🔍 [HeuristicEngine] Found ${supportedRules.length} rules from unified registry`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Load each rule
|
|
120
|
+
for (const ruleDefinition of supportedRules) {
|
|
121
|
+
await this.loadRuleFromDefinition(ruleDefinition);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Manually load C047 if needed (DEPRECATED - C047 now in enhanced registry)
|
|
125
|
+
// if (!this.semanticRules.has('C047') && !this.ruleAnalyzers.has('C047')) {
|
|
126
|
+
// await this.manuallyLoadC047();
|
|
127
|
+
// }
|
|
128
|
+
|
|
129
|
+
if (this.verbose) {
|
|
130
|
+
console.log(`✅ [HeuristicEngine] Loaded ${this.supportedRulesList.length} rules from unified registry`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Failed to load rules from registry:', error.message);
|
|
135
|
+
// Fallback to old scanning method
|
|
136
|
+
await this.scanRuleAnalyzers(config);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Load a single rule from its definition
|
|
142
|
+
* @param {Object} ruleDefinition - Rule definition from unified registry
|
|
143
|
+
*/
|
|
144
|
+
async loadRuleFromDefinition(ruleDefinition) {
|
|
145
|
+
const ruleId = ruleDefinition.id;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Resolve best analyzer path for this engine
|
|
149
|
+
const analyzerPath = this.unifiedRegistry.resolveAnalyzerPath(ruleId, 'heuristic');
|
|
150
|
+
|
|
151
|
+
if (!analyzerPath) {
|
|
152
|
+
if (this.verbose) {
|
|
153
|
+
console.warn(`⚠️ [HeuristicEngine] No compatible analyzer found for ${ruleId}`);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Determine analyzer type from path and strategy
|
|
159
|
+
const strategy = ruleDefinition.strategy.preferred;
|
|
160
|
+
const category = ruleDefinition.category;
|
|
161
|
+
|
|
162
|
+
if (strategy === 'semantic' && this.symbolTableInitialized) {
|
|
163
|
+
// Load as semantic rule
|
|
164
|
+
await this.loadSemanticRule(ruleId, analyzerPath, { category });
|
|
165
|
+
} else {
|
|
166
|
+
// Load as traditional rule (ast/regex)
|
|
167
|
+
await this.loadTraditionalRule(ruleId, analyzerPath, { category, type: strategy });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (this.verbose) {
|
|
172
|
+
console.warn(`⚠️ [HeuristicEngine] Failed to load rule ${ruleId}:`, error.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load semantic rule from analyzer path
|
|
179
|
+
* @param {string} ruleId - Rule ID
|
|
180
|
+
* @param {string} analyzerPath - Path to analyzer file
|
|
181
|
+
* @param {Object} metadata - Rule metadata
|
|
182
|
+
*/
|
|
183
|
+
async loadSemanticRule(ruleId, analyzerPath, metadata) {
|
|
184
|
+
try {
|
|
185
|
+
const SemanticRuleClass = require(analyzerPath);
|
|
186
|
+
|
|
187
|
+
// Verify it extends SemanticRuleBase
|
|
188
|
+
if (this.isSemanticRule(SemanticRuleClass)) {
|
|
189
|
+
await this.registerSemanticRule(ruleId, SemanticRuleClass, {
|
|
190
|
+
path: analyzerPath,
|
|
191
|
+
category: metadata.category
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (this.verbose) {
|
|
195
|
+
console.log(`🧠 [HeuristicEngine] Loaded semantic rule: ${ruleId}`);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Not a semantic rule, fallback to traditional
|
|
199
|
+
await this.loadTraditionalRule(ruleId, analyzerPath, metadata);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (this.verbose) {
|
|
204
|
+
console.warn(`⚠️ [HeuristicEngine] Failed to load semantic rule ${ruleId}:`, error.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Load traditional rule (ast/regex) from analyzer path
|
|
211
|
+
* @param {string} ruleId - Rule ID
|
|
212
|
+
* @param {string} analyzerPath - Path to analyzer file
|
|
213
|
+
* @param {Object} metadata - Rule metadata
|
|
214
|
+
*/
|
|
215
|
+
async loadTraditionalRule(ruleId, analyzerPath, metadata) {
|
|
216
|
+
try {
|
|
217
|
+
const analyzerModule = require(analyzerPath);
|
|
218
|
+
const AnalyzerClass = analyzerModule.default || analyzerModule;
|
|
219
|
+
|
|
220
|
+
this.registerTraditionalRule(ruleId, AnalyzerClass, {
|
|
221
|
+
path: analyzerPath,
|
|
222
|
+
category: metadata.category,
|
|
223
|
+
folder: ruleId, // Add folder name for config loading
|
|
224
|
+
type: metadata.type || 'regex'
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (this.verbose) {
|
|
228
|
+
console.log(`🔧 [HeuristicEngine] Loaded ${metadata.type} rule: ${ruleId}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (this.verbose) {
|
|
233
|
+
console.warn(`⚠️ [HeuristicEngine] Failed to load traditional rule ${ruleId}:`, error.message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Scan for available rule analyzers with semantic support
|
|
240
|
+
* Priority: semantic > ast > regex
|
|
57
241
|
* Following Rule C006: Verb-noun naming
|
|
58
242
|
*/
|
|
59
243
|
async scanRuleAnalyzers(config = {}) {
|
|
@@ -80,56 +264,195 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
80
264
|
.map(dirent => dirent.name);
|
|
81
265
|
|
|
82
266
|
for (const ruleFolder of ruleFolders) {
|
|
83
|
-
const ruleId =
|
|
84
|
-
const
|
|
267
|
+
const ruleId = ruleFolder; // Use folder name directly as rule ID
|
|
268
|
+
const rulePath = path.join(categoryPath, ruleFolder);
|
|
85
269
|
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// Load analyzer dynamically - handle both class and instance exports
|
|
89
|
-
const analyzerModule = require(analyzerPath);
|
|
90
|
-
const analyzer = analyzerModule.default || analyzerModule;
|
|
91
|
-
|
|
92
|
-
// Check if it's a class constructor, instance, or factory function
|
|
93
|
-
if (typeof analyzer === 'function') {
|
|
94
|
-
// It's a class constructor
|
|
95
|
-
this.ruleAnalyzers.set(ruleId, {
|
|
96
|
-
path: analyzerPath,
|
|
97
|
-
class: analyzer,
|
|
98
|
-
folder: ruleFolder,
|
|
99
|
-
category: categoryFolder,
|
|
100
|
-
type: 'class'
|
|
101
|
-
});
|
|
102
|
-
this.supportedRulesList.push(ruleId);
|
|
103
|
-
} else if (analyzer && typeof analyzer === 'object' && analyzer.analyze) {
|
|
104
|
-
// It's an analyzer instance with analyze method
|
|
105
|
-
this.ruleAnalyzers.set(ruleId, {
|
|
106
|
-
path: analyzerPath,
|
|
107
|
-
instance: analyzer,
|
|
108
|
-
folder: ruleFolder,
|
|
109
|
-
category: categoryFolder,
|
|
110
|
-
type: 'instance'
|
|
111
|
-
});
|
|
112
|
-
this.supportedRulesList.push(ruleId);
|
|
113
|
-
} else {
|
|
114
|
-
console.warn(`⚠️ Analyzer for ${ruleId} has unsupported format:`, typeof analyzer);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
} catch (error) {
|
|
118
|
-
console.warn(`⚠️ Failed to load analyzer for ${ruleId}:`, error.message);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
270
|
+
await this.loadRuleAnalyzer(ruleId, rulePath, categoryFolder);
|
|
121
271
|
}
|
|
122
272
|
}
|
|
273
|
+
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.warn('⚠️ Error scanning rule analyzers:', error.message);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
123
278
|
|
|
124
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Manually load C047 semantic rule (special case)
|
|
281
|
+
*/
|
|
282
|
+
async manuallyLoadC047() {
|
|
283
|
+
try {
|
|
125
284
|
if (this.verbose) {
|
|
126
|
-
console.log(
|
|
127
|
-
console.log(`🔍 Supported rules: ${this.supportedRulesList.join(', ')}`);
|
|
285
|
+
console.log(`[DEBUG] 🔬 Manually loading C047 semantic rule...`);
|
|
128
286
|
}
|
|
287
|
+
|
|
288
|
+
const c047Path = path.resolve(__dirname, '../rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js');
|
|
289
|
+
|
|
290
|
+
if (fs.existsSync(c047Path)) {
|
|
291
|
+
const C047SemanticRule = require(c047Path);
|
|
292
|
+
const instance = new C047SemanticRule();
|
|
293
|
+
|
|
294
|
+
// Register as semantic rule
|
|
295
|
+
await this.registerSemanticRule('C047', C047SemanticRule, {
|
|
296
|
+
path: c047Path,
|
|
297
|
+
category: 'common',
|
|
298
|
+
type: 'semantic',
|
|
299
|
+
description: 'C047 - No Duplicate Retry Logic (Semantic Analysis)'
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (this.verbose) {
|
|
303
|
+
console.log(`[DEBUG] ✅ C047 semantic rule loaded successfully`);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
console.warn(`⚠️ C047 semantic rule not found at: ${c047Path}`);
|
|
129
307
|
}
|
|
130
308
|
|
|
131
309
|
} catch (error) {
|
|
132
|
-
console.
|
|
310
|
+
console.warn(`⚠️ Failed to manually load C047:`, error.message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Load rule analyzer with semantic priority
|
|
316
|
+
*/
|
|
317
|
+
async loadRuleAnalyzer(ruleId, rulePath, categoryFolder) {
|
|
318
|
+
// Analyzer priority: semantic > ast > regex
|
|
319
|
+
const analyzerCandidates = [
|
|
320
|
+
{ path: path.join(rulePath, 'semantic-analyzer.js'), type: 'semantic' },
|
|
321
|
+
{ path: path.join(rulePath, 'ast-analyzer.js'), type: 'ast' },
|
|
322
|
+
{ path: path.join(rulePath, 'regex-analyzer.js'), type: 'regex' },
|
|
323
|
+
{ path: path.join(rulePath, 'analyzer.js'), type: 'regex' } // legacy fallback
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
let selectedAnalyzer = null;
|
|
327
|
+
let analyzerPath = null;
|
|
328
|
+
let analyzerType = null;
|
|
329
|
+
|
|
330
|
+
// Try semantic analyzer first if Symbol Table available
|
|
331
|
+
if (this.symbolTableInitialized) {
|
|
332
|
+
const semanticCandidate = analyzerCandidates[0];
|
|
333
|
+
if (fs.existsSync(semanticCandidate.path)) {
|
|
334
|
+
try {
|
|
335
|
+
const analyzerModule = require(semanticCandidate.path);
|
|
336
|
+
selectedAnalyzer = analyzerModule.default || analyzerModule;
|
|
337
|
+
analyzerPath = semanticCandidate.path;
|
|
338
|
+
analyzerType = 'semantic';
|
|
339
|
+
|
|
340
|
+
// Verify it extends SemanticRuleBase
|
|
341
|
+
if (this.isSemanticRule(selectedAnalyzer)) {
|
|
342
|
+
await this.registerSemanticRule(ruleId, selectedAnalyzer, {
|
|
343
|
+
path: analyzerPath,
|
|
344
|
+
category: categoryFolder
|
|
345
|
+
});
|
|
346
|
+
return; // Successfully registered semantic rule
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.debug(`Semantic analyzer for ${ruleId} failed to load:`, error.message);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Fall back to AST analyzer
|
|
355
|
+
const astCandidate = analyzerCandidates[1];
|
|
356
|
+
if (fs.existsSync(astCandidate.path)) {
|
|
357
|
+
try {
|
|
358
|
+
const analyzerModule = require(astCandidate.path);
|
|
359
|
+
selectedAnalyzer = analyzerModule.default || analyzerModule;
|
|
360
|
+
analyzerPath = astCandidate.path;
|
|
361
|
+
analyzerType = 'ast';
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.debug(`AST analyzer for ${ruleId} failed to load:`, error.message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Fall back to regex analyzer
|
|
368
|
+
if (!selectedAnalyzer) {
|
|
369
|
+
for (const regexCandidate of analyzerCandidates.slice(2)) {
|
|
370
|
+
if (fs.existsSync(regexCandidate.path)) {
|
|
371
|
+
try {
|
|
372
|
+
const analyzerModule = require(regexCandidate.path);
|
|
373
|
+
selectedAnalyzer = analyzerModule.default || analyzerModule;
|
|
374
|
+
analyzerPath = regexCandidate.path;
|
|
375
|
+
analyzerType = 'regex';
|
|
376
|
+
break;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.debug(`Regex analyzer for ${ruleId} failed to load:`, error.message);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Register traditional (non-semantic) analyzer
|
|
385
|
+
if (selectedAnalyzer) {
|
|
386
|
+
this.registerTraditionalRule(ruleId, selectedAnalyzer, {
|
|
387
|
+
path: analyzerPath,
|
|
388
|
+
category: categoryFolder,
|
|
389
|
+
folder: fullRuleId, // Add folder name for config loading
|
|
390
|
+
type: analyzerType
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Check if analyzer is a semantic rule
|
|
397
|
+
*/
|
|
398
|
+
isSemanticRule(analyzerClass) {
|
|
399
|
+
if (typeof analyzerClass !== 'function') return false;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const instance = new analyzerClass(analyzerClass.name);
|
|
403
|
+
return instance instanceof SemanticRuleBase;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Register semantic rule
|
|
411
|
+
*/
|
|
412
|
+
async registerSemanticRule(ruleId, analyzerClass, metadata) {
|
|
413
|
+
try {
|
|
414
|
+
const instance = new analyzerClass(ruleId);
|
|
415
|
+
instance.initialize(this.semanticEngine);
|
|
416
|
+
|
|
417
|
+
this.semanticRules.set(ruleId, {
|
|
418
|
+
instance,
|
|
419
|
+
metadata,
|
|
420
|
+
type: 'semantic'
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
this.supportedRulesList.push(ruleId);
|
|
424
|
+
|
|
425
|
+
if (this.verbose) {
|
|
426
|
+
console.log(`🧠 Registered semantic rule: ${ruleId}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.warn(`⚠️ Failed to register semantic rule ${ruleId}:`, error.message);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Register traditional (heuristic/AST/regex) rule
|
|
436
|
+
*/
|
|
437
|
+
registerTraditionalRule(ruleId, analyzer, metadata) {
|
|
438
|
+
if (typeof analyzer === 'function') {
|
|
439
|
+
// Class constructor
|
|
440
|
+
this.ruleAnalyzers.set(ruleId, {
|
|
441
|
+
...metadata,
|
|
442
|
+
class: analyzer,
|
|
443
|
+
type: 'class'
|
|
444
|
+
});
|
|
445
|
+
this.supportedRulesList.push(ruleId);
|
|
446
|
+
} else if (analyzer && typeof analyzer === 'object' && analyzer.analyze) {
|
|
447
|
+
// Instance with analyze method
|
|
448
|
+
this.ruleAnalyzers.set(ruleId, {
|
|
449
|
+
...metadata,
|
|
450
|
+
instance: analyzer,
|
|
451
|
+
type: 'instance'
|
|
452
|
+
});
|
|
453
|
+
this.supportedRulesList.push(ruleId);
|
|
454
|
+
} else {
|
|
455
|
+
console.warn(`⚠️ Analyzer for ${ruleId} has unsupported format:`, typeof analyzer);
|
|
133
456
|
}
|
|
134
457
|
}
|
|
135
458
|
|
|
@@ -145,6 +468,60 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
145
468
|
return match ? match[1] : folderName;
|
|
146
469
|
}
|
|
147
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Get full rule ID from short rule ID (C029 -> C029_catch_block_logging)
|
|
473
|
+
* @param {string} ruleId - Short rule ID
|
|
474
|
+
* @returns {string} Full rule ID or original if not found
|
|
475
|
+
*/
|
|
476
|
+
getFullRuleId(ruleId) {
|
|
477
|
+
// Check exact match first
|
|
478
|
+
if (this.ruleAnalyzers.has(ruleId)) {
|
|
479
|
+
return ruleId;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Find full rule ID that starts with short rule ID
|
|
483
|
+
const shortRulePattern = new RegExp(`^${ruleId}_`);
|
|
484
|
+
const fullRuleId = Array.from(this.ruleAnalyzers.keys()).find(fullId => shortRulePattern.test(fullId));
|
|
485
|
+
|
|
486
|
+
return fullRuleId || ruleId; // Return original if not found
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Check if a rule is supported by this engine
|
|
491
|
+
* Following Rule C006: Verb-noun naming
|
|
492
|
+
* @param {string} ruleId - Rule ID to check
|
|
493
|
+
* @returns {boolean} True if rule is supported
|
|
494
|
+
*/
|
|
495
|
+
isRuleSupported(ruleId) {
|
|
496
|
+
// Special case: C047 is always supported (loaded on-demand)
|
|
497
|
+
if (ruleId === 'C047') {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Use unified registry for primary lookup
|
|
502
|
+
if (this.unifiedRegistry && this.unifiedRegistry.initialized) {
|
|
503
|
+
return this.unifiedRegistry.isRuleSupported(ruleId, 'heuristic');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Fallback to original logic for backward compatibility
|
|
507
|
+
return this.supportedRulesList.includes(ruleId) ||
|
|
508
|
+
this.semanticRules.has(ruleId) ||
|
|
509
|
+
this.ruleAnalyzers.has(ruleId) ||
|
|
510
|
+
this.checkShortRuleIdMatch(ruleId);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Check short rule ID matches (backward compatibility)
|
|
515
|
+
* @param {string} ruleId - Short rule ID (e.g., C029)
|
|
516
|
+
* @returns {boolean} True if matches any full rule ID
|
|
517
|
+
*/
|
|
518
|
+
checkShortRuleIdMatch(ruleId) {
|
|
519
|
+
const shortRulePattern = new RegExp(`^${ruleId}_`);
|
|
520
|
+
return this.supportedRulesList.some(fullRuleId => shortRulePattern.test(fullRuleId)) ||
|
|
521
|
+
Array.from(this.semanticRules.keys()).some(fullRuleId => shortRulePattern.test(fullRuleId)) ||
|
|
522
|
+
Array.from(this.ruleAnalyzers.keys()).some(fullRuleId => shortRulePattern.test(fullRuleId));
|
|
523
|
+
}
|
|
524
|
+
|
|
148
525
|
/**
|
|
149
526
|
* Analyze files using heuristic patterns
|
|
150
527
|
* Following Rule C006: Verb-noun naming
|
|
@@ -178,6 +555,14 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
178
555
|
const filesByLanguage = this.groupFilesByLanguage(files);
|
|
179
556
|
|
|
180
557
|
for (const rule of rules) {
|
|
558
|
+
// Special case: Load C047 semantic rule on-demand
|
|
559
|
+
if (rule.id === 'C047' && !this.semanticRules.has('C047')) {
|
|
560
|
+
if (options.verbose) {
|
|
561
|
+
console.log(`🔬 [HeuristicEngine] Loading C047 semantic rule on-demand...`);
|
|
562
|
+
}
|
|
563
|
+
await this.manuallyLoadC047();
|
|
564
|
+
}
|
|
565
|
+
|
|
181
566
|
if (!this.isRuleSupported(rule.id)) {
|
|
182
567
|
if (options.verbose) {
|
|
183
568
|
console.warn(`⚠️ Rule ${rule.id} not supported by Heuristic engine, skipping...`);
|
|
@@ -186,7 +571,21 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
186
571
|
}
|
|
187
572
|
|
|
188
573
|
try {
|
|
189
|
-
|
|
574
|
+
let ruleViolations = [];
|
|
575
|
+
|
|
576
|
+
// Check if this is a semantic rule first (higher priority)
|
|
577
|
+
if (this.semanticRules.has(rule.id)) {
|
|
578
|
+
if (options.verbose) {
|
|
579
|
+
console.log(`🧠 [HeuristicEngine] Running semantic analysis for rule ${rule.id}`);
|
|
580
|
+
}
|
|
581
|
+
ruleViolations = await this.analyzeSemanticRule(rule, files, options);
|
|
582
|
+
} else {
|
|
583
|
+
// Fallback to traditional analysis
|
|
584
|
+
if (options.verbose) {
|
|
585
|
+
console.log(`🔧 [HeuristicEngine] Running traditional analysis for rule ${rule.id}`);
|
|
586
|
+
}
|
|
587
|
+
ruleViolations = await this.analyzeRule(rule, filesByLanguage, options);
|
|
588
|
+
}
|
|
190
589
|
|
|
191
590
|
if (ruleViolations.length > 0) {
|
|
192
591
|
// Group violations by file
|
|
@@ -214,6 +613,59 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
214
613
|
return results;
|
|
215
614
|
}
|
|
216
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Analyze semantic rule across files
|
|
618
|
+
* Following Rule C006: Verb-noun naming
|
|
619
|
+
* @param {Object} rule - Rule to analyze
|
|
620
|
+
* @param {string[]} files - Files to analyze
|
|
621
|
+
* @param {Object} options - Analysis options
|
|
622
|
+
* @returns {Promise<Object[]>} Rule violations
|
|
623
|
+
*/
|
|
624
|
+
async analyzeSemanticRule(rule, files, options) {
|
|
625
|
+
const semanticRuleInfo = this.semanticRules.get(rule.id);
|
|
626
|
+
if (!semanticRuleInfo) {
|
|
627
|
+
console.warn(`⚠️ Semantic rule ${rule.id} not found`);
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const ruleInstance = semanticRuleInfo.instance;
|
|
633
|
+
const allViolations = [];
|
|
634
|
+
|
|
635
|
+
// Run semantic analysis for each file
|
|
636
|
+
for (const filePath of files) {
|
|
637
|
+
try {
|
|
638
|
+
if (options.verbose) {
|
|
639
|
+
console.log(`🧠 [SemanticRule] Analyzing ${path.basename(filePath)} with ${rule.id}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Call semantic rule's analyzeFile method
|
|
643
|
+
await ruleInstance.analyzeFile(filePath, options);
|
|
644
|
+
|
|
645
|
+
// Get violations from the rule instance
|
|
646
|
+
const fileViolations = ruleInstance.getViolations();
|
|
647
|
+
allViolations.push(...fileViolations);
|
|
648
|
+
|
|
649
|
+
// Clear violations for next file
|
|
650
|
+
ruleInstance.clearViolations();
|
|
651
|
+
|
|
652
|
+
} catch (fileError) {
|
|
653
|
+
console.warn(`⚠️ Semantic rule ${rule.id} failed for ${filePath}:`, fileError.message);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (options.verbose && allViolations.length > 0) {
|
|
658
|
+
console.log(`🧠 [SemanticRule] Found ${allViolations.length} violations for ${rule.id}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return allViolations;
|
|
662
|
+
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.error(`❌ Failed to run semantic rule ${rule.id}:`, error.message);
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
217
669
|
/**
|
|
218
670
|
* Analyze a specific rule across files
|
|
219
671
|
* Following Rule C006: Verb-noun naming
|
|
@@ -223,21 +675,23 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
223
675
|
* @returns {Promise<Object[]>} Rule violations
|
|
224
676
|
*/
|
|
225
677
|
async analyzeRule(rule, filesByLanguage, options) {
|
|
226
|
-
|
|
678
|
+
// Get full rule ID (C029 -> C029_catch_block_logging)
|
|
679
|
+
const fullRuleId = this.getFullRuleId(rule.id);
|
|
680
|
+
const analyzerInfo = this.ruleAnalyzers.get(fullRuleId);
|
|
681
|
+
|
|
227
682
|
if (!analyzerInfo) {
|
|
228
683
|
return [];
|
|
229
684
|
}
|
|
230
685
|
|
|
231
686
|
try {
|
|
232
687
|
// Get analyzer - handle both class and instance types
|
|
233
|
-
const analyzerInfo = this.ruleAnalyzers.get(rule.id);
|
|
234
688
|
let analyzer;
|
|
235
689
|
|
|
236
690
|
if (analyzerInfo.type === 'class') {
|
|
237
691
|
// Create analyzer instance from class
|
|
238
692
|
const AnalyzerClass = analyzerInfo.class;
|
|
239
693
|
try {
|
|
240
|
-
analyzer = new AnalyzerClass();
|
|
694
|
+
analyzer = new AnalyzerClass({ verbose: options.verbose });
|
|
241
695
|
} catch (constructorError) {
|
|
242
696
|
throw new Error(`Failed to instantiate analyzer class: ${constructorError.message}`);
|
|
243
697
|
}
|
|
@@ -264,7 +718,7 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
264
718
|
|
|
265
719
|
try {
|
|
266
720
|
// Load rule config
|
|
267
|
-
const ruleConfig = await this.loadRuleConfig(rule.id, analyzerInfo.folder, analyzerInfo.category);
|
|
721
|
+
const ruleConfig = await this.loadRuleConfig(rule.id, analyzerInfo.folder, analyzerInfo.category, options.verbose);
|
|
268
722
|
|
|
269
723
|
// Run analysis with AST enhancement
|
|
270
724
|
if (options.verbose) {
|
|
@@ -330,16 +784,19 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
330
784
|
* @param {string} ruleId - Rule ID
|
|
331
785
|
* @param {string} ruleFolder - Rule folder name
|
|
332
786
|
* @param {string} category - Rule category (common, security, etc)
|
|
787
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
333
788
|
* @returns {Promise<Object>} Rule configuration
|
|
334
789
|
*/
|
|
335
|
-
async loadRuleConfig(ruleId, ruleFolder, category = 'common') {
|
|
790
|
+
async loadRuleConfig(ruleId, ruleFolder, category = 'common', verbose = false) {
|
|
336
791
|
try {
|
|
337
792
|
const configPath = path.resolve(__dirname, '../rules', category, ruleFolder, 'config.json');
|
|
338
793
|
if (fs.existsSync(configPath)) {
|
|
339
794
|
return require(configPath);
|
|
340
795
|
}
|
|
341
796
|
} catch (error) {
|
|
342
|
-
|
|
797
|
+
if (verbose) {
|
|
798
|
+
console.warn(`[DEBUG] ⚠️ Failed to load config for ${ruleId}:`, error.message);
|
|
799
|
+
}
|
|
343
800
|
}
|
|
344
801
|
|
|
345
802
|
// Return minimal config
|