@sun-asterisk/sunlint 1.2.2 ā 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/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 +171 -19
- package/engines/heuristic-engine.js +511 -78
- package/integrations/eslint/plugin/index.js +27 -27
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- 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/index.js +7 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -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/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Symbol-Based Analyzer for C047 - No Duplicate Retry Logic
|
|
3
|
+
* Using ts-morph for TypeScript symbol resolution and semantic analysis
|
|
4
|
+
*
|
|
5
|
+
* Approach:
|
|
6
|
+
* 1. Load known retry functions configuration
|
|
7
|
+
* 2. Detect retry patterns via AST + Symbol analysis
|
|
8
|
+
* 3. Group by layers and flows
|
|
9
|
+
* 4. Apply violation detection logic
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Import ts-morph for AST and symbol analysis
|
|
16
|
+
const { Project } = require('ts-morph');
|
|
17
|
+
|
|
18
|
+
class C047SymbolAnalyzerEnhanced {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.ruleId = 'C047';
|
|
21
|
+
this.ruleName = 'No Duplicate Retry Logic (Symbol-Based)';
|
|
22
|
+
this.description = 'Detect duplicate retry logic across layers using semantic analysis';
|
|
23
|
+
|
|
24
|
+
// Will be populated from config
|
|
25
|
+
this.knownRetryFunctions = [];
|
|
26
|
+
this.retryPatterns = new Map(); // flowName -> [patterns...]
|
|
27
|
+
this.project = null;
|
|
28
|
+
|
|
29
|
+
// Layer detection patterns
|
|
30
|
+
this.layerPatterns = {
|
|
31
|
+
ui: ['component', 'view', 'page', 'modal', 'form', 'screen', 'widget', '/ui/', '/components/'],
|
|
32
|
+
usecase: ['usecase', 'use-case', 'usecases', 'service', 'business', '/usecases/', '/services/'],
|
|
33
|
+
repository: ['repository', 'repo', 'dao', 'store', 'persistence', '/repositories/', '/data/'],
|
|
34
|
+
api: ['api', 'client', 'adapter', 'gateway', 'connector', '/api/', '/clients/', '/gateways/']
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Retry detection patterns
|
|
38
|
+
this.retryIndicators = {
|
|
39
|
+
variables: ['retry', 'attempt', 'tries', 'maxRetries', 'maxAttempts', 'retryCount'],
|
|
40
|
+
functions: ['retry', 'retryAsync', 'withRetry', 'retryOperation'],
|
|
41
|
+
keywords: ['retry', 'attempt', 'tries']
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async analyze(files, language, options = {}) {
|
|
46
|
+
const verbose = options.verbose || false;
|
|
47
|
+
this.verbose = verbose; // Store verbose setting for other methods
|
|
48
|
+
|
|
49
|
+
if (verbose) {
|
|
50
|
+
console.log(`[DEBUG] š Starting Symbol Analysis...`);
|
|
51
|
+
console.log(`[DEBUG] š Files: ${files.length}`);
|
|
52
|
+
console.log(`[DEBUG] š£ļø Language: ${language}`);
|
|
53
|
+
console.log(`[DEBUG] āļø Options:`, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (language !== 'typescript' && language !== 'javascript') {
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.warn('ā ļø Symbol analyzer works best with TypeScript/JavaScript files');
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Step 1: Load configuration
|
|
65
|
+
if (verbose) {
|
|
66
|
+
console.log(`[DEBUG] š Step 1: Loading configuration...`);
|
|
67
|
+
}
|
|
68
|
+
await this.loadConfiguration();
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`[DEBUG] ā
Configuration loaded`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 2: Initialize ts-morph project
|
|
74
|
+
if (verbose) {
|
|
75
|
+
console.log(`[DEBUG] šļø Step 2: Initializing project...`);
|
|
76
|
+
}
|
|
77
|
+
await this.initializeProject(files, options);
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(`[DEBUG] ā
Project initialized`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Step 3: Analyze all files for retry patterns
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(`[DEBUG] š Step 3: Detecting retry patterns...`);
|
|
85
|
+
}
|
|
86
|
+
const allRetryPatterns = await this.detectRetryPatterns(files, options);
|
|
87
|
+
if (verbose) {
|
|
88
|
+
console.log(`[DEBUG] ā
Pattern detection complete: ${allRetryPatterns.length} patterns`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Step 4: Group by layers and flows
|
|
92
|
+
if (verbose) {
|
|
93
|
+
console.log(`[DEBUG] š Step 4: Grouping patterns...`);
|
|
94
|
+
}
|
|
95
|
+
const layeredPatterns = this.groupByLayersAndFlows(allRetryPatterns);
|
|
96
|
+
if (verbose) {
|
|
97
|
+
console.log(`[DEBUG] ā
Grouping complete`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Step 5: Apply violation detection logic
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.log(`[DEBUG] ā ļø Step 5: Detecting violations...`);
|
|
103
|
+
}
|
|
104
|
+
const violations = this.detectViolations(layeredPatterns);
|
|
105
|
+
if (verbose) {
|
|
106
|
+
console.log(`[DEBUG] ā
Violation detection complete: ${violations.length} violations`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.verbose) {
|
|
110
|
+
this.printAnalysisStats(allRetryPatterns, layeredPatterns, violations);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (verbose) {
|
|
114
|
+
console.log(`[DEBUG] šÆ Symbol Analysis complete!`);
|
|
115
|
+
}
|
|
116
|
+
return violations;
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('ā Symbol analyzer failed:', error.message);
|
|
120
|
+
if (verbose) {
|
|
121
|
+
console.error('Stack trace:', error.stack);
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async loadConfiguration() {
|
|
128
|
+
try {
|
|
129
|
+
// Try to load from config file first
|
|
130
|
+
const configPath = path.join(__dirname, 'symbol-config.json');
|
|
131
|
+
|
|
132
|
+
if (fs.existsSync(configPath)) {
|
|
133
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
134
|
+
this.knownRetryFunctions = config.knownRetryFunctions || [];
|
|
135
|
+
} else {
|
|
136
|
+
// Use default configuration
|
|
137
|
+
this.knownRetryFunctions = [
|
|
138
|
+
// HTTP libraries with built-in retry
|
|
139
|
+
'axios.get', 'axios.post', 'axios.put', 'axios.delete', 'axios.patch',
|
|
140
|
+
'axios.request', 'axios.head', 'axios.options',
|
|
141
|
+
|
|
142
|
+
// React Query / TanStack Query
|
|
143
|
+
'useQuery', 'useMutation', 'useInfiniteQuery',
|
|
144
|
+
'queryClient.fetchQuery', 'queryClient.prefetchQuery',
|
|
145
|
+
|
|
146
|
+
// Apollo GraphQL
|
|
147
|
+
'apolloClient.query', 'apolloClient.mutate', 'apolloClient.watchQuery',
|
|
148
|
+
'useQuery', 'useMutation', 'useLazyQuery',
|
|
149
|
+
|
|
150
|
+
// Generic API services
|
|
151
|
+
'apiService.get', 'apiService.post', 'apiService.put', 'apiService.delete',
|
|
152
|
+
'httpClient.get', 'httpClient.post', 'httpClient.request',
|
|
153
|
+
|
|
154
|
+
// Popular retry libraries
|
|
155
|
+
'retryAsync', 'withRetry', 'retry', 'p-retry',
|
|
156
|
+
'exponentialBackoff', 'retryPromise',
|
|
157
|
+
|
|
158
|
+
// Framework-specific
|
|
159
|
+
'fetch', 'fetch-retry', 'node-fetch',
|
|
160
|
+
'got', 'superagent', 'request-promise'
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
// Save default config for future reference
|
|
164
|
+
this.saveDefaultConfiguration(configPath);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Only log if verbose mode or first time setup
|
|
168
|
+
if (this.verbose !== false) {
|
|
169
|
+
console.log(`[DEBUG] š§ Loaded ${this.knownRetryFunctions.length} known retry functions`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.warn('ā ļø Failed to load configuration, using defaults:', error.message);
|
|
174
|
+
this.knownRetryFunctions = ['axios.get', 'axios.post', 'useQuery', 'apiService.get'];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
saveDefaultConfiguration(configPath) {
|
|
179
|
+
try {
|
|
180
|
+
const defaultConfig = {
|
|
181
|
+
knownRetryFunctions: this.knownRetryFunctions,
|
|
182
|
+
_description: "Configuration for Symbol-Based Analysis of retry functions",
|
|
183
|
+
_usage: "Add functions that have built-in retry mechanisms to avoid false positives"
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
187
|
+
if (this.verbose) {
|
|
188
|
+
console.log(`[DEBUG] š Created default configuration at ${configPath}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn('ā ļø Could not save default configuration:', error.message);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async initializeProject(files, options) {
|
|
197
|
+
if (this.verbose) {
|
|
198
|
+
console.log(`[DEBUG] šļø Initializing project with ${files.length} files...`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await this.initializeTsMorphProject(files);
|
|
203
|
+
if (this.verbose) {
|
|
204
|
+
console.log(`[DEBUG] ā
Project initialization complete`);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw new Error(`Failed to initialize project: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async initializeTsMorphProject(files) {
|
|
212
|
+
try {
|
|
213
|
+
if (this.verbose) {
|
|
214
|
+
console.log(`[DEBUG] šļø Initializing ts-morph project for ${files.length} files...`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const projectConfig = {
|
|
218
|
+
useInMemoryFileSystem: true,
|
|
219
|
+
compilerOptions: {
|
|
220
|
+
target: 'es2018',
|
|
221
|
+
module: 'commonjs',
|
|
222
|
+
strict: false,
|
|
223
|
+
allowJs: true,
|
|
224
|
+
skipLibCheck: true,
|
|
225
|
+
noEmit: true
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (this.verbose) {
|
|
230
|
+
console.log(`[DEBUG] š¦ Creating ts-morph Project...`);
|
|
231
|
+
}
|
|
232
|
+
this.project = new Project(projectConfig);
|
|
233
|
+
if (this.verbose) {
|
|
234
|
+
console.log(`[DEBUG] ā
Project created successfully`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Add ALL TypeScript files to project for cross-file analysis
|
|
238
|
+
let addedCount = 0;
|
|
239
|
+
const maxFiles = 50; // Reasonable limit for performance
|
|
240
|
+
|
|
241
|
+
for (const filePath of files.slice(0, maxFiles)) {
|
|
242
|
+
if (this.isTypeScriptFile(filePath)) {
|
|
243
|
+
try {
|
|
244
|
+
if (this.verbose) {
|
|
245
|
+
console.log(`[DEBUG] š Adding file: ${path.basename(filePath)}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!require('fs').existsSync(filePath)) {
|
|
249
|
+
console.warn(`ā ļø File not found: ${filePath}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const fileContent = require('fs').readFileSync(filePath, 'utf8');
|
|
254
|
+
this.project.createSourceFile(path.basename(filePath), fileContent);
|
|
255
|
+
addedCount++;
|
|
256
|
+
if (this.verbose) {
|
|
257
|
+
console.log(`[DEBUG] ā
File added: ${path.basename(filePath)}`);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.warn(`ā ļø Failed to add ${path.basename(filePath)}: ${error.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (files.length > maxFiles && this.verbose) {
|
|
266
|
+
console.log(`[DEBUG] š Limited analysis to ${maxFiles} files for performance`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (this.verbose) {
|
|
270
|
+
console.log(`[DEBUG] šļø Project initialization complete: ${addedCount} files added`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new Error(`Failed to initialize ts-morph project: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async detectRetryPatterns(files, options) {
|
|
279
|
+
if (this.verbose) {
|
|
280
|
+
console.log(`[DEBUG] š Step 3: Detecting retry patterns...`);
|
|
281
|
+
}
|
|
282
|
+
const allPatterns = [];
|
|
283
|
+
|
|
284
|
+
const sourceFiles = this.project.getSourceFiles();
|
|
285
|
+
if (this.verbose) {
|
|
286
|
+
console.log(`[DEBUG] š Found ${sourceFiles.length} source files to analyze`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < sourceFiles.length; i++) {
|
|
290
|
+
const sourceFile = sourceFiles[i];
|
|
291
|
+
const fileName = sourceFile.getBaseName();
|
|
292
|
+
|
|
293
|
+
if (options.verbose) {
|
|
294
|
+
console.log(` š Analyzing ${i + 1}/${sourceFiles.length}: ${fileName}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const filePatterns = await this.analyzeSourceFile(sourceFile);
|
|
299
|
+
allPatterns.push(...filePatterns);
|
|
300
|
+
|
|
301
|
+
if (options.verbose) {
|
|
302
|
+
console.log(` ā
Found ${filePatterns.length} patterns in ${fileName}`);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn(` ā ļø Error analyzing ${fileName}: ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (this.verbose) {
|
|
310
|
+
console.log(`[DEBUG] šÆ Total patterns detected: ${allPatterns.length}`);
|
|
311
|
+
}
|
|
312
|
+
return allPatterns;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async analyzeSourceFile(sourceFile) {
|
|
316
|
+
const patterns = [];
|
|
317
|
+
const filePath = sourceFile.getFilePath() || sourceFile.getBaseName();
|
|
318
|
+
|
|
319
|
+
if (this.verbose) {
|
|
320
|
+
console.log(`[DEBUG] š Analyzing ${require('path').basename(filePath)}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Get all classes and their methods for better context
|
|
324
|
+
const classes = sourceFile.getClasses();
|
|
325
|
+
if (this.verbose) {
|
|
326
|
+
console.log(`[DEBUG] š¢ Found ${classes.length} classes`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const cls of classes) {
|
|
330
|
+
const className = cls.getName();
|
|
331
|
+
if (this.verbose) {
|
|
332
|
+
console.log(`[DEBUG] š¦ Analyzing class: ${className}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const methods = cls.getMethods();
|
|
336
|
+
if (this.verbose) {
|
|
337
|
+
console.log(`[DEBUG] š§ Found ${methods.length} methods in ${className}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const method of methods) {
|
|
341
|
+
const methodName = method.getName();
|
|
342
|
+
const fullFunctionName = `${className}.${methodName}`;
|
|
343
|
+
|
|
344
|
+
if (this.verbose) {
|
|
345
|
+
console.log(`[DEBUG] šÆ Analyzing method: ${fullFunctionName}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Detect retry patterns in method
|
|
349
|
+
const patterns_found = await this.analyzeFunction(method, fullFunctionName, filePath);
|
|
350
|
+
patterns.push(...patterns_found);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Also analyze standalone functions
|
|
355
|
+
const functions = sourceFile.getFunctions();
|
|
356
|
+
if (this.verbose) {
|
|
357
|
+
console.log(`[DEBUG] š§ Found ${functions.length} standalone functions`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const func of functions) {
|
|
361
|
+
const functionName = this.getFunctionName(func);
|
|
362
|
+
if (this.verbose) {
|
|
363
|
+
console.log(`[DEBUG] šÆ Analyzing function: ${functionName}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const patterns_found = await this.analyzeFunction(func, functionName, filePath);
|
|
367
|
+
patterns.push(...patterns_found);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Analyze variable declarations with arrow functions (React components)
|
|
371
|
+
const variableDeclarations = sourceFile.getVariableDeclarations();
|
|
372
|
+
if (this.verbose) {
|
|
373
|
+
console.log(`[DEBUG] ā” Found ${variableDeclarations.length} variable declarations`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const varDecl of variableDeclarations) {
|
|
377
|
+
const initializer = varDecl.getInitializer();
|
|
378
|
+
if (initializer && (initializer.getKind() === require('ts-morph').SyntaxKind.ArrowFunction ||
|
|
379
|
+
initializer.getKind() === require('ts-morph').SyntaxKind.FunctionExpression)) {
|
|
380
|
+
|
|
381
|
+
const functionName = varDecl.getName();
|
|
382
|
+
if (this.verbose) {
|
|
383
|
+
console.log(`[DEBUG] ā” Analyzing arrow function: ${functionName}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check for useQuery calls with retry
|
|
387
|
+
const useQueryPatterns = this.detectUseQueryRetryPatterns(initializer, functionName, filePath);
|
|
388
|
+
patterns.push(...useQueryPatterns);
|
|
389
|
+
|
|
390
|
+
// Also analyze for standard retry patterns
|
|
391
|
+
const patterns_found = await this.analyzeFunction(initializer, functionName, filePath);
|
|
392
|
+
patterns.push(...patterns_found);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (this.verbose) {
|
|
397
|
+
console.log(`[DEBUG] š Total patterns found in this file: ${patterns.length}`);
|
|
398
|
+
}
|
|
399
|
+
if (this.verbose) {
|
|
400
|
+
patterns.forEach((pattern, i) => {
|
|
401
|
+
console.log(`[DEBUG] ${i + 1}. ${pattern.functionName} (${pattern.retryType}) - Layer: ${pattern.layer}, Flow: ${pattern.apiFlow}`);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return patterns;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
detectUseQueryRetryPatterns(functionNode, functionName, filePath) {
|
|
409
|
+
if (this.verbose) {
|
|
410
|
+
console.log(`[DEBUG] š Checking useQuery patterns in ${functionName}`);
|
|
411
|
+
}
|
|
412
|
+
const patterns = [];
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
// Find useQuery calls
|
|
416
|
+
const callExpressions = functionNode.getDescendantsOfKind(require('ts-morph').SyntaxKind.CallExpression);
|
|
417
|
+
|
|
418
|
+
for (const call of callExpressions) {
|
|
419
|
+
const callText = call.getText();
|
|
420
|
+
if (this.verbose) {
|
|
421
|
+
console.log(`[DEBUG] š Found call: ${callText.substring(0, 50)}...`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if it's useQuery
|
|
425
|
+
if (callText.includes('useQuery')) {
|
|
426
|
+
if (this.verbose) {
|
|
427
|
+
console.log(`[DEBUG] šÆ DETECTED: useQuery call`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Extract retry configuration
|
|
431
|
+
let retryCount = 3; // default useQuery retry
|
|
432
|
+
let hasRetryEnabled = true;
|
|
433
|
+
|
|
434
|
+
// Check for explicit retry configuration
|
|
435
|
+
const retryMatch = callText.match(/retry:\s*(\d+|false|true)/);
|
|
436
|
+
if (retryMatch) {
|
|
437
|
+
const retryValue = retryMatch[1];
|
|
438
|
+
if (retryValue === 'false') {
|
|
439
|
+
hasRetryEnabled = false;
|
|
440
|
+
retryCount = 0;
|
|
441
|
+
} else if (retryValue === 'true') {
|
|
442
|
+
hasRetryEnabled = true;
|
|
443
|
+
retryCount = 3; // default
|
|
444
|
+
} else {
|
|
445
|
+
retryCount = parseInt(retryValue);
|
|
446
|
+
hasRetryEnabled = retryCount > 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.verbose) {
|
|
450
|
+
console.log(` š Explicit retry config: ${retryValue} -> ${retryCount} retries`);
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
// No explicit retry config = default retry: 3
|
|
454
|
+
if (this.verbose) {
|
|
455
|
+
console.log(` š Default retry config: ${retryCount} retries`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Only create pattern if retry is enabled (> 0)
|
|
460
|
+
if (hasRetryEnabled && retryCount > 0) {
|
|
461
|
+
if (this.verbose) {
|
|
462
|
+
console.log(` ā
useQuery has retry enabled: ${retryCount}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Try to extract API flow from the API call within useQuery
|
|
466
|
+
let apiFlow = this.extractApiFlowFromUseQuery(callText, functionName, filePath);
|
|
467
|
+
|
|
468
|
+
patterns.push(this.createRetryPatternWithFlow(
|
|
469
|
+
functionName, filePath, 'uses_active_retry_function',
|
|
470
|
+
call.getStartLineNumber(), `useQuery with retry: ${retryCount}`, apiFlow
|
|
471
|
+
));
|
|
472
|
+
} else {
|
|
473
|
+
if (this.verbose) {
|
|
474
|
+
console.log(` ā useQuery retry disabled (${retryCount})`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.warn(`ā ļø Error detecting useQuery patterns in ${functionName}:`, error.message);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (this.verbose) {
|
|
485
|
+
console.log(` š useQuery patterns found: ${patterns.length}`);
|
|
486
|
+
}
|
|
487
|
+
return patterns;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
extractApiFlowFromUseQuery(useQueryCallText, functionName, filePath) {
|
|
491
|
+
if (this.verbose) {
|
|
492
|
+
console.log(` š Extracting API flow from useQuery call...`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Look for API class calls like "new UserAPI().fetchUser"
|
|
496
|
+
const apiClassMatch = useQueryCallText.match(/new\s+(\w*API)\(\)\.(\w+)/i);
|
|
497
|
+
if (apiClassMatch) {
|
|
498
|
+
const apiClass = apiClassMatch[1]; // UserAPI
|
|
499
|
+
const apiMethod = apiClassMatch[2]; // fetchUser
|
|
500
|
+
if (this.verbose) {
|
|
501
|
+
console.log(` š” Found API call: ${apiClass}.${apiMethod}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Extract entity from API class or method
|
|
505
|
+
const entityPatterns = [
|
|
506
|
+
/(\w+)API/i, // UserAPI -> user
|
|
507
|
+
/fetch(\w+)/i, // fetchUser -> user
|
|
508
|
+
/get(\w+)/i // getUser -> user
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
for (const pattern of entityPatterns) {
|
|
512
|
+
let match = apiClass.match(pattern);
|
|
513
|
+
if (match) {
|
|
514
|
+
const entity = match[1].toLowerCase();
|
|
515
|
+
console.log(` šÆ Extracted flow from API class: ${entity}`);
|
|
516
|
+
return entity;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
match = apiMethod.match(pattern);
|
|
520
|
+
if (match) {
|
|
521
|
+
const entity = match[1].toLowerCase();
|
|
522
|
+
if (this.verbose) {
|
|
523
|
+
console.log(` šÆ Extracted flow from API method: ${entity}`);
|
|
524
|
+
}
|
|
525
|
+
return entity;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Fallback to original method
|
|
531
|
+
return this.extractApiFlow(functionName, filePath);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
createRetryPatternWithFlow(functionName, filePath, retryType, lineNumber, description, apiFlow) {
|
|
535
|
+
const layer = this.determineLayer(filePath, functionName);
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
ruleId: this.ruleId,
|
|
539
|
+
functionName,
|
|
540
|
+
filePath,
|
|
541
|
+
layer,
|
|
542
|
+
apiFlow,
|
|
543
|
+
retryType,
|
|
544
|
+
lineNumber,
|
|
545
|
+
description,
|
|
546
|
+
severity: 'warning'
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async analyzeFunction(func, functionName, filePath) {
|
|
551
|
+
const patterns = [];
|
|
552
|
+
|
|
553
|
+
if (!functionName) return patterns;
|
|
554
|
+
|
|
555
|
+
// Step 1: Detect Retry Pattern 1 - retry via exception
|
|
556
|
+
const exceptionRetryPattern = this.detectExceptionRetryPattern(func, functionName, filePath);
|
|
557
|
+
if (exceptionRetryPattern) {
|
|
558
|
+
if (this.verbose) {
|
|
559
|
+
console.log(` ā
Found exception retry pattern: ${exceptionRetryPattern.description}`);
|
|
560
|
+
}
|
|
561
|
+
patterns.push(exceptionRetryPattern);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Step 2: Detect Retry Pattern 2 - retry via empty data
|
|
565
|
+
const emptyDataRetryPattern = this.detectEmptyDataRetryPattern(func, functionName, filePath);
|
|
566
|
+
if (emptyDataRetryPattern) {
|
|
567
|
+
if (this.verbose) {
|
|
568
|
+
console.log(` ā
Found empty data retry pattern: ${emptyDataRetryPattern.description}`);
|
|
569
|
+
}
|
|
570
|
+
patterns.push(emptyDataRetryPattern);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Step 3: Detect Retry Pattern 3 - retry via while/for loops
|
|
574
|
+
const loopRetryPattern = this.detectLoopRetryPattern(func, functionName, filePath);
|
|
575
|
+
if (loopRetryPattern) {
|
|
576
|
+
if (this.verbose) {
|
|
577
|
+
console.log(` ā
Found loop retry pattern: ${loopRetryPattern.description}`);
|
|
578
|
+
}
|
|
579
|
+
patterns.push(loopRetryPattern);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Step 4: Check for calls to known retry functions
|
|
583
|
+
const knownRetryUsage = this.detectKnownRetryFunctionUsage(func, functionName, filePath);
|
|
584
|
+
if (knownRetryUsage && knownRetryUsage.length > 0) {
|
|
585
|
+
if (this.verbose) {
|
|
586
|
+
console.log(`[DEBUG] ā
Found known retry function usage: ${knownRetryUsage.length} patterns`);
|
|
587
|
+
}
|
|
588
|
+
patterns.push(...knownRetryUsage);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return patterns;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
detectExceptionRetryPattern(func, functionName, filePath) {
|
|
595
|
+
try {
|
|
596
|
+
const tryStatements = func.getDescendantsOfKind(require('ts-morph').SyntaxKind.TryStatement);
|
|
597
|
+
|
|
598
|
+
for (const tryStmt of tryStatements) {
|
|
599
|
+
const catchClause = tryStmt.getCatchClause();
|
|
600
|
+
if (!catchClause) continue;
|
|
601
|
+
|
|
602
|
+
const catchBlock = catchClause.getBlock();
|
|
603
|
+
|
|
604
|
+
// Look for retry calls in catch block
|
|
605
|
+
const callExpressions = catchBlock.getDescendantsOfKind(require('ts-morph').SyntaxKind.CallExpression);
|
|
606
|
+
|
|
607
|
+
for (const call of callExpressions) {
|
|
608
|
+
const callText = call.getExpression().getText();
|
|
609
|
+
|
|
610
|
+
// Pattern 1: Self-retry (recursive call)
|
|
611
|
+
if (callText === functionName || callText === `this.${functionName}`) {
|
|
612
|
+
return this.createRetryPattern(
|
|
613
|
+
functionName, filePath, 'exception_self_retry',
|
|
614
|
+
func.getStartLineNumber(), 'try-catch with self-call'
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Pattern 2: Direct API retry
|
|
619
|
+
if (this.isApiCall(callText)) {
|
|
620
|
+
return this.createRetryPattern(
|
|
621
|
+
functionName, filePath, 'exception_api_retry',
|
|
622
|
+
func.getStartLineNumber(), 'try-catch with API re-call'
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return null;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.warn(`ā ļø Error detecting exception retry in ${functionName}:`, error.message);
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
detectEmptyDataRetryPattern(func, functionName, filePath) {
|
|
636
|
+
try {
|
|
637
|
+
const ifStatements = func.getDescendantsOfKind(require('ts-morph').SyntaxKind.IfStatement);
|
|
638
|
+
|
|
639
|
+
for (const ifStmt of ifStatements) {
|
|
640
|
+
const condition = ifStmt.getExpression().getText();
|
|
641
|
+
|
|
642
|
+
// Look for empty data conditions
|
|
643
|
+
if (this.isEmptyDataCondition(condition)) {
|
|
644
|
+
const thenStatement = ifStmt.getThenStatement();
|
|
645
|
+
const callExpressions = thenStatement.getDescendantsOfKind(require('ts-morph').SyntaxKind.CallExpression);
|
|
646
|
+
|
|
647
|
+
for (const call of callExpressions) {
|
|
648
|
+
const callText = call.getExpression().getText();
|
|
649
|
+
|
|
650
|
+
if (this.isApiCall(callText) || callText === functionName) {
|
|
651
|
+
return this.createRetryPattern(
|
|
652
|
+
functionName, filePath, 'empty_data_retry',
|
|
653
|
+
func.getStartLineNumber(), 'retry on empty data'
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return null;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.warn(`ā ļø Error detecting empty data retry in ${functionName}:`, error.message);
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
detectLoopRetryPattern(func, functionName, filePath) {
|
|
668
|
+
try {
|
|
669
|
+
// Look for while loops with retry logic
|
|
670
|
+
const whileStatements = func.getDescendantsOfKind(require('ts-morph').SyntaxKind.WhileStatement);
|
|
671
|
+
const forStatements = func.getDescendantsOfKind(require('ts-morph').SyntaxKind.ForStatement);
|
|
672
|
+
|
|
673
|
+
const allLoops = [...whileStatements, ...forStatements];
|
|
674
|
+
|
|
675
|
+
if (this.verbose) {
|
|
676
|
+
console.log(`[DEBUG] š Found ${allLoops.length} loops in ${functionName}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (const loop of allLoops) {
|
|
680
|
+
const loopText = loop.getText().toLowerCase();
|
|
681
|
+
if (this.verbose) {
|
|
682
|
+
console.log(` š Loop text preview: ${loopText.substring(0, 100)}...`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Check if loop contains retry-related variables/keywords
|
|
686
|
+
const hasRetryIndicators = this.retryIndicators.variables.some(indicator =>
|
|
687
|
+
loopText.includes(indicator.toLowerCase())
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (hasRetryIndicators) {
|
|
691
|
+
if (this.verbose) {
|
|
692
|
+
console.log(` ā
Loop contains retry indicators`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Look for API calls within the loop
|
|
696
|
+
const callExpressions = loop.getDescendantsOfKind(require('ts-morph').SyntaxKind.CallExpression);
|
|
697
|
+
const hasApiCalls = callExpressions.some(call => this.isApiCall(call.getExpression().getText()));
|
|
698
|
+
|
|
699
|
+
if (hasApiCalls) {
|
|
700
|
+
if (this.verbose) {
|
|
701
|
+
console.log(` ā
Loop contains API calls - RETRY PATTERN DETECTED`);
|
|
702
|
+
}
|
|
703
|
+
return this.createRetryPattern(
|
|
704
|
+
functionName, filePath, 'loop_retry',
|
|
705
|
+
func.getStartLineNumber(), 'while/for loop with retry logic'
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return null;
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.warn(`ā ļø Error detecting loop retry in ${functionName}:`, error.message);
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
detectKnownRetryFunctionUsage(func, functionName, filePath) {
|
|
719
|
+
try {
|
|
720
|
+
const patterns = [];
|
|
721
|
+
const callExpressions = func.getDescendantsOfKind(require('ts-morph').SyntaxKind.CallExpression);
|
|
722
|
+
|
|
723
|
+
for (const call of callExpressions) {
|
|
724
|
+
const callText = call.getExpression().getText();
|
|
725
|
+
|
|
726
|
+
// Check if it matches a known retry function
|
|
727
|
+
const matchedRetryFunction = this.knownRetryFunctions.find(retryFunc =>
|
|
728
|
+
callText.includes(retryFunc) || callText === retryFunc
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
if (matchedRetryFunction) {
|
|
732
|
+
patterns.push(this.createRetryPattern(
|
|
733
|
+
functionName, filePath, 'uses_active_retry_function',
|
|
734
|
+
func.getStartLineNumber(), `calls ${matchedRetryFunction}`
|
|
735
|
+
));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return patterns;
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.warn(`ā ļø Error detecting known retry function usage in ${functionName}:`, error.message);
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
getFunctionName(func) {
|
|
747
|
+
try {
|
|
748
|
+
return func.getName() || 'anonymous';
|
|
749
|
+
} catch (error) {
|
|
750
|
+
return 'anonymous';
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
isTypeScriptFile(filePath) {
|
|
755
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
756
|
+
return ['.ts', '.tsx', '.js', '.jsx'].includes(ext);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
isApiCall(callText) {
|
|
760
|
+
const apiPatterns = [
|
|
761
|
+
'fetch', 'axios', 'api', 'client', 'service', 'request', 'get', 'post', 'put', 'delete'
|
|
762
|
+
];
|
|
763
|
+
return apiPatterns.some(pattern => callText.toLowerCase().includes(pattern));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
isEmptyDataCondition(condition) {
|
|
767
|
+
const emptyPatterns = ['!data', '!result', 'data === null', 'result === null', 'length === 0'];
|
|
768
|
+
return emptyPatterns.some(pattern => condition.includes(pattern));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
createRetryPattern(functionName, filePath, retryType, lineNumber, description) {
|
|
772
|
+
const layer = this.determineLayer(filePath, functionName);
|
|
773
|
+
const apiFlow = this.extractApiFlow(functionName, filePath);
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
ruleId: this.ruleId,
|
|
777
|
+
functionName,
|
|
778
|
+
filePath,
|
|
779
|
+
layer,
|
|
780
|
+
apiFlow,
|
|
781
|
+
retryType,
|
|
782
|
+
lineNumber,
|
|
783
|
+
description,
|
|
784
|
+
severity: 'warning'
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
determineLayer(filePath, functionName) {
|
|
789
|
+
const lowerPath = filePath.toLowerCase();
|
|
790
|
+
const lowerFunction = functionName.toLowerCase();
|
|
791
|
+
|
|
792
|
+
// Check file path patterns first
|
|
793
|
+
for (const [layer, patterns] of Object.entries(this.layerPatterns)) {
|
|
794
|
+
if (patterns.some(pattern => lowerPath.includes(pattern))) {
|
|
795
|
+
return layer;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Check function name patterns
|
|
800
|
+
if (lowerFunction.includes('component') || lowerFunction.includes('view') || lowerFunction.includes('page')) {
|
|
801
|
+
return 'ui';
|
|
802
|
+
}
|
|
803
|
+
if (lowerFunction.includes('usecase') || lowerFunction.includes('service')) {
|
|
804
|
+
return 'usecase';
|
|
805
|
+
}
|
|
806
|
+
if (lowerFunction.includes('repository') || lowerFunction.includes('repo')) {
|
|
807
|
+
return 'repository';
|
|
808
|
+
}
|
|
809
|
+
if (lowerFunction.includes('api') || lowerFunction.includes('client')) {
|
|
810
|
+
return 'api';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return 'unknown';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
extractApiFlow(functionName, filePath) {
|
|
817
|
+
// Extract flow name from function or file
|
|
818
|
+
const functionParts = functionName.split('.');
|
|
819
|
+
const baseName = functionParts[functionParts.length - 1];
|
|
820
|
+
|
|
821
|
+
// Look for common patterns like getUser, fetchData, etc.
|
|
822
|
+
const patterns = [
|
|
823
|
+
/get(\w+)/i,
|
|
824
|
+
/fetch(\w+)/i,
|
|
825
|
+
/load(\w+)/i,
|
|
826
|
+
/retrieve(\w+)/i,
|
|
827
|
+
/(\w+)api/i,
|
|
828
|
+
/(\w+)service/i,
|
|
829
|
+
/(\w+)component/i,
|
|
830
|
+
/(\w+)hook/i
|
|
831
|
+
];
|
|
832
|
+
|
|
833
|
+
for (const pattern of patterns) {
|
|
834
|
+
const match = baseName.match(pattern);
|
|
835
|
+
if (match) {
|
|
836
|
+
return match[1].toLowerCase();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Check for common entity names in function/file
|
|
841
|
+
const commonEntities = ['user', 'profile', 'auth', 'order', 'product', 'customer'];
|
|
842
|
+
const lowerName = baseName.toLowerCase();
|
|
843
|
+
|
|
844
|
+
for (const entity of commonEntities) {
|
|
845
|
+
if (lowerName.includes(entity)) {
|
|
846
|
+
return entity;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// For UI components calling APIs, try to extract entity from API calls
|
|
851
|
+
// This helps group UserComponent and ProfileComponent that both call UserAPI
|
|
852
|
+
if (functionName.toLowerCase().includes('component')) {
|
|
853
|
+
// Try to extract from common API patterns
|
|
854
|
+
const apiPatterns = ['userapi', 'profileapi', 'authapi'];
|
|
855
|
+
for (const apiPattern of apiPatterns) {
|
|
856
|
+
if (filePath.toLowerCase().includes(apiPattern) || functionName.toLowerCase().includes('user')) {
|
|
857
|
+
// If it's a user-related component, group it under 'user' flow
|
|
858
|
+
return 'user';
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// For files with "violation" or "test", try to extract entity from content/context
|
|
864
|
+
if (filePath.includes('violation') || filePath.includes('test')) {
|
|
865
|
+
// If it's a test file, check for user/profile/etc in the path or name
|
|
866
|
+
for (const entity of commonEntities) {
|
|
867
|
+
if (filePath.toLowerCase().includes(entity)) {
|
|
868
|
+
return entity;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// For useQuery violation samples, both UserComponent and ProfileComponent
|
|
872
|
+
// call UserAPI, so they should be grouped under 'user' flow
|
|
873
|
+
if (filePath.includes('usequery')) {
|
|
874
|
+
return 'user';
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Fallback to using file name
|
|
879
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
880
|
+
return fileName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
groupByLayersAndFlows(allPatterns) {
|
|
884
|
+
const layeredPatterns = new Map();
|
|
885
|
+
|
|
886
|
+
for (const pattern of allPatterns) {
|
|
887
|
+
const key = `${pattern.apiFlow}_${pattern.layer}`;
|
|
888
|
+
|
|
889
|
+
if (!layeredPatterns.has(pattern.apiFlow)) {
|
|
890
|
+
layeredPatterns.set(pattern.apiFlow, new Map());
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const flowMap = layeredPatterns.get(pattern.apiFlow);
|
|
894
|
+
if (!flowMap.has(pattern.layer)) {
|
|
895
|
+
flowMap.set(pattern.layer, []);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
flowMap.get(pattern.layer).push(pattern);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return layeredPatterns;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
detectViolations(layeredPatterns) {
|
|
905
|
+
const violations = [];
|
|
906
|
+
|
|
907
|
+
for (const [flow, layerMap] of layeredPatterns) {
|
|
908
|
+
const layers = Array.from(layerMap.keys());
|
|
909
|
+
|
|
910
|
+
if (layers.length > 1) {
|
|
911
|
+
// Multi-layer retry detected!
|
|
912
|
+
const allPatternsInFlow = [];
|
|
913
|
+
for (const patterns of layerMap.values()) {
|
|
914
|
+
allPatternsInFlow.push(...patterns);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Get the first pattern's file for the violation location
|
|
918
|
+
const primaryPattern = allPatternsInFlow[0];
|
|
919
|
+
const violationFile = primaryPattern ? primaryPattern.filePath : 'unknown';
|
|
920
|
+
const violationLine = primaryPattern ? primaryPattern.line : 1;
|
|
921
|
+
|
|
922
|
+
violations.push({
|
|
923
|
+
ruleId: this.ruleId,
|
|
924
|
+
file: violationFile,
|
|
925
|
+
line: violationLine,
|
|
926
|
+
column: 1,
|
|
927
|
+
message: `Multiple layers have retry logic for the same flow "${flow}": ${layers.join(', ')}`,
|
|
928
|
+
severity: 'error',
|
|
929
|
+
flow,
|
|
930
|
+
layers,
|
|
931
|
+
patterns: allPatternsInFlow,
|
|
932
|
+
violationType: 'duplicate_retry_across_layers',
|
|
933
|
+
type: 'duplicate_retry_across_layers'
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return violations;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
printAnalysisStats(allPatterns, layeredPatterns, violations) {
|
|
942
|
+
console.log(`\nš Symbol Analysis Statistics:`);
|
|
943
|
+
console.log(` š Total retry patterns found: ${allPatterns.length}`);
|
|
944
|
+
console.log(` š API flows analyzed: ${layeredPatterns.size}`);
|
|
945
|
+
console.log(` ā ļø Violations found: ${violations.length}`);
|
|
946
|
+
|
|
947
|
+
if (allPatterns.length > 0) {
|
|
948
|
+
console.log(`\nš Pattern breakdown:`);
|
|
949
|
+
const patternsByType = {};
|
|
950
|
+
for (const pattern of allPatterns) {
|
|
951
|
+
patternsByType[pattern.retryType] = (patternsByType[pattern.retryType] || 0) + 1;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
for (const [type, count] of Object.entries(patternsByType)) {
|
|
955
|
+
console.log(` ${type}: ${count}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (violations.length > 0) {
|
|
960
|
+
console.log(`\nšØ Violations summary:`);
|
|
961
|
+
for (const violation of violations) {
|
|
962
|
+
console.log(` "${violation.flow}": ${violation.layers.join(' + ')}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
module.exports = C047SymbolAnalyzerEnhanced;
|