@sun-asterisk/sunlint 1.1.8 → 1.2.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/.sunlint.json +1 -1
- package/CHANGELOG.md +50 -1
- package/README.md +66 -4
- package/config/presets/all.json +125 -0
- package/config/presets/beginner.json +16 -8
- package/config/presets/ci.json +12 -4
- package/config/presets/maintainability.json +38 -0
- package/config/presets/performance.json +32 -0
- package/config/presets/quality.json +103 -0
- package/config/presets/recommended.json +36 -12
- package/config/presets/security.json +88 -0
- package/config/presets/strict.json +15 -5
- package/config/rules/rules-registry-generated.json +6312 -0
- package/config/rules-summary.json +1941 -0
- package/core/adapters/sunlint-rule-adapter.js +452 -0
- package/core/analysis-orchestrator.js +4 -4
- package/core/config-manager.js +28 -5
- package/core/rule-selection-service.js +52 -55
- package/docs/CONFIGURATION.md +111 -3
- package/docs/LANGUAGE-SPECIFIC-RULES.md +308 -0
- package/docs/README.md +3 -0
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +156 -0
- package/engines/heuristic-engine.js +8 -31
- package/origin-rules/common-en.md +1320 -0
- package/origin-rules/dart-en.md +289 -0
- package/origin-rules/java-en.md +60 -0
- package/origin-rules/kotlin-mobile-en.md +453 -0
- package/origin-rules/reactjs-en.md +102 -0
- package/origin-rules/security-en.md +1055 -0
- package/origin-rules/swift-en.md +449 -0
- package/origin-rules/typescript-en.md +136 -0
- package/package.json +6 -5
- package/scripts/copy-rules.js +86 -0
- package/rules/README.md +0 -252
- package/rules/common/C002_no_duplicate_code/analyzer.js +0 -65
- package/rules/common/C002_no_duplicate_code/config.json +0 -23
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +0 -418
- package/rules/common/C003_no_vague_abbreviations/config.json +0 -35
- package/rules/common/C006_function_naming/analyzer.js +0 -349
- package/rules/common/C006_function_naming/config.json +0 -86
- package/rules/common/C010_limit_block_nesting/analyzer.js +0 -389
- package/rules/common/C013_no_dead_code/analyzer.js +0 -206
- package/rules/common/C014_dependency_injection/analyzer.js +0 -338
- package/rules/common/C017_constructor_logic/analyzer.js +0 -314
- package/rules/common/C019_log_level_usage/analyzer.js +0 -362
- package/rules/common/C019_log_level_usage/config.json +0 -121
- package/rules/common/C029_catch_block_logging/analyzer.js +0 -373
- package/rules/common/C029_catch_block_logging/config.json +0 -59
- package/rules/common/C031_validation_separation/analyzer.js +0 -186
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +0 -292
- package/rules/common/C042_boolean_name_prefix/analyzer.js +0 -300
- package/rules/common/C043_no_console_or_print/analyzer.js +0 -304
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +0 -351
- package/rules/common/C075_explicit_return_types/analyzer.js +0 -103
- package/rules/common/C076_single_test_behavior/analyzer.js +0 -121
- package/rules/docs/C002_no_duplicate_code.md +0 -57
- package/rules/docs/C031_validation_separation.md +0 -72
- package/rules/index.js +0 -149
- package/rules/migration/converter.js +0 -385
- package/rules/migration/mapping.json +0 -164
- package/rules/security/S026_json_schema_validation/analyzer.js +0 -251
- package/rules/security/S026_json_schema_validation/config.json +0 -27
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +0 -263
- package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
- package/rules/security/S029_csrf_protection/analyzer.js +0 -264
- package/rules/tests/C002_no_duplicate_code.test.js +0 -50
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +0 -191
- package/rules/utils/base-analyzer.js +0 -98
- package/rules/utils/pattern-matchers.js +0 -239
- package/rules/utils/rule-helpers.js +0 -264
- package/rules/utils/severity-constants.js +0 -93
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* C043 Rule: No console.log or print in production code
|
|
3
|
-
* Prevents usage of console logging and print statements in production
|
|
4
|
-
* Severity: warning
|
|
5
|
-
* Category: Quality
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
|
|
11
|
-
class C043NoConsoleOrPrintAnalyzer {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.ruleId = 'C043';
|
|
14
|
-
this.ruleName = 'No Console Or Print';
|
|
15
|
-
this.description = 'Do not use console.log or print in production code';
|
|
16
|
-
this.severity = 'warning';
|
|
17
|
-
|
|
18
|
-
// Enhanced patterns to catch various console and print usages
|
|
19
|
-
// Negative lookahead to avoid matches within strings or comments
|
|
20
|
-
this.consolePatterns = [
|
|
21
|
-
/\bconsole\.(?:log|debug|info|table|time|timeEnd|count|group|groupCollapsed|trace|dir|dirxml|profile|profileEnd)\s*\(/g,
|
|
22
|
-
/\bprint\s*\(/g,
|
|
23
|
-
/\balert\s*\(/g,
|
|
24
|
-
/\bconfirm\s*\(/g,
|
|
25
|
-
/\bprompt\s*\(/g
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
// Also check if the line is within a comment or string
|
|
29
|
-
function isInStringOrComment(line, matchIndex) {
|
|
30
|
-
// Check for single/double quotes before the match
|
|
31
|
-
const beforeMatch = line.substring(0, matchIndex);
|
|
32
|
-
const afterMatch = line.substring(matchIndex);
|
|
33
|
-
|
|
34
|
-
// Count quotes before match
|
|
35
|
-
const singleQuotes = (beforeMatch.match(/'/g) || []).length;
|
|
36
|
-
const doubleQuotes = (beforeMatch.match(/"/g) || []).length;
|
|
37
|
-
const backticks = (beforeMatch.match(/`/g) || []).length;
|
|
38
|
-
|
|
39
|
-
// If odd number of quotes, we're inside a string
|
|
40
|
-
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check for comments
|
|
45
|
-
if (beforeMatch.includes('//') || beforeMatch.includes('/*')) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Patterns for print statements
|
|
53
|
-
this.printPatterns = [
|
|
54
|
-
/\bprint\s*\(/g,
|
|
55
|
-
/\balert\s*\(/g,
|
|
56
|
-
/\bconfirm\s*\(/g,
|
|
57
|
-
/\bprompt\s*\(/g
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
// Allowed console methods (errors/warnings might be OK)
|
|
61
|
-
this.allowedConsoleMethods = ['error', 'warn'];
|
|
62
|
-
|
|
63
|
-
// Development context patterns that might allow console
|
|
64
|
-
this.devContextPatterns = [
|
|
65
|
-
/if\s*\(\s*(__DEV__|DEBUG|process\.env\.NODE_ENV)/,
|
|
66
|
-
/if\s*\(\s*process\.env\.NODE_ENV\s*===\s*['"`]development['"`]/,
|
|
67
|
-
/if\s*\(\s*ENABLE_LOGGING|FEATURES\.debug/
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async analyze(files, language, config) {
|
|
72
|
-
const violations = [];
|
|
73
|
-
|
|
74
|
-
for (const filePath of files) {
|
|
75
|
-
try {
|
|
76
|
-
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
77
|
-
const fileViolations = await this.analyzeFile(filePath, fileContent, language, config);
|
|
78
|
-
violations.push(...fileViolations);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.warn(`C043 analysis error for ${filePath}:`, error.message);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return violations;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async analyzeFile(filePath, fileContent, language, config) {
|
|
88
|
-
const violations = [];
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
// Skip test files (console.log often used in tests)
|
|
92
|
-
if (this.isTestFile(filePath)) {
|
|
93
|
-
return violations;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Skip development/debug files
|
|
97
|
-
if (this.isDevelopmentFile(filePath)) {
|
|
98
|
-
return violations;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const lines = fileContent.split('\n');
|
|
102
|
-
let inTemplateString = false;
|
|
103
|
-
let templateStartLine = -1;
|
|
104
|
-
|
|
105
|
-
for (let i = 0; i < lines.length; i++) {
|
|
106
|
-
const line = lines[i];
|
|
107
|
-
const lineNumber = i + 1;
|
|
108
|
-
|
|
109
|
-
// Track template literal state
|
|
110
|
-
const backtickCount = (line.match(/`/g) || []).length;
|
|
111
|
-
if (backtickCount % 2 === 1) {
|
|
112
|
-
if (!inTemplateString) {
|
|
113
|
-
inTemplateString = true;
|
|
114
|
-
templateStartLine = i;
|
|
115
|
-
} else {
|
|
116
|
-
inTemplateString = false;
|
|
117
|
-
templateStartLine = -1;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Skip if inside template literal
|
|
122
|
-
if (inTemplateString && templateStartLine !== i) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Skip comments and strings (basic check)
|
|
127
|
-
if (this.isInCommentOrString(line)) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Skip if in development context
|
|
132
|
-
if (this.isInDevelopmentContext(lines, i)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Check for console.* calls
|
|
137
|
-
for (const pattern of this.consolePatterns) {
|
|
138
|
-
let match;
|
|
139
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
140
|
-
// Skip if the match is within a string or comment
|
|
141
|
-
if (this.isInCommentOrString(line)) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const method = match[1] || 'log';
|
|
146
|
-
|
|
147
|
-
// Skip allowed methods
|
|
148
|
-
if (this.allowedConsoleMethods.includes(method)) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
violations.push({
|
|
153
|
-
ruleId: this.ruleId,
|
|
154
|
-
severity: this.severity,
|
|
155
|
-
message: `Do not use console.${method}() in production code. Use proper logging instead.`,
|
|
156
|
-
filePath: filePath,
|
|
157
|
-
line: lineNumber,
|
|
158
|
-
column: match.index + 1,
|
|
159
|
-
source: line.trim(),
|
|
160
|
-
suggestion: `Consider using a proper logging library (logger.${method}())`
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
pattern.lastIndex = 0; // Reset regex
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Check for print/alert calls
|
|
167
|
-
for (const pattern of this.printPatterns) {
|
|
168
|
-
let match;
|
|
169
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
170
|
-
// Skip if the match is within a string or comment
|
|
171
|
-
if (this.isInCommentOrString(line)) {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const method = match[0].split('(')[0].trim();
|
|
176
|
-
|
|
177
|
-
violations.push({
|
|
178
|
-
ruleId: this.ruleId,
|
|
179
|
-
severity: this.severity,
|
|
180
|
-
message: `Do not use ${method}() in production code. Use proper logging or UI notifications instead.`,
|
|
181
|
-
filePath: filePath,
|
|
182
|
-
line: lineNumber,
|
|
183
|
-
column: match.index + 1,
|
|
184
|
-
source: line.trim(),
|
|
185
|
-
suggestion: `Consider using a logging library or proper UI notification system`
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
pattern.lastIndex = 0; // Reset regex
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
} catch (error) {
|
|
193
|
-
console.warn(`C043 analysis error for ${filePath}:`, error.message);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return violations;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
isTestFile(filePath) {
|
|
200
|
-
const testPatterns = [
|
|
201
|
-
/\.test\.(js|ts|jsx|tsx)$/,
|
|
202
|
-
/\.spec\.(js|ts|jsx|tsx)$/,
|
|
203
|
-
/\/__tests__\//,
|
|
204
|
-
/\/tests?\//,
|
|
205
|
-
/\.e2e\./,
|
|
206
|
-
/\.integration\./,
|
|
207
|
-
/test\.config\./,
|
|
208
|
-
/jest\.config\./
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
return testPatterns.some(pattern => pattern.test(filePath));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
isDevelopmentFile(filePath) {
|
|
215
|
-
const devPatterns = [
|
|
216
|
-
/\.dev\.(js|ts|jsx|tsx)$/,
|
|
217
|
-
/\.development\./,
|
|
218
|
-
/\.debug\./,
|
|
219
|
-
/\/dev\//,
|
|
220
|
-
/\/debug\//,
|
|
221
|
-
/\/development\//
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
return devPatterns.some(pattern => pattern.test(filePath));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
isInCommentOrString(line) {
|
|
228
|
-
const trimmed = line.trim();
|
|
229
|
-
|
|
230
|
-
// Single line comment
|
|
231
|
-
if (trimmed.startsWith('//')) {
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Multi-line comment (basic check)
|
|
236
|
-
if (trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Check for console usage within quotes (simple approach)
|
|
241
|
-
// This is a heuristic - if there are more opening quotes than closing before console, likely inside string
|
|
242
|
-
const beforeConsole = line.split(/console\.|print\(|alert\(|confirm\(|prompt\(/)[0];
|
|
243
|
-
|
|
244
|
-
if (beforeConsole) {
|
|
245
|
-
const singleQuotes = (beforeConsole.match(/'/g) || []).length;
|
|
246
|
-
const doubleQuotes = (beforeConsole.match(/"/g) || []).length;
|
|
247
|
-
const backticks = (beforeConsole.match(/`/g) || []).length;
|
|
248
|
-
|
|
249
|
-
// If any odd count, we're probably inside a string
|
|
250
|
-
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 || backticks % 2 === 1) {
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
isInDevelopmentContext(lines, currentLineIndex) {
|
|
259
|
-
// Look backwards for development context (like if (__DEV__))
|
|
260
|
-
const lookBehindLines = 5;
|
|
261
|
-
const startIndex = Math.max(0, currentLineIndex - lookBehindLines);
|
|
262
|
-
|
|
263
|
-
for (let i = startIndex; i <= currentLineIndex; i++) {
|
|
264
|
-
const line = lines[i];
|
|
265
|
-
|
|
266
|
-
// Check for development context patterns
|
|
267
|
-
for (const pattern of this.devContextPatterns) {
|
|
268
|
-
if (pattern.test(line)) {
|
|
269
|
-
// Check if current line is within the same block
|
|
270
|
-
if (this.isWithinBlock(lines, i, currentLineIndex)) {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
isWithinBlock(lines, conditionLineIndex, currentLineIndex) {
|
|
281
|
-
// Simple block detection based on braces
|
|
282
|
-
let braceCount = 0;
|
|
283
|
-
|
|
284
|
-
for (let i = conditionLineIndex; i <= currentLineIndex; i++) {
|
|
285
|
-
const line = lines[i];
|
|
286
|
-
|
|
287
|
-
// Count opening/closing braces
|
|
288
|
-
const openBraces = (line.match(/\{/g) || []).length;
|
|
289
|
-
const closeBraces = (line.match(/\}/g) || []).length;
|
|
290
|
-
|
|
291
|
-
braceCount += openBraces - closeBraces;
|
|
292
|
-
|
|
293
|
-
// If we're at current line and still have positive brace count,
|
|
294
|
-
// we're likely inside the block
|
|
295
|
-
if (i === currentLineIndex && braceCount > 0) {
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
module.exports = C043NoConsoleOrPrintAnalyzer;
|
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Heuristic analyzer for: C047 – Logic retry không được viết lặp lại nhiều nơi
|
|
3
|
-
* Purpose: Detect duplicate retry logic patterns and suggest centralized retry utilities
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class C047Analyzer {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.ruleId = 'C047';
|
|
9
|
-
this.ruleName = 'No Duplicate Retry Logic';
|
|
10
|
-
this.description = 'Logic retry không được viết lặp lại nhiều nơi - use centralized retry utility instead';
|
|
11
|
-
|
|
12
|
-
// Patterns that indicate retry logic
|
|
13
|
-
this.retryIndicators = [
|
|
14
|
-
'maxretries', 'maxattempts', 'maxtries',
|
|
15
|
-
'attempt', 'retry', 'tries', 'retries',
|
|
16
|
-
'backoff', 'delay', 'timeout',
|
|
17
|
-
'exponential', 'linear'
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
// Allowed centralized retry utilities
|
|
21
|
-
this.allowedRetryUtils = [
|
|
22
|
-
'RetryUtil', 'retryWithBackoff', 'withRetry', 'retry',
|
|
23
|
-
'retryAsync', 'retryPromise', 'retryOperation',
|
|
24
|
-
'exponentialBackoff', 'linearBackoff'
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
// Detected retry patterns for duplicate checking
|
|
28
|
-
this.retryPatterns = [];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async analyze(files, language, options = {}) {
|
|
32
|
-
const violations = [];
|
|
33
|
-
|
|
34
|
-
for (const filePath of files) {
|
|
35
|
-
if (options.verbose) {
|
|
36
|
-
console.log(`🔍 Running C047 analysis on ${require('path').basename(filePath)}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
41
|
-
this.retryPatterns = []; // Reset for each file
|
|
42
|
-
const fileViolations = this.analyzeFile(content, filePath);
|
|
43
|
-
violations.push(...fileViolations);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return violations;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
analyzeFile(content, filePath) {
|
|
53
|
-
const violations = [];
|
|
54
|
-
const lines = content.split('\n');
|
|
55
|
-
|
|
56
|
-
// Find all retry patterns in the file
|
|
57
|
-
this.findRetryPatterns(lines, filePath);
|
|
58
|
-
|
|
59
|
-
// Check for duplicates
|
|
60
|
-
const duplicateGroups = this.findDuplicateRetryLogic();
|
|
61
|
-
|
|
62
|
-
// Report violations for duplicate patterns - only once per function
|
|
63
|
-
const reportedFunctions = new Set();
|
|
64
|
-
|
|
65
|
-
duplicateGroups.forEach(group => {
|
|
66
|
-
if (group.patterns.length > 1) {
|
|
67
|
-
// Find the first occurrence of each function to report
|
|
68
|
-
const functionViolations = new Map();
|
|
69
|
-
|
|
70
|
-
group.patterns.forEach(pattern => {
|
|
71
|
-
const functionName = this.getFunctionNameForLine(lines, pattern.line - 1);
|
|
72
|
-
const functionKey = `${functionName}_${Math.floor(pattern.line / 20)}`; // Group by function and rough location
|
|
73
|
-
|
|
74
|
-
if (!functionViolations.has(functionKey)) {
|
|
75
|
-
functionViolations.set(functionKey, pattern);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Report one violation per function
|
|
80
|
-
functionViolations.forEach(pattern => {
|
|
81
|
-
violations.push({
|
|
82
|
-
file: filePath,
|
|
83
|
-
line: pattern.line,
|
|
84
|
-
column: pattern.column,
|
|
85
|
-
message: `Duplicate retry logic detected (${group.patterns.length} similar patterns found). Consider using a centralized retry utility like RetryUtil.withRetry() or retryWithBackoff().`,
|
|
86
|
-
severity: 'warning',
|
|
87
|
-
ruleId: this.ruleId,
|
|
88
|
-
type: 'duplicate_retry_logic',
|
|
89
|
-
duplicateCount: group.patterns.length
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return violations;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
findRetryPatterns(lines, filePath) {
|
|
99
|
-
for (let i = 0; i < lines.length; i++) {
|
|
100
|
-
const line = lines[i];
|
|
101
|
-
const trimmedLine = line.trim().toLowerCase();
|
|
102
|
-
|
|
103
|
-
// Skip comments and empty lines
|
|
104
|
-
if (!trimmedLine || trimmedLine.startsWith('//') || trimmedLine.startsWith('/*')) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Skip if using allowed retry utilities
|
|
109
|
-
if (this.usesAllowedRetryUtil(line)) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Pattern 1: For/while loop with retry indicators
|
|
114
|
-
if (this.isRetryLoopPattern(line, lines, i)) {
|
|
115
|
-
const pattern = this.extractRetryPattern(lines, i, 'loop');
|
|
116
|
-
if (pattern) {
|
|
117
|
-
this.retryPatterns.push({
|
|
118
|
-
...pattern,
|
|
119
|
-
line: i + 1,
|
|
120
|
-
column: line.indexOf(line.trim()) + 1
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Pattern 2: Variable declarations with retry indicators
|
|
126
|
-
else if (this.isRetryVariableDeclaration(trimmedLine)) {
|
|
127
|
-
const pattern = this.extractRetryPattern(lines, i, 'variable');
|
|
128
|
-
if (pattern) {
|
|
129
|
-
this.retryPatterns.push({
|
|
130
|
-
...pattern,
|
|
131
|
-
line: i + 1,
|
|
132
|
-
column: line.indexOf(line.trim()) + 1
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Pattern 3: Recursive function with retry logic
|
|
138
|
-
else if (this.isRetryFunctionPattern(lines, i)) {
|
|
139
|
-
const pattern = this.extractRetryPattern(lines, i, 'recursive');
|
|
140
|
-
if (pattern) {
|
|
141
|
-
this.retryPatterns.push({
|
|
142
|
-
...pattern,
|
|
143
|
-
line: i + 1,
|
|
144
|
-
column: line.indexOf(line.trim()) + 1
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
isRetryLoopPattern(line, lines, index) {
|
|
152
|
-
const trimmedLine = line.trim().toLowerCase();
|
|
153
|
-
|
|
154
|
-
// Check for for/while loops with retry indicators
|
|
155
|
-
if ((trimmedLine.includes('for') || trimmedLine.includes('while')) &&
|
|
156
|
-
(trimmedLine.includes('(') && trimmedLine.includes(')'))) {
|
|
157
|
-
|
|
158
|
-
// Look in the loop condition and surrounding context for retry indicators
|
|
159
|
-
const contextLines = lines.slice(Math.max(0, index - 2), Math.min(lines.length, index + 10));
|
|
160
|
-
const contextText = contextLines.join('\n').toLowerCase();
|
|
161
|
-
|
|
162
|
-
return this.retryIndicators.some(indicator => contextText.includes(indicator)) &&
|
|
163
|
-
(contextText.includes('try') || contextText.includes('catch') || contextText.includes('error'));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
isRetryVariableDeclaration(line) {
|
|
170
|
-
// Check for variable declarations with retry-related names
|
|
171
|
-
const declarationPatterns = [
|
|
172
|
-
/(?:const|let|var)\s+.*(?:retry|attempt|tries|maxretries|maxattempts)/,
|
|
173
|
-
/(?:retry|attempt|tries|maxretries|maxattempts)\s*[:=]/
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
return declarationPatterns.some(pattern => pattern.test(line));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
isRetryFunctionPattern(lines, index) {
|
|
180
|
-
const line = lines[index].trim().toLowerCase();
|
|
181
|
-
|
|
182
|
-
// Check for function declarations with retry indicators
|
|
183
|
-
if ((line.includes('function') || line.includes('=>')) &&
|
|
184
|
-
this.retryIndicators.some(indicator => line.includes(indicator))) {
|
|
185
|
-
|
|
186
|
-
// Look for retry logic in function body
|
|
187
|
-
const functionBody = this.getFunctionBody(lines, index);
|
|
188
|
-
return functionBody &&
|
|
189
|
-
this.retryIndicators.some(indicator => functionBody.includes(indicator)) &&
|
|
190
|
-
(functionBody.includes('try') || functionBody.includes('catch') ||
|
|
191
|
-
functionBody.includes('throw') || functionBody.includes('error'));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
usesAllowedRetryUtil(line) {
|
|
198
|
-
return this.allowedRetryUtils.some(util =>
|
|
199
|
-
line.includes(util) && (line.includes('.') || line.includes('('))
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
extractRetryPattern(lines, startIndex, type) {
|
|
204
|
-
// Extract the pattern characteristics for similarity comparison
|
|
205
|
-
const contextLines = lines.slice(startIndex, Math.min(lines.length, startIndex + 20));
|
|
206
|
-
const contextText = contextLines.join('\n').toLowerCase();
|
|
207
|
-
|
|
208
|
-
// Extract key characteristics
|
|
209
|
-
const characteristics = {
|
|
210
|
-
type: type,
|
|
211
|
-
hasForLoop: contextText.includes('for'),
|
|
212
|
-
hasWhileLoop: contextText.includes('while'),
|
|
213
|
-
hasDoWhile: contextText.includes('do') && contextText.includes('while'),
|
|
214
|
-
hasTryCatch: contextText.includes('try') && contextText.includes('catch'),
|
|
215
|
-
hasMaxRetries: /max.*(?:retry|attempt|tries)/.test(contextText),
|
|
216
|
-
hasBackoff: contextText.includes('backoff') || contextText.includes('delay') || contextText.includes('timeout'),
|
|
217
|
-
hasExponential: contextText.includes('exponential') || /math\.pow|2\s*\*\*|\*\s*2/.test(contextText),
|
|
218
|
-
hasLinear: contextText.includes('linear') || /\*\s*(?:attempt|retry)/.test(contextText),
|
|
219
|
-
hasSetTimeout: contextText.includes('settimeout') || contextText.includes('promise') && contextText.includes('resolve'),
|
|
220
|
-
signature: this.generatePatternSignature(contextText)
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
return characteristics;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
generatePatternSignature(contextText) {
|
|
227
|
-
// Create a normalized signature for pattern matching
|
|
228
|
-
let signature = '';
|
|
229
|
-
|
|
230
|
-
if (contextText.includes('for')) signature += 'FOR_';
|
|
231
|
-
if (contextText.includes('while')) signature += 'WHILE_';
|
|
232
|
-
if (/max.*(?:retry|attempt)/.test(contextText)) signature += 'MAX_';
|
|
233
|
-
if (contextText.includes('try') && contextText.includes('catch')) signature += 'TRYCATCH_';
|
|
234
|
-
if (contextText.includes('settimeout') || contextText.includes('delay')) signature += 'DELAY_';
|
|
235
|
-
if (contextText.includes('exponential') || /math\.pow/.test(contextText)) signature += 'EXPONENTIAL_';
|
|
236
|
-
if (contextText.includes('throw')) signature += 'THROW_';
|
|
237
|
-
|
|
238
|
-
return signature || 'GENERIC_RETRY';
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
getFunctionBody(lines, startIndex) {
|
|
242
|
-
// Extract function body for analysis
|
|
243
|
-
let braceDepth = 0;
|
|
244
|
-
let foundStartBrace = false;
|
|
245
|
-
const bodyLines = [];
|
|
246
|
-
|
|
247
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
248
|
-
const line = lines[i];
|
|
249
|
-
|
|
250
|
-
for (const char of line) {
|
|
251
|
-
if (char === '{') {
|
|
252
|
-
braceDepth++;
|
|
253
|
-
foundStartBrace = true;
|
|
254
|
-
} else if (char === '}') {
|
|
255
|
-
braceDepth--;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (foundStartBrace) {
|
|
260
|
-
bodyLines.push(line);
|
|
261
|
-
|
|
262
|
-
if (braceDepth === 0) {
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return bodyLines.join('\n').toLowerCase();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
findDuplicateRetryLogic() {
|
|
272
|
-
const groups = [];
|
|
273
|
-
const processed = new Set();
|
|
274
|
-
|
|
275
|
-
this.retryPatterns.forEach((pattern, index) => {
|
|
276
|
-
if (processed.has(index)) return;
|
|
277
|
-
|
|
278
|
-
const similarPatterns = [pattern];
|
|
279
|
-
processed.add(index);
|
|
280
|
-
|
|
281
|
-
// Find similar patterns
|
|
282
|
-
this.retryPatterns.forEach((otherPattern, otherIndex) => {
|
|
283
|
-
if (otherIndex !== index && !processed.has(otherIndex)) {
|
|
284
|
-
if (this.areSimilarPatterns(pattern, otherPattern)) {
|
|
285
|
-
similarPatterns.push(otherPattern);
|
|
286
|
-
processed.add(otherIndex);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
if (similarPatterns.length > 0) {
|
|
292
|
-
groups.push({
|
|
293
|
-
signature: pattern.signature,
|
|
294
|
-
patterns: similarPatterns
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return groups;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
areSimilarPatterns(pattern1, pattern2) {
|
|
303
|
-
// Check if two patterns are similar enough to be considered duplicates
|
|
304
|
-
|
|
305
|
-
// Same signature indicates very similar patterns
|
|
306
|
-
if (pattern1.signature === pattern2.signature) {
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Compare characteristics
|
|
311
|
-
const similarities = [
|
|
312
|
-
pattern1.hasForLoop === pattern2.hasForLoop,
|
|
313
|
-
pattern1.hasWhileLoop === pattern2.hasWhileLoop,
|
|
314
|
-
pattern1.hasTryCatch === pattern2.hasTryCatch,
|
|
315
|
-
pattern1.hasMaxRetries === pattern2.hasMaxRetries,
|
|
316
|
-
pattern1.hasBackoff === pattern2.hasBackoff,
|
|
317
|
-
pattern1.hasSetTimeout === pattern2.hasSetTimeout
|
|
318
|
-
].filter(Boolean).length;
|
|
319
|
-
|
|
320
|
-
// Consider patterns similar if they share at least 4 out of 6 characteristics
|
|
321
|
-
return similarities >= 4;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
getFunctionNameForLine(lines, lineIndex) {
|
|
325
|
-
// Look backwards to find the function declaration for this line
|
|
326
|
-
for (let i = lineIndex; i >= 0; i--) {
|
|
327
|
-
const line = lines[i].trim();
|
|
328
|
-
|
|
329
|
-
// Match function declarations
|
|
330
|
-
const functionMatch = line.match(/(?:function|async\s+function)\s+(\w+)/);
|
|
331
|
-
if (functionMatch) {
|
|
332
|
-
return functionMatch[1];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Match arrow functions assigned to variables
|
|
336
|
-
const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=.*=>/);
|
|
337
|
-
if (arrowMatch) {
|
|
338
|
-
return arrowMatch[1];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Stop at class/module boundaries
|
|
342
|
-
if (line.includes('class ') || line.includes('module.exports') || line.includes('export')) {
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return 'anonymous';
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
module.exports = C047Analyzer;
|