@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,330 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class S029Analyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'S029';
|
|
7
|
+
this.ruleName = 'CSRF Protection Required';
|
|
8
|
+
this.description = 'Cần áp dụng cơ chế chống CSRF cho các chức năng xác thực';
|
|
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 S029 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.analyzeTypeScript(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeTypeScript(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
|
|
45
|
+
// Find lines where global CSRF protection is applied
|
|
46
|
+
const globalCSRFLines = this.findGlobalCSRFLines(lines);
|
|
47
|
+
|
|
48
|
+
lines.forEach((line, index) => {
|
|
49
|
+
const lineNumber = index + 1;
|
|
50
|
+
const trimmedLine = line.trim();
|
|
51
|
+
|
|
52
|
+
// Skip comments and imports
|
|
53
|
+
if (this.isCommentOrImport(trimmedLine)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Look for Express route handlers that need CSRF protection
|
|
58
|
+
const routeHandlers = this.findRouteHandlers(trimmedLine, line);
|
|
59
|
+
|
|
60
|
+
routeHandlers.forEach(handler => {
|
|
61
|
+
// Skip if this is a mock or test context
|
|
62
|
+
if (this.isMockOrTestContext(content, handler.instance)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if global CSRF protection was applied before this route
|
|
67
|
+
const hasGlobalCSRFProtection = this.hasGlobalCSRFProtectionBeforeLine(globalCSRFLines, index, handler.instance);
|
|
68
|
+
|
|
69
|
+
// Check if this specific route has CSRF protection
|
|
70
|
+
const hasRouteCSRFProtection = this.hasRouteSpecificCSRFProtection(lines, index, handler);
|
|
71
|
+
|
|
72
|
+
if (!hasGlobalCSRFProtection && !hasRouteCSRFProtection) {
|
|
73
|
+
violations.push({
|
|
74
|
+
ruleId: this.ruleId,
|
|
75
|
+
file: filePath,
|
|
76
|
+
line: lineNumber,
|
|
77
|
+
column: handler.column,
|
|
78
|
+
message: `CSRF protection is missing for route handler '${handler.route}'. Apply csurf() or equivalent middleware`,
|
|
79
|
+
severity: 'error',
|
|
80
|
+
code: trimmedLine,
|
|
81
|
+
type: 'missing_csrf_protection',
|
|
82
|
+
confidence: handler.confidence,
|
|
83
|
+
suggestion: 'Add CSRF middleware: app.use(csurf()) or use CSRF token validation'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return violations;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isCommentOrImport(line) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
return trimmed.startsWith('//') ||
|
|
95
|
+
trimmed.startsWith('/*') ||
|
|
96
|
+
trimmed.startsWith('*') ||
|
|
97
|
+
trimmed.startsWith('import ') ||
|
|
98
|
+
trimmed.startsWith('export ');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
findRouteHandlers(line, originalLine) {
|
|
102
|
+
const handlers = [];
|
|
103
|
+
const foundMatches = new Set(); // Prevent duplicates
|
|
104
|
+
|
|
105
|
+
// Only detect Express.js route patterns, not HTTP client methods
|
|
106
|
+
const routePatterns = [
|
|
107
|
+
// Express method with middleware: app.post('/path', middleware, handler)
|
|
108
|
+
{
|
|
109
|
+
regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])\s*,/gi,
|
|
110
|
+
type: 'express_route_with_middleware',
|
|
111
|
+
priority: 1 // Higher priority to check first
|
|
112
|
+
},
|
|
113
|
+
// app.post(), router.put(), etc.
|
|
114
|
+
{
|
|
115
|
+
regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])/gi,
|
|
116
|
+
type: 'express_route',
|
|
117
|
+
priority: 2
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// Sort by priority to avoid duplicates
|
|
122
|
+
routePatterns.sort((a, b) => a.priority - b.priority);
|
|
123
|
+
|
|
124
|
+
routePatterns.forEach(pattern => {
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
127
|
+
const instance = match[1]; // app, router, server
|
|
128
|
+
const method = match[2]; // post, put, delete, patch
|
|
129
|
+
const route = match[3]; // '/path'
|
|
130
|
+
const matchKey = `${instance}.${method}(${route})`; // Unique key
|
|
131
|
+
|
|
132
|
+
// Skip duplicates
|
|
133
|
+
if (foundMatches.has(matchKey)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip if it's clearly not Express.js context
|
|
138
|
+
if (this.isNotExpressContext(line, instance)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
foundMatches.add(matchKey);
|
|
143
|
+
handlers.push({
|
|
144
|
+
type: pattern.type,
|
|
145
|
+
instance: instance,
|
|
146
|
+
method: method,
|
|
147
|
+
route: route.replace(/['"]/g, ''),
|
|
148
|
+
column: match.index + 1,
|
|
149
|
+
confidence: this.calculateConfidence(line, pattern.type)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return handlers;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
isNotExpressContext(line, instance) {
|
|
158
|
+
// Skip HTTP client methods (like axios, fetch wrappers)
|
|
159
|
+
const clientPatterns = [
|
|
160
|
+
'public ', 'private ', 'protected ', // Class methods
|
|
161
|
+
'async ', 'function ', // Function definitions
|
|
162
|
+
'const ', 'let ', 'var ', // Variable assignments
|
|
163
|
+
': Promise<', ': BaseResponse<', // TypeScript return types
|
|
164
|
+
'this.http', 'httpClient', // HTTP client instances
|
|
165
|
+
'axios.', 'fetch(', // HTTP client calls
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const lowerLine = line.toLowerCase();
|
|
169
|
+
|
|
170
|
+
// If line contains client patterns, likely not Express route
|
|
171
|
+
const hasClientPattern = clientPatterns.some(pattern =>
|
|
172
|
+
lowerLine.includes(pattern.toLowerCase())
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (hasClientPattern) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If instance name suggests HTTP client, skip
|
|
180
|
+
const clientInstanceNames = ['httpclient', 'client', 'api', 'service'];
|
|
181
|
+
if (clientInstanceNames.includes(instance.toLowerCase())) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if the file content suggests this is a mock/test rather than real Express app
|
|
189
|
+
isMockOrTestContext(content, instance) {
|
|
190
|
+
const lowerContent = content.toLowerCase();
|
|
191
|
+
|
|
192
|
+
// Look for mock object definitions
|
|
193
|
+
const mockPatterns = [
|
|
194
|
+
`const ${instance.toLowerCase()} = {`,
|
|
195
|
+
`let ${instance.toLowerCase()} = {`,
|
|
196
|
+
`var ${instance.toLowerCase()} = {`,
|
|
197
|
+
`${instance.toLowerCase()}: {`,
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const hasMockDefinition = mockPatterns.some(pattern =>
|
|
201
|
+
lowerContent.includes(pattern)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (hasMockDefinition) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check for test file patterns
|
|
209
|
+
const testIndicators = ['.test.', '.spec.', '__tests__', 'test case', 'mock'];
|
|
210
|
+
const isTestContext = testIndicators.some(indicator =>
|
|
211
|
+
lowerContent.includes(indicator)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return isTestContext;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
hasCSRFProtection(content) {
|
|
218
|
+
const csrfPatterns = [
|
|
219
|
+
// Middleware usage
|
|
220
|
+
'csurf()',
|
|
221
|
+
'csrfProtection',
|
|
222
|
+
'verifyCsrfToken',
|
|
223
|
+
'checkCsrf',
|
|
224
|
+
'csrf-token',
|
|
225
|
+
'_csrf',
|
|
226
|
+
|
|
227
|
+
// Manual CSRF checks
|
|
228
|
+
'req.csrfToken',
|
|
229
|
+
'csrf.verify',
|
|
230
|
+
'validateCSRF',
|
|
231
|
+
|
|
232
|
+
// Security headers
|
|
233
|
+
'x-csrf-token',
|
|
234
|
+
'x-xsrf-token',
|
|
235
|
+
|
|
236
|
+
// Framework-specific
|
|
237
|
+
'protect_from_forgery', // Rails
|
|
238
|
+
'@csrf', // Laravel
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const lowerContent = content.toLowerCase();
|
|
242
|
+
|
|
243
|
+
return csrfPatterns.some(pattern =>
|
|
244
|
+
lowerContent.includes(pattern.toLowerCase())
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
hasGlobalCSRFProtection(content) {
|
|
249
|
+
// Check for global CSRF middleware: app.use(csurf())
|
|
250
|
+
const globalPatterns = [
|
|
251
|
+
/\b(app|router|server)\s*\.\s*use\s*\(\s*csurf\(\)/gi,
|
|
252
|
+
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrfProtection/gi,
|
|
253
|
+
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrf\(\)/gi,
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
return globalPatterns.some(pattern => pattern.test(content));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
findGlobalCSRFLines(lines) {
|
|
260
|
+
const csrfLines = [];
|
|
261
|
+
|
|
262
|
+
lines.forEach((line, index) => {
|
|
263
|
+
const globalPatterns = [
|
|
264
|
+
/\b(app|router|server)\s*\.\s*use\s*\(\s*csurf\(\)/gi,
|
|
265
|
+
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrfProtection/gi,
|
|
266
|
+
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrf\(\)/gi,
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
globalPatterns.forEach(pattern => {
|
|
270
|
+
let match;
|
|
271
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
272
|
+
csrfLines.push({
|
|
273
|
+
lineIndex: index,
|
|
274
|
+
instance: match[1], // app, router, server
|
|
275
|
+
line: line.trim()
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return csrfLines;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
hasGlobalCSRFProtectionBeforeLine(globalCSRFLines, routeLineIndex, routeInstance) {
|
|
285
|
+
// Check if any global CSRF protection was applied for this instance before this route
|
|
286
|
+
return globalCSRFLines.some(csrf =>
|
|
287
|
+
csrf.instance === routeInstance && csrf.lineIndex < routeLineIndex
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
hasRouteSpecificCSRFProtection(lines, currentIndex, handler) {
|
|
292
|
+
// Check if the route has CSRF middleware as parameter
|
|
293
|
+
// e.g. app.post('/path', csrfProtection, handler)
|
|
294
|
+
const currentLine = lines[currentIndex];
|
|
295
|
+
|
|
296
|
+
const csrfMiddlewarePatterns = [
|
|
297
|
+
'csrfProtection',
|
|
298
|
+
'csurf()',
|
|
299
|
+
'verifyCsrfToken',
|
|
300
|
+
'checkCsrf',
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
return csrfMiddlewarePatterns.some(pattern =>
|
|
304
|
+
currentLine.includes(pattern)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
calculateConfidence(line, patternType) {
|
|
309
|
+
let confidence = 0.8;
|
|
310
|
+
|
|
311
|
+
// Higher confidence for clear Express patterns
|
|
312
|
+
if (patternType === 'express_route_with_middleware') {
|
|
313
|
+
confidence += 0.1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Lower confidence if mixed with client-like patterns
|
|
317
|
+
const clientIndicators = ['public', 'class', 'Promise<', 'async'];
|
|
318
|
+
const hasClientIndicators = clientIndicators.some(indicator =>
|
|
319
|
+
line.includes(indicator)
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (hasClientIndicators) {
|
|
323
|
+
confidence -= 0.3;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return Math.max(0.3, Math.min(1.0, confidence));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = new S029Analyzer();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C002_no_duplicate_code - Rule Tests
|
|
3
|
+
* Tests for heuristic rule analyzer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const C002_no_duplicate_codeAnalyzer = require('./analyzer');
|
|
7
|
+
|
|
8
|
+
describe('C002_no_duplicate_code Heuristic Rule', () => {
|
|
9
|
+
let analyzer;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
analyzer = new C002_no_duplicate_codeAnalyzer();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Valid Code', () => {
|
|
16
|
+
test('should not report violations for valid code', () => {
|
|
17
|
+
const code = `
|
|
18
|
+
// TODO: Add valid code examples
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const violations = analyzer.analyze(code, 'test.js');
|
|
22
|
+
expect(violations).toHaveLength(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Invalid Code', () => {
|
|
27
|
+
test('should report violations for invalid code', () => {
|
|
28
|
+
const code = `
|
|
29
|
+
// TODO: Add invalid code examples
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const violations = analyzer.analyze(code, 'test.js');
|
|
33
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
34
|
+
expect(violations[0].ruleId).toBe('C002_no_duplicate_code');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Edge Cases', () => {
|
|
39
|
+
test('should handle empty code', () => {
|
|
40
|
+
const violations = analyzer.analyze('', 'test.js');
|
|
41
|
+
expect(violations).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should handle syntax errors gracefully', () => {
|
|
45
|
+
const code = 'invalid javascript syntax {{{';
|
|
46
|
+
const violations = analyzer.analyze(code, 'test.js');
|
|
47
|
+
expect(Array.isArray(violations)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Utilities for Heuristic Rules
|
|
3
|
+
* Provides AST parsing and traversal utilities for rule analyzers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ASTUtils {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.supportedLanguages = ['javascript', 'typescript'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse code content into AST
|
|
13
|
+
* @param {string} content - Code content
|
|
14
|
+
* @param {string} language - Language (javascript, typescript)
|
|
15
|
+
* @returns {Object|null} Parsed AST or null if parsing fails
|
|
16
|
+
*/
|
|
17
|
+
parse(content, language = 'javascript') {
|
|
18
|
+
try {
|
|
19
|
+
// TODO: Implement proper AST parsing
|
|
20
|
+
// For now, return a simple representation
|
|
21
|
+
return {
|
|
22
|
+
type: 'Program',
|
|
23
|
+
body: [],
|
|
24
|
+
language,
|
|
25
|
+
sourceCode: content
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn('AST parsing failed:', error.message);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find function declarations in code
|
|
35
|
+
* @param {string} content - Code content
|
|
36
|
+
* @returns {Array} Array of function matches
|
|
37
|
+
*/
|
|
38
|
+
findFunctions(content) {
|
|
39
|
+
const functions = [];
|
|
40
|
+
const functionRegex = /(?:function\s+(\w+)|(\w+)\s*=\s*function|(\w+)\s*=\s*\([^)]*\)\s*=>)/g;
|
|
41
|
+
let match;
|
|
42
|
+
|
|
43
|
+
while ((match = functionRegex.exec(content)) !== null) {
|
|
44
|
+
const line = this.getLineNumber(content, match.index);
|
|
45
|
+
const functionName = match[1] || match[2] || match[3];
|
|
46
|
+
|
|
47
|
+
functions.push({
|
|
48
|
+
name: functionName,
|
|
49
|
+
line: line,
|
|
50
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
51
|
+
match: match[0]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return functions;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find variable declarations
|
|
60
|
+
* @param {string} content - Code content
|
|
61
|
+
* @returns {Array} Array of variable matches
|
|
62
|
+
*/
|
|
63
|
+
findVariables(content) {
|
|
64
|
+
const variables = [];
|
|
65
|
+
const varRegex = /(?:var|let|const)\s+(\w+)/g;
|
|
66
|
+
let match;
|
|
67
|
+
|
|
68
|
+
while ((match = varRegex.exec(content)) !== null) {
|
|
69
|
+
const line = this.getLineNumber(content, match.index);
|
|
70
|
+
|
|
71
|
+
variables.push({
|
|
72
|
+
name: match[1],
|
|
73
|
+
line: line,
|
|
74
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
75
|
+
type: match[0].split(' ')[0] // var, let, const
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return variables;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find import/require statements
|
|
84
|
+
* @param {string} content - Code content
|
|
85
|
+
* @returns {Array} Array of import matches
|
|
86
|
+
*/
|
|
87
|
+
findImports(content) {
|
|
88
|
+
const imports = [];
|
|
89
|
+
|
|
90
|
+
// ES6 imports
|
|
91
|
+
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
92
|
+
let match;
|
|
93
|
+
|
|
94
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
95
|
+
const line = this.getLineNumber(content, match.index);
|
|
96
|
+
imports.push({
|
|
97
|
+
type: 'import',
|
|
98
|
+
module: match[1],
|
|
99
|
+
line: line,
|
|
100
|
+
match: match[0]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// CommonJS requires
|
|
105
|
+
const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
|
|
106
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
107
|
+
const line = this.getLineNumber(content, match.index);
|
|
108
|
+
imports.push({
|
|
109
|
+
type: 'require',
|
|
110
|
+
module: match[1],
|
|
111
|
+
line: line,
|
|
112
|
+
match: match[0]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return imports;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get line number for a character index
|
|
121
|
+
* @param {string} content - Code content
|
|
122
|
+
* @param {number} index - Character index
|
|
123
|
+
* @returns {number} Line number (1-based)
|
|
124
|
+
*/
|
|
125
|
+
getLineNumber(content, index) {
|
|
126
|
+
return content.substring(0, index).split('\n').length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get line start position for a character index
|
|
131
|
+
* @param {string} content - Code content
|
|
132
|
+
* @param {number} index - Character index
|
|
133
|
+
* @returns {number} Line start index
|
|
134
|
+
*/
|
|
135
|
+
getLineStart(content, index) {
|
|
136
|
+
const beforeIndex = content.substring(0, index);
|
|
137
|
+
const lastNewline = beforeIndex.lastIndexOf('\n');
|
|
138
|
+
return lastNewline === -1 ? 0 : lastNewline + 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get line content for a line number
|
|
143
|
+
* @param {string} content - Code content
|
|
144
|
+
* @param {number} lineNumber - Line number (1-based)
|
|
145
|
+
* @returns {string} Line content
|
|
146
|
+
*/
|
|
147
|
+
getLineContent(content, lineNumber) {
|
|
148
|
+
const lines = content.split('\n');
|
|
149
|
+
return lines[lineNumber - 1] || '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if position is inside a comment
|
|
154
|
+
* @param {string} content - Code content
|
|
155
|
+
* @param {number} index - Character index
|
|
156
|
+
* @returns {boolean} True if inside comment
|
|
157
|
+
*/
|
|
158
|
+
isInComment(content, index) {
|
|
159
|
+
const beforeIndex = content.substring(0, index);
|
|
160
|
+
|
|
161
|
+
// Single line comment
|
|
162
|
+
const lastLineStart = beforeIndex.lastIndexOf('\n');
|
|
163
|
+
const lineContent = beforeIndex.substring(lastLineStart + 1);
|
|
164
|
+
if (lineContent.includes('//')) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Block comment
|
|
169
|
+
const lastBlockStart = beforeIndex.lastIndexOf('/*');
|
|
170
|
+
const lastBlockEnd = beforeIndex.lastIndexOf('*/');
|
|
171
|
+
|
|
172
|
+
return lastBlockStart > lastBlockEnd;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Extract function parameters
|
|
177
|
+
* @param {string} functionDeclaration - Function declaration string
|
|
178
|
+
* @returns {Array} Array of parameter names
|
|
179
|
+
*/
|
|
180
|
+
extractParameters(functionDeclaration) {
|
|
181
|
+
const paramMatch = functionDeclaration.match(/\(([^)]*)\)/);
|
|
182
|
+
if (!paramMatch || !paramMatch[1]) return [];
|
|
183
|
+
|
|
184
|
+
return paramMatch[1]
|
|
185
|
+
.split(',')
|
|
186
|
+
.map(param => param.trim().split('=')[0].trim()) // Handle default parameters
|
|
187
|
+
.filter(param => param.length > 0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { ASTUtils };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Analyzer Class for SunLint Rules
|
|
3
|
+
* Provides common functionality and consistent severity management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { getSeverity, isValidSeverity } = require('./severity-constants');
|
|
7
|
+
|
|
8
|
+
class BaseAnalyzer {
|
|
9
|
+
constructor(ruleId, ruleName, description, category = 'QUALITY') {
|
|
10
|
+
this.ruleId = ruleId;
|
|
11
|
+
this.ruleName = ruleName;
|
|
12
|
+
this.description = description;
|
|
13
|
+
this.category = category;
|
|
14
|
+
|
|
15
|
+
// Severity will be determined dynamically
|
|
16
|
+
this._severity = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get severity for this rule, considering config overrides
|
|
21
|
+
* @param {Object} config - Configuration object
|
|
22
|
+
* @returns {string} Severity level
|
|
23
|
+
*/
|
|
24
|
+
getSeverity(config = {}) {
|
|
25
|
+
// Check if already cached
|
|
26
|
+
if (this._severity) {
|
|
27
|
+
return this._severity;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get from config override
|
|
31
|
+
const configOverride = config?.rules?.[this.ruleId]?.severity ||
|
|
32
|
+
config?.rules?.[this.ruleId];
|
|
33
|
+
|
|
34
|
+
this._severity = getSeverity(this.ruleId, this.category, configOverride);
|
|
35
|
+
return this._severity;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set severity (for backward compatibility or testing)
|
|
40
|
+
* @param {string} severity - Severity level
|
|
41
|
+
*/
|
|
42
|
+
setSeverity(severity) {
|
|
43
|
+
if (!isValidSeverity(severity)) {
|
|
44
|
+
console.warn(`Invalid severity '${severity}' for rule ${this.ruleId}. Using default.`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this._severity = severity;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a violation object with consistent structure
|
|
52
|
+
* @param {Object} params - Violation parameters
|
|
53
|
+
* @returns {Object} Formatted violation
|
|
54
|
+
*/
|
|
55
|
+
createViolation(params) {
|
|
56
|
+
const {
|
|
57
|
+
filePath,
|
|
58
|
+
line,
|
|
59
|
+
column,
|
|
60
|
+
message,
|
|
61
|
+
source,
|
|
62
|
+
suggestion,
|
|
63
|
+
additionalData = {}
|
|
64
|
+
} = params;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
ruleId: this.ruleId,
|
|
68
|
+
severity: this._severity || this.getSeverity(),
|
|
69
|
+
message: message || this.description,
|
|
70
|
+
filePath,
|
|
71
|
+
line,
|
|
72
|
+
column,
|
|
73
|
+
source,
|
|
74
|
+
suggestion,
|
|
75
|
+
category: this.category,
|
|
76
|
+
...additionalData
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if rule is enabled based on severity
|
|
82
|
+
* @param {Object} config - Configuration
|
|
83
|
+
* @returns {boolean} True if rule should run
|
|
84
|
+
*/
|
|
85
|
+
isEnabled(config = {}) {
|
|
86
|
+
const severity = this.getSeverity(config);
|
|
87
|
+
return severity !== 'off';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Abstract analyze method - must be implemented by subclasses
|
|
92
|
+
*/
|
|
93
|
+
async analyze(files, language, config) {
|
|
94
|
+
throw new Error(`analyze() method must be implemented by ${this.constructor.name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = BaseAnalyzer;
|