@sun-asterisk/sunlint 1.2.1 → 1.2.2
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/config/rule-analysis-strategies.js +18 -2
- package/engines/eslint-engine.js +9 -11
- package/engines/heuristic-engine.js +55 -31
- package/package.json +2 -1
- 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-backup.js +426 -0
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -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/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 +155 -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/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -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/generate_insights.js +188 -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
|
@@ -0,0 +1,590 @@
|
|
|
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
|
+
// Enhanced 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
|
+
// Architectural layer detection patterns
|
|
21
|
+
this.layerPatterns = {
|
|
22
|
+
ui: ['component', 'view', 'page', 'modal', 'form', 'screen', 'widget', 'button'],
|
|
23
|
+
logic: ['service', 'usecase', 'viewmodel', 'controller', 'handler', 'manager', 'business'],
|
|
24
|
+
repository: ['repository', 'dao', 'store', 'cache', 'persistence', 'data'],
|
|
25
|
+
infrastructure: ['client', 'adapter', 'gateway', 'connector', 'network', 'http', 'api']
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Retry purpose classification
|
|
29
|
+
this.purposeIndicators = {
|
|
30
|
+
network: ['fetch', 'axios', 'request', 'http', 'api', 'ajax', 'xhr'],
|
|
31
|
+
database: ['query', 'transaction', 'connection', 'db', 'sql', 'insert', 'update'],
|
|
32
|
+
validation: ['validate', 'check', 'verify', 'confirm', 'assert'],
|
|
33
|
+
ui: ['click', 'submit', 'load', 'render', 'update', 'refresh'],
|
|
34
|
+
auth: ['login', 'authenticate', 'authorize', 'token', 'session']
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Allowed centralized retry utilities
|
|
38
|
+
this.allowedRetryUtils = [
|
|
39
|
+
'RetryUtil', 'retryWithBackoff', 'withRetry', 'retry',
|
|
40
|
+
'retryAsync', 'retryPromise', 'retryOperation',
|
|
41
|
+
'exponentialBackoff', 'linearBackoff'
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Detected retry patterns for duplicate checking with context
|
|
45
|
+
this.retryPatterns = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async analyze(files, language, options = {}) {
|
|
49
|
+
const violations = [];
|
|
50
|
+
|
|
51
|
+
for (const filePath of files) {
|
|
52
|
+
if (options.verbose) {
|
|
53
|
+
console.log(`🔍 Running C047 analysis on ${require('path').basename(filePath)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
58
|
+
this.retryPatterns = []; // Reset for each file
|
|
59
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
60
|
+
violations.push(...fileViolations);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
analyzeFile(content, filePath) {
|
|
70
|
+
const violations = [];
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
|
|
73
|
+
// Find all retry patterns in the file
|
|
74
|
+
this.findRetryPatterns(lines, filePath);
|
|
75
|
+
|
|
76
|
+
// Add architectural context to patterns
|
|
77
|
+
this.retryPatterns.forEach(pattern => {
|
|
78
|
+
if (!pattern.context) {
|
|
79
|
+
const contextLines = lines.slice(Math.max(0, pattern.line - 10), pattern.line + 10);
|
|
80
|
+
const contextContent = contextLines.join('\n');
|
|
81
|
+
pattern.context = this.analyzeArchitecturalContext(filePath, contextContent);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Enhanced duplicate detection with architectural intelligence
|
|
86
|
+
const duplicateGroups = this.enhancedDuplicateDetection();
|
|
87
|
+
|
|
88
|
+
// Generate violations with architectural context
|
|
89
|
+
const enhancedViolations = this.generateEnhancedViolations(duplicateGroups, filePath);
|
|
90
|
+
violations.push(...enhancedViolations);
|
|
91
|
+
|
|
92
|
+
return violations;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
findRetryPatterns(lines, filePath) {
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
const trimmedLine = line.trim().toLowerCase();
|
|
99
|
+
|
|
100
|
+
// Skip comments and empty lines
|
|
101
|
+
if (!trimmedLine || trimmedLine.startsWith('//') || trimmedLine.startsWith('/*')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Skip if using allowed retry utilities
|
|
106
|
+
if (this.usesAllowedRetryUtil(line)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Pattern 1: For/while loop with retry indicators
|
|
111
|
+
if (this.isRetryLoopPattern(line, lines, i)) {
|
|
112
|
+
const pattern = this.extractRetryPattern(lines, i, 'loop');
|
|
113
|
+
if (pattern) {
|
|
114
|
+
this.retryPatterns.push({
|
|
115
|
+
...pattern,
|
|
116
|
+
line: i + 1,
|
|
117
|
+
column: line.indexOf(line.trim()) + 1
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Pattern 2: Variable declarations with retry indicators
|
|
123
|
+
else if (this.isRetryVariableDeclaration(trimmedLine)) {
|
|
124
|
+
// Additional context check for false positives
|
|
125
|
+
const contextLines = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10));
|
|
126
|
+
const contextText = contextLines.join('\n').toLowerCase();
|
|
127
|
+
|
|
128
|
+
// Skip if it's clearly data processing context
|
|
129
|
+
if (contextText.includes('filter(') && contextText.includes('map(') &&
|
|
130
|
+
!contextText.includes('try') && !contextText.includes('catch') &&
|
|
131
|
+
!contextText.includes('timeout') && !contextText.includes('delay')) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pattern = this.extractRetryPattern(lines, i, 'variable');
|
|
136
|
+
if (pattern) {
|
|
137
|
+
this.retryPatterns.push({
|
|
138
|
+
...pattern,
|
|
139
|
+
line: i + 1,
|
|
140
|
+
column: line.indexOf(line.trim()) + 1
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Pattern 3: Recursive function with retry logic
|
|
146
|
+
else if (this.isRetryFunctionPattern(lines, i)) {
|
|
147
|
+
// Additional check for actual retry vs. recursive data processing
|
|
148
|
+
const contextLines = lines.slice(i, Math.min(lines.length, i + 20));
|
|
149
|
+
const contextText = contextLines.join('\n').toLowerCase();
|
|
150
|
+
|
|
151
|
+
// Skip recursive data processing functions
|
|
152
|
+
if (contextText.includes('flatmap') || contextText.includes('children') ||
|
|
153
|
+
(contextText.includes('recursive') && !contextText.includes('retry'))) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pattern = this.extractRetryPattern(lines, i, 'recursive');
|
|
158
|
+
if (pattern) {
|
|
159
|
+
this.retryPatterns.push({
|
|
160
|
+
...pattern,
|
|
161
|
+
line: i + 1,
|
|
162
|
+
column: line.indexOf(line.trim()) + 1
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isRetryLoopPattern(line, lines, index) {
|
|
170
|
+
const trimmedLine = line.trim().toLowerCase();
|
|
171
|
+
|
|
172
|
+
// Check for for/while loops with retry indicators
|
|
173
|
+
if ((trimmedLine.includes('for') || trimmedLine.includes('while')) &&
|
|
174
|
+
(trimmedLine.includes('(') && trimmedLine.includes(')'))) {
|
|
175
|
+
|
|
176
|
+
// Look in the loop condition and surrounding context for retry indicators
|
|
177
|
+
const contextLines = lines.slice(Math.max(0, index - 2), Math.min(lines.length, index + 10));
|
|
178
|
+
const contextText = contextLines.join('\n').toLowerCase();
|
|
179
|
+
|
|
180
|
+
return this.retryIndicators.some(indicator => contextText.includes(indicator)) &&
|
|
181
|
+
(contextText.includes('try') || contextText.includes('catch') || contextText.includes('error'));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
isRetryVariableDeclaration(line) {
|
|
188
|
+
// Skip simple constant definitions (export const X = number)
|
|
189
|
+
if (/^export\s+const\s+\w+\s*=\s*\d+/.test(line.trim())) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Skip test-related patterns (jest mocks, test descriptions)
|
|
194
|
+
if (/it\(|describe\(|test\(|mock\w*\(|\.mock/.test(line)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip HTTP response patterns
|
|
199
|
+
if (/response\.status\(|\.json\(|return.*errors/.test(line)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Skip simple array declarations
|
|
204
|
+
if (/(?:const|let|var)\s+\w+Array\s*=\s*\[\s*\]/.test(line.trim())) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check for variable declarations with retry-related names that involve logic
|
|
209
|
+
const declarationPatterns = [
|
|
210
|
+
/(?:const|let|var)\s+.*(?:retry|attempt|tries|maxretries|maxattempts)/,
|
|
211
|
+
/(?:retry|attempt|tries|maxretries|maxattempts)\s*[:=]/
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// Only consider it a retry pattern if it has logical complexity
|
|
215
|
+
if (declarationPatterns.some(pattern => pattern.test(line))) {
|
|
216
|
+
// Exclude simple constant assignments to numbers
|
|
217
|
+
if (/=\s*\d+\s*[;,]?\s*$/.test(line.trim())) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
// Exclude simple array initializations
|
|
221
|
+
if (/=\s*\[\s*\]\s*[;,]?\s*$/.test(line.trim())) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
isRetryFunctionPattern(lines, index) {
|
|
231
|
+
const line = lines[index].trim().toLowerCase();
|
|
232
|
+
|
|
233
|
+
// Skip test functions
|
|
234
|
+
if (line.includes('it(') || line.includes('describe(') || line.includes('test(')) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for function declarations with retry indicators
|
|
239
|
+
if ((line.includes('function') || line.includes('=>')) &&
|
|
240
|
+
this.retryIndicators.some(indicator => line.includes(indicator))) {
|
|
241
|
+
|
|
242
|
+
// Skip obvious non-retry functions (formatters, validators, mappers)
|
|
243
|
+
if (line.includes('format') || line.includes('validate') || line.includes('map') ||
|
|
244
|
+
line.includes('filter') || line.includes('transform')) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Look for retry logic in function body
|
|
249
|
+
const functionBody = this.getFunctionBody(lines, index);
|
|
250
|
+
if (!functionBody) return false;
|
|
251
|
+
|
|
252
|
+
// Check if it's a recursive function (calls itself) without actual retry patterns
|
|
253
|
+
const functionName = this.extractFunctionName(line);
|
|
254
|
+
if (functionName && functionBody.includes(functionName) &&
|
|
255
|
+
!functionBody.includes('try') && !functionBody.includes('catch') &&
|
|
256
|
+
!functionBody.includes('timeout') && !functionBody.includes('delay')) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return functionBody &&
|
|
261
|
+
this.retryIndicators.some(indicator => functionBody.includes(indicator)) &&
|
|
262
|
+
(functionBody.includes('try') || functionBody.includes('catch') ||
|
|
263
|
+
functionBody.includes('throw') || functionBody.includes('error'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
usesAllowedRetryUtil(line) {
|
|
270
|
+
return this.allowedRetryUtils.some(util =>
|
|
271
|
+
line.includes(util) && (line.includes('.') || line.includes('('))
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
extractRetryPattern(lines, startIndex, type) {
|
|
276
|
+
// Extract the pattern characteristics for similarity comparison
|
|
277
|
+
const contextLines = lines.slice(startIndex, Math.min(lines.length, startIndex + 20));
|
|
278
|
+
const contextText = contextLines.join('\n').toLowerCase();
|
|
279
|
+
|
|
280
|
+
// Extract key characteristics
|
|
281
|
+
const characteristics = {
|
|
282
|
+
type: type,
|
|
283
|
+
hasForLoop: contextText.includes('for'),
|
|
284
|
+
hasWhileLoop: contextText.includes('while'),
|
|
285
|
+
hasDoWhile: contextText.includes('do') && contextText.includes('while'),
|
|
286
|
+
hasTryCatch: contextText.includes('try') && contextText.includes('catch'),
|
|
287
|
+
hasMaxRetries: /max.*(?:retry|attempt|tries)/.test(contextText),
|
|
288
|
+
hasBackoff: contextText.includes('backoff') || contextText.includes('delay') || contextText.includes('timeout'),
|
|
289
|
+
hasExponential: contextText.includes('exponential') || /math\.pow|2\s*\*\*|\*\s*2/.test(contextText),
|
|
290
|
+
hasLinear: contextText.includes('linear') || /\*\s*(?:attempt|retry)/.test(contextText),
|
|
291
|
+
hasSetTimeout: contextText.includes('settimeout') || contextText.includes('promise') && contextText.includes('resolve'),
|
|
292
|
+
signature: this.generatePatternSignature(contextText)
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return characteristics;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
generatePatternSignature(contextText) {
|
|
299
|
+
// Create a normalized signature for pattern matching
|
|
300
|
+
let signature = '';
|
|
301
|
+
|
|
302
|
+
if (contextText.includes('for')) signature += 'FOR_';
|
|
303
|
+
if (contextText.includes('while')) signature += 'WHILE_';
|
|
304
|
+
if (/max.*(?:retry|attempt)/.test(contextText)) signature += 'MAX_';
|
|
305
|
+
if (contextText.includes('try') && contextText.includes('catch')) signature += 'TRYCATCH_';
|
|
306
|
+
if (contextText.includes('settimeout') || contextText.includes('delay')) signature += 'DELAY_';
|
|
307
|
+
if (contextText.includes('exponential') || /math\.pow/.test(contextText)) signature += 'EXPONENTIAL_';
|
|
308
|
+
if (contextText.includes('throw')) signature += 'THROW_';
|
|
309
|
+
|
|
310
|
+
return signature || 'GENERIC_RETRY';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
extractFunctionName(line) {
|
|
314
|
+
// Extract function name from function declaration
|
|
315
|
+
const functionMatch = line.match(/(?:function\s+(\w+)|(\w+)\s*(?:\([^)]*\))?\s*=>|(\w+)\s*:\s*(?:async\s+)?function)/);
|
|
316
|
+
if (functionMatch) {
|
|
317
|
+
return functionMatch[1] || functionMatch[2] || functionMatch[3];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Try to extract from method declaration
|
|
321
|
+
const methodMatch = line.match(/(\w+)\s*\(/);
|
|
322
|
+
if (methodMatch) {
|
|
323
|
+
return methodMatch[1];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
getFunctionBody(lines, startIndex) {
|
|
330
|
+
// Extract function body for analysis
|
|
331
|
+
let braceDepth = 0;
|
|
332
|
+
let foundStartBrace = false;
|
|
333
|
+
const bodyLines = [];
|
|
334
|
+
|
|
335
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
336
|
+
const line = lines[i];
|
|
337
|
+
|
|
338
|
+
for (const char of line) {
|
|
339
|
+
if (char === '{') {
|
|
340
|
+
braceDepth++;
|
|
341
|
+
foundStartBrace = true;
|
|
342
|
+
} else if (char === '}') {
|
|
343
|
+
braceDepth--;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (foundStartBrace) {
|
|
348
|
+
bodyLines.push(line);
|
|
349
|
+
|
|
350
|
+
if (braceDepth === 0) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return bodyLines.join('\n').toLowerCase();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
findDuplicateRetryLogic() {
|
|
360
|
+
const groups = [];
|
|
361
|
+
const processed = new Set();
|
|
362
|
+
|
|
363
|
+
this.retryPatterns.forEach((pattern, index) => {
|
|
364
|
+
if (processed.has(index)) return;
|
|
365
|
+
|
|
366
|
+
const similarPatterns = [pattern];
|
|
367
|
+
processed.add(index);
|
|
368
|
+
|
|
369
|
+
// Find similar patterns
|
|
370
|
+
this.retryPatterns.forEach((otherPattern, otherIndex) => {
|
|
371
|
+
if (otherIndex !== index && !processed.has(otherIndex)) {
|
|
372
|
+
if (this.areSimilarPatterns(pattern, otherPattern)) {
|
|
373
|
+
similarPatterns.push(otherPattern);
|
|
374
|
+
processed.add(otherIndex);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (similarPatterns.length > 0) {
|
|
380
|
+
groups.push({
|
|
381
|
+
signature: pattern.signature,
|
|
382
|
+
patterns: similarPatterns
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return groups;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
areSimilarPatterns(pattern1, pattern2) {
|
|
391
|
+
// Check if two patterns are similar enough to be considered duplicates
|
|
392
|
+
|
|
393
|
+
// Same signature indicates very similar patterns
|
|
394
|
+
if (pattern1.signature === pattern2.signature) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Compare characteristics
|
|
399
|
+
const similarities = [
|
|
400
|
+
pattern1.hasForLoop === pattern2.hasForLoop,
|
|
401
|
+
pattern1.hasWhileLoop === pattern2.hasWhileLoop,
|
|
402
|
+
pattern1.hasTryCatch === pattern2.hasTryCatch,
|
|
403
|
+
pattern1.hasMaxRetries === pattern2.hasMaxRetries,
|
|
404
|
+
pattern1.hasBackoff === pattern2.hasBackoff,
|
|
405
|
+
pattern1.hasSetTimeout === pattern2.hasSetTimeout
|
|
406
|
+
].filter(Boolean).length;
|
|
407
|
+
|
|
408
|
+
// Consider patterns similar if they share at least 4 out of 6 characteristics
|
|
409
|
+
return similarities >= 4;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
getFunctionNameForLine(lines, lineIndex) {
|
|
413
|
+
// Look backwards to find the function declaration for this line
|
|
414
|
+
for (let i = lineIndex; i >= 0; i--) {
|
|
415
|
+
const line = lines[i].trim();
|
|
416
|
+
|
|
417
|
+
// Match function declarations
|
|
418
|
+
const functionMatch = line.match(/(?:function|async\s+function)\s+(\w+)/);
|
|
419
|
+
if (functionMatch) {
|
|
420
|
+
return functionMatch[1];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Match arrow functions assigned to variables
|
|
424
|
+
const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=.*=>/);
|
|
425
|
+
if (arrowMatch) {
|
|
426
|
+
return arrowMatch[1];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Stop at class/module boundaries
|
|
430
|
+
if (line.includes('class ') || line.includes('module.exports') || line.includes('export')) {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return 'anonymous';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 🧠 ARCHITECTURAL INTELLIGENCE METHODS
|
|
439
|
+
|
|
440
|
+
analyzeArchitecturalContext(filePath, content) {
|
|
441
|
+
const fileName = require('path').basename(filePath).toLowerCase();
|
|
442
|
+
const dirPath = require('path').dirname(filePath).toLowerCase();
|
|
443
|
+
|
|
444
|
+
// Determine architectural layer
|
|
445
|
+
let layer = 'unknown';
|
|
446
|
+
for (const [layerName, patterns] of Object.entries(this.layerPatterns)) {
|
|
447
|
+
if (patterns.some(pattern => fileName.includes(pattern) || dirPath.includes(pattern))) {
|
|
448
|
+
layer = layerName;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Extract retry purpose/scope
|
|
454
|
+
const purpose = this.extractRetryPurpose(content);
|
|
455
|
+
|
|
456
|
+
return { layer, purpose, filePath: fileName };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
extractRetryPurpose(content) {
|
|
460
|
+
const contentLower = content.toLowerCase();
|
|
461
|
+
|
|
462
|
+
for (const [purpose, indicators] of Object.entries(this.purposeIndicators)) {
|
|
463
|
+
if (indicators.some(indicator => contentLower.includes(indicator))) {
|
|
464
|
+
return purpose;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return 'general';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
enhancedDuplicateDetection() {
|
|
472
|
+
const groups = [];
|
|
473
|
+
const processed = new Set();
|
|
474
|
+
|
|
475
|
+
// Add architectural context to each pattern
|
|
476
|
+
this.retryPatterns.forEach((pattern, index) => {
|
|
477
|
+
if (!pattern.context) {
|
|
478
|
+
// This would be called during pattern extraction in a real implementation
|
|
479
|
+
pattern.context = { layer: 'unknown', purpose: 'general' };
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
this.retryPatterns.forEach((pattern, index) => {
|
|
484
|
+
if (processed.has(index)) return;
|
|
485
|
+
|
|
486
|
+
const similarPatterns = [pattern];
|
|
487
|
+
processed.add(index);
|
|
488
|
+
|
|
489
|
+
this.retryPatterns.forEach((otherPattern, otherIndex) => {
|
|
490
|
+
if (otherIndex !== index && !processed.has(otherIndex)) {
|
|
491
|
+
if (this.areSimilarPatternsWithContext(pattern, otherPattern)) {
|
|
492
|
+
similarPatterns.push(otherPattern);
|
|
493
|
+
processed.add(otherIndex);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (similarPatterns.length > 1) {
|
|
499
|
+
const legitimacy = this.assessDuplicateLegitimacy(similarPatterns);
|
|
500
|
+
|
|
501
|
+
if (!legitimacy.isLegitimate) {
|
|
502
|
+
groups.push({
|
|
503
|
+
signature: pattern.signature,
|
|
504
|
+
patterns: similarPatterns,
|
|
505
|
+
legitimacy: legitimacy
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
return groups;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
areSimilarPatternsWithContext(pattern1, pattern2) {
|
|
515
|
+
// First check basic similarity
|
|
516
|
+
if (!this.areSimilarPatterns(pattern1, pattern2)) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Enhanced context-aware similarity
|
|
521
|
+
const context1 = pattern1.context || { layer: 'unknown', purpose: 'general' };
|
|
522
|
+
const context2 = pattern2.context || { layer: 'unknown', purpose: 'general' };
|
|
523
|
+
|
|
524
|
+
// Same layer AND same purpose = likely duplicate
|
|
525
|
+
// Different layer OR different purpose = likely legitimate
|
|
526
|
+
return context1.layer === context2.layer && context1.purpose === context2.purpose;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
assessDuplicateLegitimacy(patterns) {
|
|
530
|
+
const layers = new Set(patterns.map(p => p.context?.layer || 'unknown'));
|
|
531
|
+
const purposes = new Set(patterns.map(p => p.context?.purpose || 'general'));
|
|
532
|
+
|
|
533
|
+
// Cross-layer retries are often legitimate
|
|
534
|
+
if (layers.size > 1) {
|
|
535
|
+
return {
|
|
536
|
+
isLegitimate: true,
|
|
537
|
+
reason: 'Cross-layer retry patterns are architecturally valid',
|
|
538
|
+
confidence: 'high'
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Different purposes in same layer can be legitimate
|
|
543
|
+
if (purposes.size > 1) {
|
|
544
|
+
return {
|
|
545
|
+
isLegitimate: true,
|
|
546
|
+
reason: 'Different retry purposes in same layer',
|
|
547
|
+
confidence: 'medium'
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Same layer, same purpose - likely duplicate
|
|
552
|
+
const layer = [...layers][0];
|
|
553
|
+
const purpose = [...purposes][0];
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
isLegitimate: false,
|
|
557
|
+
reason: `Duplicate ${purpose} retry logic in ${layer} layer`,
|
|
558
|
+
confidence: 'high',
|
|
559
|
+
severity: 'warning'
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
generateEnhancedViolations(duplicateGroups, filePath) {
|
|
564
|
+
const violations = [];
|
|
565
|
+
|
|
566
|
+
duplicateGroups.forEach(group => {
|
|
567
|
+
const firstPattern = group.patterns[0];
|
|
568
|
+
|
|
569
|
+
violations.push({
|
|
570
|
+
file: filePath,
|
|
571
|
+
line: firstPattern.line,
|
|
572
|
+
column: firstPattern.column || 1,
|
|
573
|
+
message: `${group.legitimacy.reason} (${group.patterns.length} similar patterns found). Consider using a centralized retry utility.`,
|
|
574
|
+
severity: group.legitimacy.severity || 'warning',
|
|
575
|
+
ruleId: this.ruleId,
|
|
576
|
+
type: 'duplicate_retry_logic',
|
|
577
|
+
duplicateCount: group.patterns.length,
|
|
578
|
+
architecturalContext: {
|
|
579
|
+
layers: [...new Set(group.patterns.map(p => p.context?.layer))],
|
|
580
|
+
purposes: [...new Set(group.patterns.map(p => p.context?.purpose))],
|
|
581
|
+
confidence: group.legitimacy.confidence
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return violations;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
module.exports = C047Analyzer;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C075 Rule: Functions must have explicit return type declarations
|
|
3
|
+
* Ensures type safety by requiring explicit return type annotations
|
|
4
|
+
* Severity: warning
|
|
5
|
+
* Category: Quality
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
class C075ExplicitReturnTypesAnalyzer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.ruleId = 'C075';
|
|
14
|
+
this.ruleName = 'Explicit Function Return Types';
|
|
15
|
+
this.description = 'Functions must have explicit return type declarations';
|
|
16
|
+
this.severity = 'warning';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async analyze(files, language, config) {
|
|
20
|
+
const violations = [];
|
|
21
|
+
|
|
22
|
+
for (const filePath of files) {
|
|
23
|
+
try {
|
|
24
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
25
|
+
const fileViolations = await this.analyzeFile(filePath, fileContent, language, config);
|
|
26
|
+
violations.push(...fileViolations);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn(`C075 analysis error for ${filePath}:`, error.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return violations;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async analyzeFile(filePath, fileContent, language, config) {
|
|
36
|
+
const violations = [];
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Skip non-TypeScript files
|
|
40
|
+
if (!this.isTypeScriptFile(filePath)) {
|
|
41
|
+
return violations;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Simple regex-based analysis for now
|
|
45
|
+
const lines = fileContent.split('\n');
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
const lineNumber = i + 1;
|
|
50
|
+
|
|
51
|
+
// Look for function declarations without return types
|
|
52
|
+
const functionPatterns = [
|
|
53
|
+
/^(\s*)(function\s+\w+\s*\([^)]*\))\s*\{/, // function name() {
|
|
54
|
+
/^(\s*)(export\s+function\s+\w+\s*\([^)]*\))\s*\{/, // export function name() {
|
|
55
|
+
/^(\s*)(\w+\s*=\s*function\s*\([^)]*\))\s*\{/, // name = function() {
|
|
56
|
+
/^(\s*)(\w+\s*=\s*\([^)]*\)\s*=>\s*)\{/, // name = () => {
|
|
57
|
+
/^(\s*)(\w+\([^)]*\))\s*\{/, // method() {
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const pattern of functionPatterns) {
|
|
61
|
+
const match = line.match(pattern);
|
|
62
|
+
if (match) {
|
|
63
|
+
const fullMatch = match[2];
|
|
64
|
+
|
|
65
|
+
// Skip if already has return type annotation
|
|
66
|
+
if (fullMatch.includes('):') || line.includes('):')) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip constructors
|
|
71
|
+
if (fullMatch.includes('constructor')) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
violations.push({
|
|
76
|
+
ruleId: this.ruleId,
|
|
77
|
+
severity: this.severity,
|
|
78
|
+
message: `Function is missing explicit return type annotation`,
|
|
79
|
+
filePath: filePath,
|
|
80
|
+
line: lineNumber,
|
|
81
|
+
column: match[1].length + 1,
|
|
82
|
+
source: line.trim(),
|
|
83
|
+
suggestion: 'Add explicit return type annotation (: ReturnType)'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.warn(`C075 analysis error for ${filePath}:`, error.message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return violations;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isTypeScriptFile(filePath) {
|
|
97
|
+
return /\.(ts|tsx)$/.test(filePath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = C075ExplicitReturnTypesAnalyzer;
|
|
102
|
+
|
|
103
|
+
module.exports = C075ExplicitReturnTypesAnalyzer;
|