@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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class C041ASTAnalyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'C041';
|
|
7
|
+
this.ruleName = 'No Hardcoded Sensitive Information (AST-Enhanced)';
|
|
8
|
+
this.description = 'AST-based detection of hardcoded sensitive information - superior to regex approach';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze(files, language, options = {}) {
|
|
12
|
+
const violations = [];
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
if (options.verbose) {
|
|
16
|
+
console.log(`🎯 Running C041 AST analysis on ${path.basename(filePath)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
22
|
+
violations.push(...fileViolations);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return violations;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async analyzeFile(filePath, content, language, config) {
|
|
32
|
+
switch (language) {
|
|
33
|
+
case 'typescript':
|
|
34
|
+
case 'javascript':
|
|
35
|
+
return this.analyzeJSTS(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeJSTS(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Try AST analysis first (like ESLint approach)
|
|
46
|
+
const astViolations = await this.analyzeWithAST(filePath, content, config);
|
|
47
|
+
if (astViolations.length > 0) {
|
|
48
|
+
violations.push(...astViolations);
|
|
49
|
+
}
|
|
50
|
+
} catch (astError) {
|
|
51
|
+
if (config.verbose) {
|
|
52
|
+
console.log(`⚠️ AST analysis failed for ${path.basename(filePath)}, falling back to regex`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback to regex-based analysis
|
|
56
|
+
const regexViolations = await this.analyzeWithRegex(filePath, content, config);
|
|
57
|
+
violations.push(...regexViolations);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async analyzeWithAST(filePath, content, config) {
|
|
64
|
+
const violations = [];
|
|
65
|
+
|
|
66
|
+
// Import AST modules dynamically
|
|
67
|
+
let astModules;
|
|
68
|
+
try {
|
|
69
|
+
astModules = require('../../../core/ast-modules');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error('AST modules not available');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try to parse with AST
|
|
75
|
+
let ast;
|
|
76
|
+
try {
|
|
77
|
+
// Use the registry's parseCode method
|
|
78
|
+
ast = await astModules.parseCode(content, 'javascript', filePath);
|
|
79
|
+
if (!ast) {
|
|
80
|
+
throw new Error('AST parsing returned null');
|
|
81
|
+
}
|
|
82
|
+
} catch (parseError) {
|
|
83
|
+
throw new Error(`Parse error: ${parseError.message}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Traverse AST to find sensitive information - mimicking ESLint's approach
|
|
87
|
+
const rootNode = ast.program || ast; // Handle both Babel and ESLint formats
|
|
88
|
+
this.traverseAST(rootNode, (node) => {
|
|
89
|
+
if (this.isLiteralNode(node)) {
|
|
90
|
+
const violation = this.checkLiteralForSensitiveInfo(node, filePath, content);
|
|
91
|
+
if (violation) {
|
|
92
|
+
violations.push(violation);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.isTemplateLiteralNode(node)) {
|
|
97
|
+
const violation = this.checkTemplateLiteralForSensitiveInfo(node, filePath, content);
|
|
98
|
+
if (violation) {
|
|
99
|
+
violations.push(violation);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
traverseAST(node, callback) {
|
|
108
|
+
if (!node || typeof node !== 'object') return;
|
|
109
|
+
|
|
110
|
+
callback(node);
|
|
111
|
+
|
|
112
|
+
for (const key in node) {
|
|
113
|
+
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
114
|
+
|
|
115
|
+
const child = node[key];
|
|
116
|
+
if (Array.isArray(child)) {
|
|
117
|
+
child.forEach(item => this.traverseAST(item, callback));
|
|
118
|
+
} else if (child && typeof child === 'object') {
|
|
119
|
+
this.traverseAST(child, callback);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isLiteralNode(node) {
|
|
125
|
+
// Support both ESLint format (Literal) and Babel format (StringLiteral)
|
|
126
|
+
return node && (node.type === 'Literal' || node.type === 'StringLiteral') && typeof node.value === 'string';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isTemplateLiteralNode(node) {
|
|
130
|
+
return node && node.type === 'TemplateLiteral' &&
|
|
131
|
+
node.quasis && node.quasis.length === 1 && // No variable interpolation
|
|
132
|
+
node.expressions && node.expressions.length === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
checkLiteralForSensitiveInfo(node, filePath, content) {
|
|
136
|
+
const value = node.value;
|
|
137
|
+
if (!value || value.length < 4) return null;
|
|
138
|
+
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
const lineNumber = node.loc.start.line;
|
|
141
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
142
|
+
|
|
143
|
+
// Skip if it's in UI/component context - same as ESLint
|
|
144
|
+
if (this.isFalsePositive(value, lineText)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check against sensitive patterns - enhanced version of ESLint patterns
|
|
149
|
+
const sensitivePattern = this.detectSensitivePattern(value, lineText);
|
|
150
|
+
if (sensitivePattern) {
|
|
151
|
+
return {
|
|
152
|
+
ruleId: this.ruleId,
|
|
153
|
+
file: filePath,
|
|
154
|
+
line: lineNumber,
|
|
155
|
+
column: node.loc.start.column + 1,
|
|
156
|
+
message: sensitivePattern.message,
|
|
157
|
+
severity: 'warning', // Match ESLint severity
|
|
158
|
+
code: lineText.trim(),
|
|
159
|
+
type: sensitivePattern.type,
|
|
160
|
+
confidence: sensitivePattern.confidence,
|
|
161
|
+
suggestion: sensitivePattern.suggestion
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
checkTemplateLiteralForSensitiveInfo(node, filePath, content) {
|
|
169
|
+
if (!node.quasis || node.quasis.length !== 1) return null;
|
|
170
|
+
|
|
171
|
+
const value = node.quasis[0].value.raw;
|
|
172
|
+
if (!value || value.length < 4) return null;
|
|
173
|
+
|
|
174
|
+
// Create a mock literal node for consistent processing
|
|
175
|
+
const mockNode = {
|
|
176
|
+
...node,
|
|
177
|
+
value: value,
|
|
178
|
+
loc: node.loc
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return this.checkLiteralForSensitiveInfo(mockNode, filePath, content);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
detectSensitivePattern(value, lineText) {
|
|
185
|
+
const lowerValue = value.toLowerCase();
|
|
186
|
+
const lowerLine = lineText.toLowerCase();
|
|
187
|
+
|
|
188
|
+
// Enhanced patterns based on ESLint rule but with better detection
|
|
189
|
+
const sensitivePatterns = [
|
|
190
|
+
{
|
|
191
|
+
type: 'password',
|
|
192
|
+
condition: () => /password/i.test(lineText) && value.length >= 4,
|
|
193
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
194
|
+
confidence: 0.8,
|
|
195
|
+
suggestion: 'Move sensitive values to environment variables or secure config files'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'secret',
|
|
199
|
+
condition: () => /secret/i.test(lineText) && value.length >= 6,
|
|
200
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
201
|
+
confidence: 0.8,
|
|
202
|
+
suggestion: 'Use environment variables for secrets'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
type: 'api_key',
|
|
206
|
+
condition: () => /api[_-]?key/i.test(lineText) && value.length >= 10,
|
|
207
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
208
|
+
confidence: 0.9,
|
|
209
|
+
suggestion: 'Use environment variables for API keys'
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
type: 'auth_token',
|
|
213
|
+
condition: () => /auth[_-]?token/i.test(lineText) && value.length >= 16,
|
|
214
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
215
|
+
confidence: 0.9,
|
|
216
|
+
suggestion: 'Store tokens in secure storage'
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'access_token',
|
|
220
|
+
condition: () => /access[_-]?token/i.test(lineText) && value.length >= 16,
|
|
221
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
222
|
+
confidence: 0.9,
|
|
223
|
+
suggestion: 'Store tokens in secure storage'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'database_url',
|
|
227
|
+
condition: () => /(mongodb|mysql|postgres|redis):\/\//i.test(value) && value.length >= 10,
|
|
228
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
229
|
+
confidence: 0.95,
|
|
230
|
+
suggestion: 'Use environment variables for database connections'
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const pattern of sensitivePatterns) {
|
|
235
|
+
if (pattern.condition()) {
|
|
236
|
+
return pattern;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isFalsePositive(value, sourceCode) {
|
|
244
|
+
const lowerValue = value.toLowerCase();
|
|
245
|
+
const lowerLine = sourceCode.toLowerCase();
|
|
246
|
+
|
|
247
|
+
// Global false positive indicators - same as ESLint
|
|
248
|
+
const globalFalsePositives = [
|
|
249
|
+
'test', 'mock', 'example', 'demo', 'sample', 'placeholder', 'dummy', 'fake',
|
|
250
|
+
'xmlns', 'namespace', 'schema', 'w3.org', 'google.com', 'googleapis.com',
|
|
251
|
+
'error', 'message', 'missing', 'invalid', 'failed', 'localhost', '127.0.0.1'
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
// Check global false positives
|
|
255
|
+
if (globalFalsePositives.some(pattern => lowerValue.includes(pattern))) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if line context suggests UI/component usage - same as ESLint
|
|
260
|
+
if (this.isConfigOrUIContext(lowerLine)) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
isConfigOrUIContext(line) {
|
|
268
|
+
// Same logic as ESLint rule
|
|
269
|
+
const uiContexts = [
|
|
270
|
+
'inputtype', 'type:', 'type =', 'inputtype=',
|
|
271
|
+
'routes =', 'route:', 'path:', 'routes:',
|
|
272
|
+
'import {', 'export {', 'from ', 'import ',
|
|
273
|
+
'interface', 'type ', 'enum ',
|
|
274
|
+
'props:', 'defaultprops',
|
|
275
|
+
'schema', 'validator',
|
|
276
|
+
'hook', 'use', 'const use', 'import.*use',
|
|
277
|
+
// React/UI specific
|
|
278
|
+
'textinput', 'input ', 'field ', 'form',
|
|
279
|
+
'component', 'page', 'screen', 'modal',
|
|
280
|
+
// Route/navigation specific
|
|
281
|
+
'navigation', 'route', 'path', 'url:', 'route:',
|
|
282
|
+
'setuppassword', 'resetpassword', 'forgotpassword',
|
|
283
|
+
'changepassword', 'confirmpassword'
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
return uiContexts.some(context => line.includes(context));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async analyzeWithRegex(filePath, content, config) {
|
|
290
|
+
// Fallback to original regex approach if AST fails
|
|
291
|
+
const originalAnalyzer = require('./analyzer.js');
|
|
292
|
+
return originalAnalyzer.analyzeTypeScript(filePath, content, config);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = new C041ASTAnalyzer();
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic analyzer for: C042 – Boolean variable names should start with proper prefixes
|
|
3
|
+
* Purpose: Detect boolean variables that don't follow naming conventions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class C042Analyzer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.ruleId = 'C042';
|
|
9
|
+
this.ruleName = 'Boolean Variable Naming';
|
|
10
|
+
this.description = 'Boolean variable names should start with is, has, should, can, will, must, may, or check';
|
|
11
|
+
|
|
12
|
+
// User requested to add "check" prefix
|
|
13
|
+
this.booleanPrefixes = [
|
|
14
|
+
'is', 'has', 'should', 'can', 'will', 'must', 'may', 'check',
|
|
15
|
+
'are', 'were', 'was', 'could', 'might', 'shall', 'need', 'want'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Common non-boolean patterns to ignore (user feedback)
|
|
19
|
+
this.ignoredPatterns = [
|
|
20
|
+
// Fallback/default patterns: var = value || fallback
|
|
21
|
+
/\w+\s*=\s*\w+\s*\|\|\s*[^|]/,
|
|
22
|
+
// Assignment patterns that are clearly not boolean
|
|
23
|
+
/\w+\s*=\s*['"`][^'"`]*['"`]/, // String assignments
|
|
24
|
+
/\w+\s*=\s*\d+/, // Number assignments
|
|
25
|
+
/\w+\s*=\s*\{/, // Object assignments
|
|
26
|
+
/\w+\s*=\s*\[/, // Array assignments
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Variables that commonly aren't boolean but might look like it
|
|
30
|
+
this.commonNonBooleans = [
|
|
31
|
+
'value', 'result', 'data', 'config', 'name', 'id', 'key',
|
|
32
|
+
'path', 'url', 'src', 'href', 'text', 'message', 'error',
|
|
33
|
+
'response', 'request', 'params', 'options', 'settings'
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async analyze(files, language, options = {}) {
|
|
38
|
+
const violations = [];
|
|
39
|
+
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
if (options.verbose) {
|
|
42
|
+
console.log(`🔍 Running C042 analysis on ${require('path').basename(filePath)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
47
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
48
|
+
violations.push(...fileViolations);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return violations;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
analyzeFile(content, filePath) {
|
|
58
|
+
const violations = [];
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < lines.length; i++) {
|
|
62
|
+
const line = lines[i].trim();
|
|
63
|
+
|
|
64
|
+
// Skip empty lines, comments, imports, and type declarations
|
|
65
|
+
if (!line || line.startsWith('//') || line.startsWith('/*') ||
|
|
66
|
+
line.startsWith('import') || line.startsWith('export') ||
|
|
67
|
+
line.startsWith('declare') || line.startsWith('*')) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lineViolations = this.analyzeDeclaration(line, i + 1);
|
|
72
|
+
lineViolations.forEach(violation => {
|
|
73
|
+
violations.push({
|
|
74
|
+
...violation,
|
|
75
|
+
file: filePath
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return violations;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
analyzeDeclaration(line, lineNumber) {
|
|
84
|
+
const violations = [];
|
|
85
|
+
|
|
86
|
+
// Match variable declarations with boolean values
|
|
87
|
+
const booleanAssignments = this.findBooleanAssignments(line);
|
|
88
|
+
|
|
89
|
+
for (const assignment of booleanAssignments) {
|
|
90
|
+
const { varName, isBooleanValue, hasPrefix, actualValue } = assignment;
|
|
91
|
+
|
|
92
|
+
// Case 1: Variable is assigned a boolean value but doesn't have proper prefix
|
|
93
|
+
if (isBooleanValue && !hasPrefix) {
|
|
94
|
+
// Skip if this matches user feedback patterns (false positives)
|
|
95
|
+
if (this.shouldSkipBooleanVariableCheck(varName, line, actualValue)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
violations.push({
|
|
100
|
+
line: lineNumber,
|
|
101
|
+
column: line.indexOf(varName) + 1,
|
|
102
|
+
message: `Boolean variable '${varName}' should start with a descriptive prefix like 'is', 'has', 'should', 'can', or 'check'. Consider: ${this.generateSuggestions(varName).join(', ')}.`,
|
|
103
|
+
severity: 'warning',
|
|
104
|
+
ruleId: this.ruleId
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Case 2: Variable has boolean prefix but is assigned a non-boolean value
|
|
109
|
+
else if (hasPrefix && !isBooleanValue && actualValue && this.isDefinitelyNotBoolean(actualValue)) {
|
|
110
|
+
// Only skip very basic cases for prefix misuse
|
|
111
|
+
if (varName.length <= 2) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const prefix = this.extractPrefix(varName);
|
|
116
|
+
violations.push({
|
|
117
|
+
line: lineNumber,
|
|
118
|
+
column: line.indexOf(varName) + 1,
|
|
119
|
+
message: `Variable '${varName}' uses boolean prefix '${prefix}' but is assigned a non-boolean value. Consider renaming or changing the value.`,
|
|
120
|
+
severity: 'warning',
|
|
121
|
+
ruleId: this.ruleId
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return violations;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
findBooleanAssignments(line) {
|
|
130
|
+
const assignments = [];
|
|
131
|
+
|
|
132
|
+
// Match declaration patterns only (avoid duplicates)
|
|
133
|
+
const patterns = [
|
|
134
|
+
// let/const/var varName = value
|
|
135
|
+
/(?:let|const|var)\s+(\w+)\s*(?::\s*\w+\s*)?=\s*(.+?)(?:;|$)/g,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
for (const pattern of patterns) {
|
|
139
|
+
let match;
|
|
140
|
+
const seenVariables = new Set(); // Avoid duplicates
|
|
141
|
+
|
|
142
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
143
|
+
const varName = match[1];
|
|
144
|
+
const value = match[2].trim();
|
|
145
|
+
|
|
146
|
+
// Skip if already processed
|
|
147
|
+
if (seenVariables.has(varName)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
seenVariables.add(varName);
|
|
151
|
+
|
|
152
|
+
// Skip destructuring and complex patterns
|
|
153
|
+
if (varName.includes('[') || varName.includes('{') || value.includes('{') || value.includes('[')) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isBooleanValue = this.isBooleanValue(value);
|
|
158
|
+
const hasPrefix = this.hasBooleanPrefix(varName);
|
|
159
|
+
|
|
160
|
+
assignments.push({
|
|
161
|
+
varName,
|
|
162
|
+
isBooleanValue,
|
|
163
|
+
hasPrefix,
|
|
164
|
+
actualValue: value
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return assignments;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
isBooleanValue(value) {
|
|
173
|
+
const trimmedValue = value.trim();
|
|
174
|
+
|
|
175
|
+
// Direct boolean literals
|
|
176
|
+
if (trimmedValue === 'true' || trimmedValue === 'false') {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Boolean expressions that clearly result in boolean
|
|
181
|
+
const booleanExpressions = [
|
|
182
|
+
/\w+\s*[<>!=]=/, // Comparisons
|
|
183
|
+
/\w+\s*(&&|\|\|)/, // Logical operations (but not fallback patterns)
|
|
184
|
+
/^\!\w+/, // Negation
|
|
185
|
+
/instanceof\s+/, // instanceof
|
|
186
|
+
/\.test\(/, // regex.test()
|
|
187
|
+
/\.includes\(/, // array.includes()
|
|
188
|
+
/\.hasOwnProperty\(/, // hasOwnProperty
|
|
189
|
+
/\.some\(/, // array.some()
|
|
190
|
+
/\.every\(/, // array.every()
|
|
191
|
+
/typeof\s+.*\s*===/, // typeof checks
|
|
192
|
+
/Math\.random\(\)\s*[<>]/, // Math.random() comparisons
|
|
193
|
+
/\.length\s*[<>!=]=/, // Length comparisons
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
// Exclude fallback patterns (user feedback - these are NOT boolean)
|
|
197
|
+
if (trimmedValue.includes('||')) {
|
|
198
|
+
// Check if it's a boolean expression or just a fallback
|
|
199
|
+
// If the || is followed by a non-boolean value, it's likely a fallback
|
|
200
|
+
const parts = trimmedValue.split('||');
|
|
201
|
+
if (parts.length === 2) {
|
|
202
|
+
const fallback = parts[1].trim();
|
|
203
|
+
// If fallback is clearly not boolean, this is not a boolean assignment
|
|
204
|
+
if (this.isDefinitelyNotBoolean(fallback) || /^\d+$/.test(fallback) || /^['"`]/.test(fallback)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return booleanExpressions.some(pattern => pattern.test(trimmedValue));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
hasBooleanPrefix(varName) {
|
|
214
|
+
const lowerName = varName.toLowerCase();
|
|
215
|
+
return this.booleanPrefixes.some(prefix =>
|
|
216
|
+
lowerName.startsWith(prefix.toLowerCase()) &&
|
|
217
|
+
lowerName.length > prefix.length
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
extractPrefix(varName) {
|
|
222
|
+
const lowerName = varName.toLowerCase();
|
|
223
|
+
for (const prefix of this.booleanPrefixes) {
|
|
224
|
+
if (lowerName.startsWith(prefix.toLowerCase())) {
|
|
225
|
+
return prefix;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
shouldSkipBooleanVariableCheck(varName, line, value) {
|
|
232
|
+
// Skip very short names
|
|
233
|
+
if (varName.length <= 2) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Skip common non-boolean variable names
|
|
238
|
+
if (this.commonNonBooleans.includes(varName.toLowerCase())) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Skip user feedback patterns (fallback/default patterns)
|
|
243
|
+
if (this.ignoredPatterns.some(pattern => pattern.test(line))) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Skip function parameters and loop variables
|
|
248
|
+
if (line.includes('function') || line.includes('for') || line.includes('=>')) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
isDefinitelyNotBoolean(value) {
|
|
256
|
+
const trimmedValue = value.trim();
|
|
257
|
+
|
|
258
|
+
// String literals (including single quotes)
|
|
259
|
+
if (trimmedValue.match(/^['"`][^'"`]*['"`]$/)) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Number literals
|
|
264
|
+
if (trimmedValue.match(/^\d+(\.\d+)?$/)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Object/array literals
|
|
269
|
+
if (trimmedValue.startsWith('{') || trimmedValue.startsWith('[')) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// null, undefined
|
|
274
|
+
if (trimmedValue === 'null' || trimmedValue === 'undefined') {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Common non-boolean patterns
|
|
279
|
+
if (trimmedValue.includes('new ') || trimmedValue.includes('function')) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
generateSuggestions(varName) {
|
|
287
|
+
const suggestions = [];
|
|
288
|
+
const baseName = varName.replace(/^(is|has|should|can|will|must|may|check)/i, '');
|
|
289
|
+
const capitalizedBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
290
|
+
|
|
291
|
+
// Generate a few reasonable suggestions
|
|
292
|
+
suggestions.push(`is${capitalizedBase}`);
|
|
293
|
+
suggestions.push(`has${capitalizedBase}`);
|
|
294
|
+
suggestions.push(`should${capitalizedBase}`);
|
|
295
|
+
|
|
296
|
+
return suggestions.slice(0, 3); // Limit to 3 suggestions
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = C042Analyzer;
|