@sun-asterisk/sunlint 1.3.5 → 1.3.6
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 +30 -0
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +23 -0
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +78 -44
- package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
- package/rules/common/C070_no_real_time_tests/config.json +78 -0
- package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 🔧 **v1.3.6 - C067 False Positive Reduction (September 8, 2025)**
|
|
6
|
+
|
|
7
|
+
**Release Date**: September 8, 2025
|
|
8
|
+
**Type**: Bug Fix & Improvement
|
|
9
|
+
|
|
10
|
+
### 🐛 **Bug Fixes**
|
|
11
|
+
- **FIXED**: C067 "no hardcoded config" rule - Massive false positive reduction
|
|
12
|
+
- **replace-fe**: From 296 → 2 violations (-99.3%)
|
|
13
|
+
- **replace-be**: From 171 → 3 violations (-98.2%)
|
|
14
|
+
- **jmb-app-be**: From 121 → 5 violations (-95.9%)
|
|
15
|
+
- **mdx-cycle-hack**: From 8 → 6 violations (-25%)
|
|
16
|
+
|
|
17
|
+
### 🔧 **Technical Improvements**
|
|
18
|
+
- **ENHANCED**: C067 analyzer logic improvements
|
|
19
|
+
- Skip dummy/test files and entity files completely
|
|
20
|
+
- Exclude field mapping objects and ORM configurations
|
|
21
|
+
- Skip database constraint names (primaryKeyConstraintName, etc.)
|
|
22
|
+
- Focus only on truly environment-dependent configurations
|
|
23
|
+
- Exclude business logic constants and UI field mappings
|
|
24
|
+
- **IMPROVED**: Rule precision - Only flag real environment config issues
|
|
25
|
+
- API endpoints, AWS service URLs, application keys
|
|
26
|
+
- Credential values and connection strings
|
|
27
|
+
- Environment-dependent timeouts and ports
|
|
28
|
+
|
|
29
|
+
### 📊 **Performance**
|
|
30
|
+
- **OPTIMIZED**: Reduced analysis noise by 95%+ on large projects
|
|
31
|
+
- **ENHANCED**: Better developer experience with fewer false alarms
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
5
35
|
## 🔧 **v1.3.5 - Preset System Refactor (September 8, 2025)**
|
|
6
36
|
|
|
7
37
|
**Release Date**: September 8, 2025
|
|
@@ -61,6 +61,11 @@ module.exports = {
|
|
|
61
61
|
methods: ['regex'],
|
|
62
62
|
accuracy: { regex: 90 }
|
|
63
63
|
},
|
|
64
|
+
'C070': {
|
|
65
|
+
reason: 'Real-time dependencies detection via timer/sleep patterns',
|
|
66
|
+
methods: ['regex'],
|
|
67
|
+
accuracy: { regex: 95 }
|
|
68
|
+
},
|
|
64
69
|
'S001': {
|
|
65
70
|
reason: 'Security patterns are often string-based',
|
|
66
71
|
methods: ['regex', 'ast'],
|
|
@@ -1357,6 +1357,29 @@
|
|
|
1357
1357
|
"heuristic": ["rules/common/C067_no_hardcoded_config/analyzer.js"]
|
|
1358
1358
|
}
|
|
1359
1359
|
},
|
|
1360
|
+
"C070": {
|
|
1361
|
+
"name": "No Real Time Tests",
|
|
1362
|
+
"description": "Tests should not depend on real time delays or sleeps. Use fake timers, clock injection, or condition-based waits to improve test reliability and speed.",
|
|
1363
|
+
"category": "testing",
|
|
1364
|
+
"severity": "error",
|
|
1365
|
+
"languages": ["typescript", "javascript"],
|
|
1366
|
+
"analyzer": "../rules/common/C070_no_real_time_tests/regex-analyzer.js",
|
|
1367
|
+
"config": "../rules/common/C070_no_real_time_tests/config.json",
|
|
1368
|
+
"version": "1.0.0",
|
|
1369
|
+
"status": "stable",
|
|
1370
|
+
"tags": ["testing", "flaky-tests", "timing", "fake-timers", "reliability"],
|
|
1371
|
+
"strategy": {
|
|
1372
|
+
"preferred": "ast",
|
|
1373
|
+
"fallbacks": ["regex"],
|
|
1374
|
+
"accuracy": {
|
|
1375
|
+
"ast": 95,
|
|
1376
|
+
"regex": 88
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
"engineMappings": {
|
|
1380
|
+
"heuristic": ["../rules/common/C070_no_real_time_tests/regex-analyzer.js"]
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1360
1383
|
"C072": {
|
|
1361
1384
|
"id": "C072",
|
|
1362
1385
|
"name": "Single Test Behavior",
|
package/package.json
CHANGED
|
@@ -246,7 +246,7 @@ class C067SymbolBasedAnalyzer {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
isConfigOrTestFile(filePath) {
|
|
249
|
-
// Skip config files themselves and test files
|
|
249
|
+
// Skip config files themselves and test files, including dummy/test data files
|
|
250
250
|
const fileName = filePath.toLowerCase();
|
|
251
251
|
const configPatterns = [
|
|
252
252
|
/config\.(ts|js|json)$/,
|
|
@@ -264,9 +264,12 @@ class C067SymbolBasedAnalyzer {
|
|
|
264
264
|
/\/test\//,
|
|
265
265
|
/\/tests\//,
|
|
266
266
|
/\.stories\.(ts|tsx|js|jsx)$/,
|
|
267
|
-
/\.mock\.(ts|tsx|js|jsx)
|
|
268
|
-
//
|
|
269
|
-
//
|
|
267
|
+
/\.mock\.(ts|tsx|js|jsx)$/,
|
|
268
|
+
/\/dummy\//, // Skip dummy data files
|
|
269
|
+
/dummy\.(ts|js)$/, // Skip dummy files
|
|
270
|
+
/test-fixtures\//, // Skip test fixture files
|
|
271
|
+
/\.fixture\.(ts|js)$/, // Skip fixture files
|
|
272
|
+
/entity\.(ts|js)$/ // Skip entity/ORM files (contain DB constraints)
|
|
270
273
|
];
|
|
271
274
|
|
|
272
275
|
return configPatterns.some(pattern => pattern.test(fileName)) ||
|
|
@@ -451,66 +454,97 @@ class C067SymbolBasedAnalyzer {
|
|
|
451
454
|
const propertyName = nameNode.getText();
|
|
452
455
|
const position = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
453
456
|
|
|
454
|
-
// Skip field mapping objects
|
|
457
|
+
// Skip ALL field mapping objects and ORM/database entity configurations
|
|
455
458
|
const ancestorObj = node.getParent();
|
|
456
459
|
if (ancestorObj && Node.isObjectLiteralExpression(ancestorObj)) {
|
|
457
460
|
const objParent = ancestorObj.getParent();
|
|
458
461
|
if (objParent && Node.isVariableDeclaration(objParent)) {
|
|
459
462
|
const varName = objParent.getName();
|
|
460
|
-
|
|
461
|
-
|
|
463
|
+
// Skip field mappings, database schemas, etc.
|
|
464
|
+
if (/mapping|map|field|column|decode|schema|entity|constraint|table/i.test(varName)) {
|
|
465
|
+
return null;
|
|
462
466
|
}
|
|
463
467
|
}
|
|
468
|
+
|
|
469
|
+
// Check if this looks like a table column definition or field mapping
|
|
470
|
+
const objText = ancestorObj.getText();
|
|
471
|
+
if (/primaryKeyConstraintName|foreignKeyConstraintName|key.*may contain/i.test(objText)) {
|
|
472
|
+
return null; // Skip database constraint definitions
|
|
473
|
+
}
|
|
464
474
|
}
|
|
465
475
|
|
|
466
|
-
// Skip
|
|
476
|
+
// Skip properties that are clearly field mappings or business data
|
|
467
477
|
const businessLogicProperties = [
|
|
468
|
-
|
|
469
|
-
'
|
|
470
|
-
|
|
471
|
-
'
|
|
478
|
+
// Field mappings
|
|
479
|
+
'key', 'field', 'dataKey', 'valueKey', 'labelKey', 'sortKey',
|
|
480
|
+
// Business logic
|
|
481
|
+
'endpoint', 'path', 'route', 'method',
|
|
482
|
+
'limit', 'pageSize', 'batchSize', 'maxResults',
|
|
483
|
+
'retry', 'retries', 'maxRetries', 'attempts',
|
|
484
|
+
'count', 'max', 'min', 'size', 'length',
|
|
485
|
+
// UI properties
|
|
486
|
+
'className', 'style', 'disabled', 'readonly',
|
|
487
|
+
// Database/ORM
|
|
488
|
+
'primaryKeyConstraintName', 'foreignKeyConstraintName', 'constraintName',
|
|
489
|
+
'tableName', 'columnName', 'schemaName'
|
|
472
490
|
];
|
|
473
491
|
|
|
474
492
|
const lowerPropertyName = propertyName.toLowerCase();
|
|
475
493
|
if (businessLogicProperties.some(prop => lowerPropertyName.includes(prop))) {
|
|
476
|
-
|
|
477
|
-
let value = null;
|
|
478
|
-
if (valueNode.getKind() === SyntaxKind.StringLiteral) {
|
|
479
|
-
value = valueNode.getLiteralValue();
|
|
480
|
-
// Only flag URLs or clearly environment-dependent strings
|
|
481
|
-
if (!this.configPatterns.urls.regex.test(value) || !this.isEnvironmentDependentUrl(value)) {
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
} else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
|
|
485
|
-
value = valueNode.getLiteralValue();
|
|
486
|
-
const parentContext = this.getParentContext(node);
|
|
487
|
-
// Only flag if it's clearly environment-dependent (like ports, large timeouts)
|
|
488
|
-
if (!this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
494
|
+
return null; // Skip these completely
|
|
492
495
|
}
|
|
493
496
|
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
+
// Only check for CLEARLY environment-dependent properties
|
|
498
|
+
const trulyEnvironmentDependentProps = [
|
|
499
|
+
'baseurl', 'baseURL', 'host', 'hostname', 'server', 'endpoint',
|
|
500
|
+
'apikey', 'api_key', 'secret_key', 'client_secret',
|
|
501
|
+
'database', 'connectionstring', 'dbhost', 'dbport',
|
|
502
|
+
'port', 'timeout', // Only when they have suspicious values
|
|
503
|
+
'bucket', 'region', // Cloud-specific
|
|
504
|
+
'clientid', 'tenantid' // OAuth-specific
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
if (!trulyEnvironmentDependentProps.some(prop => lowerPropertyName.includes(prop))) {
|
|
508
|
+
return null; // Not clearly environment-dependent
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let value = null;
|
|
512
|
+
let configType = null;
|
|
513
|
+
|
|
514
|
+
if (valueNode.getKind() === SyntaxKind.StringLiteral) {
|
|
515
|
+
value = valueNode.getLiteralValue();
|
|
497
516
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
517
|
+
// Only flag URLs or clearly sensitive values
|
|
518
|
+
if (this.configPatterns.urls.regex.test(value) && this.isEnvironmentDependentUrl(value)) {
|
|
519
|
+
configType = 'url';
|
|
520
|
+
} else if (this.isRealCredential(value, propertyName)) {
|
|
521
|
+
configType = 'credential';
|
|
522
|
+
} else {
|
|
523
|
+
return null; // Skip other string values
|
|
502
524
|
}
|
|
525
|
+
} else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
|
|
526
|
+
value = valueNode.getLiteralValue();
|
|
527
|
+
const parentContext = this.getParentContext(node);
|
|
503
528
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
column: position.column,
|
|
510
|
-
node: node,
|
|
511
|
-
propertyName: propertyName
|
|
512
|
-
};
|
|
529
|
+
// Only flag numbers that are clearly environment-dependent
|
|
530
|
+
if (this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
|
|
531
|
+
configType = 'environment_config';
|
|
532
|
+
} else {
|
|
533
|
+
return null;
|
|
513
534
|
}
|
|
535
|
+
} else {
|
|
536
|
+
return null; // Skip other value types
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (configType) {
|
|
540
|
+
return {
|
|
541
|
+
type: configType,
|
|
542
|
+
value: value,
|
|
543
|
+
line: position.line,
|
|
544
|
+
column: position.column,
|
|
545
|
+
node: node,
|
|
546
|
+
propertyName: propertyName
|
|
547
|
+
};
|
|
514
548
|
}
|
|
515
549
|
|
|
516
550
|
return null;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* C070 - Tests Should Not Depend on Real Time
|
|
6
|
+
* Detects real-time sleeps/timeouts in test files and suggests fake timers
|
|
7
|
+
*
|
|
8
|
+
* Focus: Improve test reliability by avoiding time-dependent flaky tests
|
|
9
|
+
*/
|
|
10
|
+
class C070TestRealTimeAnalyzer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ruleId = 'C070';
|
|
13
|
+
this.configPath = path.join(__dirname, 'config.json');
|
|
14
|
+
this.config = this.loadConfig();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
loadConfig() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn(`Failed to load config for ${this.ruleId}:`, error.message);
|
|
22
|
+
return this.getDefaultConfig();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getDefaultConfig() {
|
|
27
|
+
return {
|
|
28
|
+
options: {
|
|
29
|
+
timerApis: {
|
|
30
|
+
ts_js: [
|
|
31
|
+
"setTimeout\\s*\\(",
|
|
32
|
+
"setInterval\\s*\\(",
|
|
33
|
+
"\\.sleep\\s*\\(",
|
|
34
|
+
"\\.delay\\s*\\(",
|
|
35
|
+
"\\.wait\\s*\\(",
|
|
36
|
+
"new\\s+Promise.*setTimeout"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
fakeTimerDetectors: {
|
|
40
|
+
jest_vitest: [
|
|
41
|
+
"jest\\.useFakeTimers\\(\\)",
|
|
42
|
+
"vi\\.useFakeTimers\\(\\)",
|
|
43
|
+
"jest\\.advanceTimersByTime",
|
|
44
|
+
"vi\\.advanceTimersByTime"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
busyPollingDetectors: {
|
|
48
|
+
ts_js: ["Date\\.now\\(\\)", "new\\s+Date\\(\\)"]
|
|
49
|
+
},
|
|
50
|
+
allowAnnotations: ["@perf", "@benchmark", "@allow-real-time", "// @allow-real-time"]
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if file is a test file
|
|
57
|
+
*/
|
|
58
|
+
isTestFile(filePath) {
|
|
59
|
+
const testPatterns = [
|
|
60
|
+
/\.test\.(js|ts|jsx|tsx)$/,
|
|
61
|
+
/\.spec\.(js|ts|jsx|tsx)$/,
|
|
62
|
+
/__tests__\//,
|
|
63
|
+
/\/tests?\//,
|
|
64
|
+
/test-cases\.(js|ts)$/ // Add pattern for our test cases
|
|
65
|
+
];
|
|
66
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if line has annotation allowing real-time
|
|
71
|
+
*/
|
|
72
|
+
hasAllowAnnotation(content, lineIndex) {
|
|
73
|
+
const allowAnnotations = this.config.options.allowAnnotations || [];
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
|
|
76
|
+
// Check current line and 2 lines above for annotations
|
|
77
|
+
for (let i = Math.max(0, lineIndex - 2); i <= lineIndex; i++) {
|
|
78
|
+
const line = lines[i] || '';
|
|
79
|
+
for (const annotation of allowAnnotations) {
|
|
80
|
+
if (line.includes(annotation)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if fake timers are used in the file
|
|
90
|
+
*/
|
|
91
|
+
hasFakeTimers(content) {
|
|
92
|
+
const fakeTimerPatterns = this.config.options.fakeTimerDetectors.jest_vitest || [];
|
|
93
|
+
return fakeTimerPatterns.some(pattern => {
|
|
94
|
+
const regex = new RegExp(pattern, 'g');
|
|
95
|
+
return regex.test(content);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Detect timer API violations
|
|
101
|
+
*/
|
|
102
|
+
detectTimerViolations(content, filePath) {
|
|
103
|
+
const violations = [];
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
const timerPatterns = this.config.options.timerApis.ts_js || this.getDefaultConfig().options.timerApis.ts_js;
|
|
106
|
+
const hasFakeTimersInFile = this.hasFakeTimers(content);
|
|
107
|
+
|
|
108
|
+
timerPatterns.forEach(pattern => {
|
|
109
|
+
// Convert config patterns (* wildcards) to proper regex
|
|
110
|
+
let regexPattern = pattern;
|
|
111
|
+
if (pattern.includes('(*)')) {
|
|
112
|
+
regexPattern = pattern.replace(/\(\*\)/g, '\\([^)]*\\)');
|
|
113
|
+
}
|
|
114
|
+
if (pattern.includes('*')) {
|
|
115
|
+
regexPattern = pattern.replace(/\*/g, '[^)]*');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const regex = new RegExp(regexPattern, 'g');
|
|
119
|
+
|
|
120
|
+
lines.forEach((line, index) => {
|
|
121
|
+
const trimmedLine = line.trim();
|
|
122
|
+
|
|
123
|
+
// Skip comments and empty lines
|
|
124
|
+
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || !trimmedLine) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip if has allow annotation
|
|
129
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const matches = [...line.matchAll(regex)];
|
|
134
|
+
if (matches.length > 0) {
|
|
135
|
+
matches.forEach(match => {
|
|
136
|
+
const column = match.index + 1;
|
|
137
|
+
|
|
138
|
+
let suggestion = "Use fake timers instead of real-time delays in tests.";
|
|
139
|
+
let severity = "error";
|
|
140
|
+
|
|
141
|
+
// Specific suggestions based on pattern
|
|
142
|
+
if (pattern.includes('setTimeout') || pattern.includes('setInterval')) {
|
|
143
|
+
if (!hasFakeTimersInFile) {
|
|
144
|
+
suggestion = "Use jest.useFakeTimers() and jest.advanceTimersByTime() instead of setTimeout/setInterval.";
|
|
145
|
+
} else {
|
|
146
|
+
suggestion = "You have fake timers setup. Use jest.advanceTimersByTime() to control time instead of real setTimeout.";
|
|
147
|
+
}
|
|
148
|
+
} else if (pattern.includes('sleep') || pattern.includes('delay')) {
|
|
149
|
+
suggestion = "Replace sleep/delay with fake timers or condition-based waiting.";
|
|
150
|
+
} else if (pattern.includes('Promise.*setTimeout')) {
|
|
151
|
+
suggestion = "Replace Promise+setTimeout with fake timers: await jest.advanceTimersByTimeAsync().";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
violations.push({
|
|
155
|
+
line: index + 1,
|
|
156
|
+
column: column,
|
|
157
|
+
message: `Avoid real-time ${match[0]} in tests. ${suggestion}`,
|
|
158
|
+
severity: severity,
|
|
159
|
+
ruleId: this.ruleId,
|
|
160
|
+
evidence: line.trim(),
|
|
161
|
+
suggestion: suggestion
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return violations;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Detect busy polling violations (Date.now(), new Date() in loops)
|
|
173
|
+
*/
|
|
174
|
+
detectBusyPollingViolations(content, filePath) {
|
|
175
|
+
const violations = [];
|
|
176
|
+
const lines = content.split('\n');
|
|
177
|
+
const pollingPatterns = this.config.options.busyPollingDetectors.ts_js || [];
|
|
178
|
+
|
|
179
|
+
pollingPatterns.forEach(pattern => {
|
|
180
|
+
const regex = new RegExp(pattern, 'g');
|
|
181
|
+
|
|
182
|
+
lines.forEach((line, index) => {
|
|
183
|
+
const trimmedLine = line.trim();
|
|
184
|
+
|
|
185
|
+
// Skip comments
|
|
186
|
+
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || !trimmedLine) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Skip if has allow annotation
|
|
191
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Look for Date.now()/new Date() in potential polling contexts
|
|
196
|
+
const matches = line.match(regex);
|
|
197
|
+
if (matches && this.isLikelyPolling(lines, index)) {
|
|
198
|
+
matches.forEach(match => {
|
|
199
|
+
const column = line.indexOf(match) + 1;
|
|
200
|
+
|
|
201
|
+
violations.push({
|
|
202
|
+
line: index + 1,
|
|
203
|
+
column: column,
|
|
204
|
+
message: `Avoid busy polling with ${match} in tests. Use condition-based waiting instead.`,
|
|
205
|
+
severity: "warning",
|
|
206
|
+
ruleId: this.ruleId,
|
|
207
|
+
evidence: line.trim(),
|
|
208
|
+
suggestion: "Use waitFor conditions or fake timers instead of polling Date.now()."
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return violations;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if Date.now()/new Date() usage looks like polling
|
|
220
|
+
*/
|
|
221
|
+
isLikelyPolling(lines, currentIndex) {
|
|
222
|
+
// Look for while/for loops, or repeated checks around this line
|
|
223
|
+
const contextRange = 5;
|
|
224
|
+
const start = Math.max(0, currentIndex - contextRange);
|
|
225
|
+
const end = Math.min(lines.length - 1, currentIndex + contextRange);
|
|
226
|
+
|
|
227
|
+
for (let i = start; i <= end; i++) {
|
|
228
|
+
const line = lines[i].trim();
|
|
229
|
+
if (line.includes('while') || line.includes('for') ||
|
|
230
|
+
line.includes('setInterval') || line.includes('setTimeout')) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Detect E2E specific violations
|
|
240
|
+
*/
|
|
241
|
+
detectE2EViolations(content, filePath) {
|
|
242
|
+
const violations = [];
|
|
243
|
+
const lines = content.split('\n');
|
|
244
|
+
const e2ePatterns = this.config.options.timerApis.e2e || [];
|
|
245
|
+
|
|
246
|
+
e2ePatterns.forEach(pattern => {
|
|
247
|
+
const regex = new RegExp(pattern, 'g');
|
|
248
|
+
|
|
249
|
+
lines.forEach((line, index) => {
|
|
250
|
+
const trimmedLine = line.trim();
|
|
251
|
+
|
|
252
|
+
if (trimmedLine.startsWith('//') || !trimmedLine) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const matches = line.match(regex);
|
|
261
|
+
if (matches) {
|
|
262
|
+
matches.forEach(match => {
|
|
263
|
+
const column = line.indexOf(match) + 1;
|
|
264
|
+
|
|
265
|
+
let suggestion = "Use element-based waiting instead of fixed timeouts.";
|
|
266
|
+
if (match.includes('page.waitForTimeout')) {
|
|
267
|
+
suggestion = "Use page.waitForSelector() or page.waitForFunction() instead of waitForTimeout().";
|
|
268
|
+
} else if (match.includes('cy.wait')) {
|
|
269
|
+
suggestion = "Use cy.get().should() or cy.intercept() instead of cy.wait() with fixed time.";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
violations.push({
|
|
273
|
+
line: index + 1,
|
|
274
|
+
column: column,
|
|
275
|
+
message: `Avoid fixed timeout ${match} in E2E tests. ${suggestion}`,
|
|
276
|
+
severity: "warning",
|
|
277
|
+
ruleId: this.ruleId,
|
|
278
|
+
evidence: line.trim(),
|
|
279
|
+
suggestion: suggestion
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return violations;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Main analysis function
|
|
291
|
+
*/
|
|
292
|
+
analyze(content, filePath) {
|
|
293
|
+
// Only analyze test files
|
|
294
|
+
if (!this.isTestFile(filePath)) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let violations = [];
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Detect timer API violations
|
|
302
|
+
violations = violations.concat(this.detectTimerViolations(content, filePath));
|
|
303
|
+
|
|
304
|
+
// Detect busy polling violations
|
|
305
|
+
violations = violations.concat(this.detectBusyPollingViolations(content, filePath));
|
|
306
|
+
|
|
307
|
+
// Detect E2E violations (if file looks like E2E test)
|
|
308
|
+
if (filePath.includes('e2e') || content.includes('playwright') || content.includes('cypress')) {
|
|
309
|
+
violations = violations.concat(this.detectE2EViolations(content, filePath));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.warn(`C070 analysis error for ${filePath}:`, error.message);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return violations;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = C070TestRealTimeAnalyzer;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "C070",
|
|
3
|
+
"name": "C070_tests_should_not_depend_on_real_time",
|
|
4
|
+
"category": "testing",
|
|
5
|
+
"description": "C070 - Avoid real-time sleeps/timeouts in tests. Use fake timers, clock injection, or condition/event-based waits.",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"priority": "high",
|
|
11
|
+
"fallback": "heuristic"
|
|
12
|
+
},
|
|
13
|
+
"patterns": {
|
|
14
|
+
"include": ["**/*.test.*", "**/*.spec.*", "**/__tests__/**"],
|
|
15
|
+
"exclude": ["**/performance/**", "**/benchmarks/**", "**/e2e/**"]
|
|
16
|
+
},
|
|
17
|
+
"options": {
|
|
18
|
+
"timerApis": {
|
|
19
|
+
"ts_js": [
|
|
20
|
+
"setTimeout\\s*\\(",
|
|
21
|
+
"setInterval\\s*\\(",
|
|
22
|
+
"\\.sleep\\s*\\(",
|
|
23
|
+
"\\.delay\\s*\\(",
|
|
24
|
+
"\\.wait\\s*\\(",
|
|
25
|
+
"new\\s+Promise.*setTimeout"
|
|
26
|
+
],
|
|
27
|
+
"java": [
|
|
28
|
+
"Thread.sleep(*)",
|
|
29
|
+
"TimeUnit.*.sleep(*)"
|
|
30
|
+
],
|
|
31
|
+
"go": [
|
|
32
|
+
"time.Sleep(*)"
|
|
33
|
+
],
|
|
34
|
+
"e2e": [
|
|
35
|
+
"page\\.waitForTimeout\\s*\\(",
|
|
36
|
+
"cy\\.wait\\s*\\(\\s*[0-9]+"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"fakeTimerDetectors": {
|
|
40
|
+
"jest_vitest": [
|
|
41
|
+
"jest\\.useFakeTimers\\(\\)", "vi\\.useFakeTimers\\(\\)",
|
|
42
|
+
"jest\\.advanceTimersByTime\\s*\\(", "vi\\.advanceTimersByTime\\s*\\(",
|
|
43
|
+
"jest\\.runAllTimers\\(\\)", "vi\\.runAllTimers\\(\\)"
|
|
44
|
+
],
|
|
45
|
+
"rxjs": ["new TestScheduler(*)"]
|
|
46
|
+
},
|
|
47
|
+
"busyPollingDetectors": {
|
|
48
|
+
"ts_js": ["Date.now()", "new Date()"],
|
|
49
|
+
"java": ["System.currentTimeMillis()", "Instant.now()"],
|
|
50
|
+
"go": ["time.Now()"]
|
|
51
|
+
},
|
|
52
|
+
"policy": {
|
|
53
|
+
"forbidRealSleeps": true,
|
|
54
|
+
"requireFakeTimersWhenUsingTimers": true,
|
|
55
|
+
"preferConditionBasedWaits": true,
|
|
56
|
+
"allowE2EExplicitWaits": false,
|
|
57
|
+
"allowAnnotatedPerfTests": true
|
|
58
|
+
},
|
|
59
|
+
"allowAnnotations": [
|
|
60
|
+
"@perf", "@benchmark", "@allow-real-time", "// @allow-real-time"
|
|
61
|
+
],
|
|
62
|
+
"suggestions": {
|
|
63
|
+
"ts_js": [
|
|
64
|
+
"Use jest/vi fake timers and advanceTimersByTime instead of sleep/timeout.",
|
|
65
|
+
"Await condition or event (locator.waitFor / cy.get(...).should(...)).",
|
|
66
|
+
"Inject a Clock or pass now(): number into the unit under test."
|
|
67
|
+
],
|
|
68
|
+
"java": [
|
|
69
|
+
"Inject java.time.Clock; use Clock.fixed/Clock.offset in tests.",
|
|
70
|
+
"Use Awaitility untilAsserted instead of Thread.sleep."
|
|
71
|
+
],
|
|
72
|
+
"go": [
|
|
73
|
+
"Use benbjohnson/clock mock; advance time in tests.",
|
|
74
|
+
"Avoid time.Sleep; wait on channels/conditions/events instead."
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { CommentDetector } = require('../../utils/rule-helpers');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* C070 - Tests Should Not Depend on Real Time
|
|
7
|
+
* Detects real-time sleeps/timeouts in test files and suggests fake timers
|
|
8
|
+
*
|
|
9
|
+
* Focus: Improve test reliability by avoiding time-dependent flaky tests
|
|
10
|
+
*/
|
|
11
|
+
class C070TestRealTimeAnalyzer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.ruleId = 'C070';
|
|
14
|
+
this.configPath = path.join(__dirname, 'config.json');
|
|
15
|
+
this.config = this.loadConfig();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadConfig() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.warn(`Failed to load config for ${this.ruleId}:`, error.message);
|
|
23
|
+
return this.getDefaultConfig();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getDefaultConfig() {
|
|
28
|
+
return {
|
|
29
|
+
options: {
|
|
30
|
+
timerApis: {
|
|
31
|
+
ts_js: [
|
|
32
|
+
"setTimeout\\s*\\(",
|
|
33
|
+
"setInterval\\s*\\(",
|
|
34
|
+
"\\.sleep\\s*\\(",
|
|
35
|
+
"\\.delay\\s*\\(",
|
|
36
|
+
"\\.wait\\s*\\(",
|
|
37
|
+
"new\\s+Promise.*setTimeout"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
fakeTimerDetectors: {
|
|
41
|
+
jest_vitest: [
|
|
42
|
+
"jest\\.useFakeTimers\\(\\)",
|
|
43
|
+
"vi\\.useFakeTimers\\(\\)",
|
|
44
|
+
"jest\\.advanceTimersByTime",
|
|
45
|
+
"vi\\.advanceTimersByTime"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
busyPollingDetectors: {
|
|
49
|
+
ts_js: ["Date\\.now\\(\\)", "new\\s+Date\\(\\)"]
|
|
50
|
+
},
|
|
51
|
+
allowAnnotations: ["@perf", "@benchmark", "@allow-real-time", "// @allow-real-time"]
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if file is a test file
|
|
58
|
+
*/
|
|
59
|
+
isTestFile(filePath) {
|
|
60
|
+
const testPatterns = [
|
|
61
|
+
/\.test\.(js|ts|jsx|tsx)$/,
|
|
62
|
+
/\.spec\.(js|ts|jsx|tsx)$/,
|
|
63
|
+
/__tests__\//,
|
|
64
|
+
/\/tests?\//,
|
|
65
|
+
/test-cases\.(js|ts)$/ // Add pattern for our test cases
|
|
66
|
+
];
|
|
67
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if line has annotation allowing real-time
|
|
72
|
+
*/
|
|
73
|
+
hasAllowAnnotation(content, lineIndex) {
|
|
74
|
+
const allowAnnotations = this.config.options.allowAnnotations || [];
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
|
|
77
|
+
// Check current line and 2 lines above for annotations
|
|
78
|
+
for (let i = Math.max(0, lineIndex - 2); i <= lineIndex; i++) {
|
|
79
|
+
const line = lines[i] || '';
|
|
80
|
+
for (const annotation of allowAnnotations) {
|
|
81
|
+
if (line.includes(annotation)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if fake timers are used in the file
|
|
91
|
+
*/
|
|
92
|
+
hasFakeTimers(content) {
|
|
93
|
+
const fakeTimerPatterns = this.config.options.fakeTimerDetectors.jest_vitest || [];
|
|
94
|
+
return fakeTimerPatterns.some(pattern => {
|
|
95
|
+
const regex = new RegExp(pattern, 'g');
|
|
96
|
+
return regex.test(content);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detect timer API violations
|
|
102
|
+
*/
|
|
103
|
+
detectTimerViolations(content, filePath) {
|
|
104
|
+
const violations = [];
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
const timerPatterns = this.config.options.timerApis.ts_js || this.getDefaultConfig().options.timerApis.ts_js;
|
|
107
|
+
const hasFakeTimersInFile = this.hasFakeTimers(content);
|
|
108
|
+
|
|
109
|
+
timerPatterns.forEach(pattern => {
|
|
110
|
+
// Convert config patterns (* wildcards) to proper regex
|
|
111
|
+
let regexPattern = pattern;
|
|
112
|
+
if (pattern.includes('(*)')) {
|
|
113
|
+
regexPattern = pattern.replace(/\(\*\)/g, '\\([^)]*\\)');
|
|
114
|
+
}
|
|
115
|
+
if (pattern.includes('*')) {
|
|
116
|
+
regexPattern = pattern.replace(/\*/g, '[^)]*');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const regex = new RegExp(regexPattern, 'g');
|
|
120
|
+
|
|
121
|
+
lines.forEach((line, index) => {
|
|
122
|
+
const trimmedLine = line.trim();
|
|
123
|
+
|
|
124
|
+
// Skip empty lines
|
|
125
|
+
if (!trimmedLine) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip if entire line is in block comment
|
|
130
|
+
if (CommentDetector.isLineInBlockComment(lines, index)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Skip if has allow annotation
|
|
135
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const matches = [...line.matchAll(regex)];
|
|
140
|
+
if (matches.length > 0) {
|
|
141
|
+
matches.forEach(match => {
|
|
142
|
+
const column = match.index + 1;
|
|
143
|
+
|
|
144
|
+
// Skip if match position is inside a comment
|
|
145
|
+
if (CommentDetector.isPositionInComment(line, match.index)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let suggestion = "Use fake timers instead of real-time delays in tests.";
|
|
150
|
+
let severity = "error";
|
|
151
|
+
|
|
152
|
+
// Specific suggestions based on pattern
|
|
153
|
+
if (pattern.includes('setTimeout') || pattern.includes('setInterval')) {
|
|
154
|
+
if (!hasFakeTimersInFile) {
|
|
155
|
+
suggestion = "Use jest.useFakeTimers() and jest.advanceTimersByTime() instead of setTimeout/setInterval.";
|
|
156
|
+
} else {
|
|
157
|
+
suggestion = "You have fake timers setup. Use jest.advanceTimersByTime() to control time instead of real setTimeout.";
|
|
158
|
+
}
|
|
159
|
+
} else if (pattern.includes('sleep') || pattern.includes('delay')) {
|
|
160
|
+
suggestion = "Replace sleep/delay with fake timers or condition-based waiting.";
|
|
161
|
+
} else if (pattern.includes('Promise.*setTimeout')) {
|
|
162
|
+
suggestion = "Replace Promise+setTimeout with fake timers: await jest.advanceTimersByTimeAsync().";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
violations.push({
|
|
166
|
+
file: filePath,
|
|
167
|
+
line: index + 1,
|
|
168
|
+
column: column,
|
|
169
|
+
message: `Avoid real-time ${match[0]} in tests. ${suggestion}`,
|
|
170
|
+
severity: severity,
|
|
171
|
+
ruleId: this.ruleId,
|
|
172
|
+
evidence: line.trim(),
|
|
173
|
+
suggestion: suggestion
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return violations;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Detect busy polling violations (Date.now(), new Date() in loops)
|
|
185
|
+
*/
|
|
186
|
+
detectBusyPollingViolations(content, filePath) {
|
|
187
|
+
const violations = [];
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
const pollingPatterns = this.config.options.busyPollingDetectors.ts_js || [];
|
|
190
|
+
|
|
191
|
+
pollingPatterns.forEach(pattern => {
|
|
192
|
+
const regex = new RegExp(pattern, 'g');
|
|
193
|
+
|
|
194
|
+
lines.forEach((line, index) => {
|
|
195
|
+
const trimmedLine = line.trim();
|
|
196
|
+
|
|
197
|
+
// Skip empty lines
|
|
198
|
+
if (!trimmedLine) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Skip if entire line is in block comment
|
|
203
|
+
if (CommentDetector.isLineInBlockComment(lines, index)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Skip if has allow annotation
|
|
208
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Look for Date.now()/new Date() in potential polling contexts
|
|
213
|
+
const matches = line.match(regex);
|
|
214
|
+
if (matches && this.isLikelyPolling(lines, index)) {
|
|
215
|
+
matches.forEach(match => {
|
|
216
|
+
const column = line.indexOf(match) + 1;
|
|
217
|
+
|
|
218
|
+
// Skip if match position is inside a comment
|
|
219
|
+
if (CommentDetector.isPositionInComment(line, line.indexOf(match))) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
violations.push({
|
|
224
|
+
file: filePath,
|
|
225
|
+
line: index + 1,
|
|
226
|
+
column: column,
|
|
227
|
+
message: `Avoid busy polling with ${match} in tests. Use condition-based waiting instead.`,
|
|
228
|
+
severity: "warning",
|
|
229
|
+
ruleId: this.ruleId,
|
|
230
|
+
evidence: line.trim(),
|
|
231
|
+
suggestion: "Use waitFor conditions or fake timers instead of polling Date.now()."
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return violations;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if Date.now()/new Date() usage looks like polling
|
|
243
|
+
*/
|
|
244
|
+
isLikelyPolling(lines, currentIndex) {
|
|
245
|
+
const currentLine = lines[currentIndex];
|
|
246
|
+
|
|
247
|
+
// Skip if new Date() has static parameters (test data)
|
|
248
|
+
if (currentLine.includes('new Date(') && /new Date\(\s*\d/.test(currentLine)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Look for polling patterns around this line
|
|
253
|
+
const contextRange = 5;
|
|
254
|
+
const start = Math.max(0, currentIndex - contextRange);
|
|
255
|
+
const end = Math.min(lines.length - 1, currentIndex + contextRange);
|
|
256
|
+
|
|
257
|
+
let hasLoop = false;
|
|
258
|
+
let hasTimeCheck = false;
|
|
259
|
+
|
|
260
|
+
for (let i = start; i <= end; i++) {
|
|
261
|
+
const line = lines[i].trim().toLowerCase();
|
|
262
|
+
|
|
263
|
+
// Check for loop patterns
|
|
264
|
+
if (line.includes('while') && (line.includes('date.now') || line.includes('new date'))) {
|
|
265
|
+
hasLoop = true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check for time-based conditions
|
|
269
|
+
if ((line.includes('date.now') || line.includes('new date')) &&
|
|
270
|
+
(line.includes(' - ') || line.includes(' < ') || line.includes(' > ')) &&
|
|
271
|
+
(line.includes('start') || line.includes('time') || line.includes('duration'))) {
|
|
272
|
+
hasTimeCheck = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for explicit polling patterns
|
|
276
|
+
if (line.includes('setinterval') && (line.includes('date.now') || line.includes('new date'))) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Only flag as polling if both loop and time check are present
|
|
282
|
+
return hasLoop && hasTimeCheck;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Detect E2E specific violations
|
|
287
|
+
*/
|
|
288
|
+
detectE2EViolations(content, filePath) {
|
|
289
|
+
const violations = [];
|
|
290
|
+
const lines = content.split('\n');
|
|
291
|
+
const e2ePatterns = this.config.options.timerApis.e2e || [];
|
|
292
|
+
|
|
293
|
+
e2ePatterns.forEach(pattern => {
|
|
294
|
+
const regex = new RegExp(pattern, 'g');
|
|
295
|
+
|
|
296
|
+
lines.forEach((line, index) => {
|
|
297
|
+
const trimmedLine = line.trim();
|
|
298
|
+
|
|
299
|
+
if (!trimmedLine) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Skip if entire line is in block comment
|
|
304
|
+
if (CommentDetector.isLineInBlockComment(lines, index)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const matches = line.match(regex);
|
|
313
|
+
if (matches) {
|
|
314
|
+
matches.forEach(match => {
|
|
315
|
+
const column = line.indexOf(match) + 1;
|
|
316
|
+
|
|
317
|
+
// Skip if match position is inside a comment
|
|
318
|
+
if (CommentDetector.isPositionInComment(line, line.indexOf(match))) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let suggestion = "Use element-based waiting instead of fixed timeouts.";
|
|
323
|
+
if (match.includes('page.waitForTimeout')) {
|
|
324
|
+
suggestion = "Use page.waitForSelector() or page.waitForFunction() instead of waitForTimeout().";
|
|
325
|
+
} else if (match.includes('cy.wait')) {
|
|
326
|
+
suggestion = "Use cy.get().should() or cy.intercept() instead of cy.wait() with fixed time.";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
violations.push({
|
|
330
|
+
file: filePath,
|
|
331
|
+
line: index + 1,
|
|
332
|
+
column: column,
|
|
333
|
+
message: `Avoid fixed timeout ${match} in E2E tests. ${suggestion}`,
|
|
334
|
+
severity: "warning",
|
|
335
|
+
ruleId: this.ruleId,
|
|
336
|
+
evidence: line.trim(),
|
|
337
|
+
suggestion: suggestion
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return violations;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Main analysis function - Engine interface
|
|
349
|
+
* Expected signature: analyze(files, language, options)
|
|
350
|
+
*/
|
|
351
|
+
async analyze(files, language, options = {}) {
|
|
352
|
+
const allViolations = [];
|
|
353
|
+
|
|
354
|
+
for (const filePath of files) {
|
|
355
|
+
try {
|
|
356
|
+
// Only analyze test files
|
|
357
|
+
if (!this.isTestFile(filePath)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Read file content
|
|
362
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
363
|
+
|
|
364
|
+
// Skip if file has explicit allow annotation
|
|
365
|
+
if (this.hasAllowAnnotation(content)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let violations = [];
|
|
370
|
+
|
|
371
|
+
// Detect timer API violations
|
|
372
|
+
violations = violations.concat(this.detectTimerViolations(content, filePath));
|
|
373
|
+
|
|
374
|
+
// Detect busy polling violations
|
|
375
|
+
violations = violations.concat(this.detectBusyPollingViolations(content, filePath));
|
|
376
|
+
|
|
377
|
+
// Detect E2E violations (if file looks like E2E test)
|
|
378
|
+
if (filePath.includes('e2e') || content.includes('playwright') || content.includes('cypress')) {
|
|
379
|
+
violations = violations.concat(this.detectE2EViolations(content, filePath));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
allViolations.push(...violations);
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn(`C070 analysis error for ${filePath}:`, error.message);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return allViolations;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Legacy analysis function for single file
|
|
394
|
+
* @deprecated Use analyze(files, language, options) instead
|
|
395
|
+
*/
|
|
396
|
+
analyzeSingleFile(content, filePath) {
|
|
397
|
+
// Only analyze test files
|
|
398
|
+
if (!this.isTestFile(filePath)) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let violations = [];
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// Detect timer API violations
|
|
406
|
+
violations = violations.concat(this.detectTimerViolations(content, filePath));
|
|
407
|
+
|
|
408
|
+
// Detect busy polling violations
|
|
409
|
+
violations = violations.concat(this.detectBusyPollingViolations(content, filePath));
|
|
410
|
+
|
|
411
|
+
// Detect E2E violations (if file looks like E2E test)
|
|
412
|
+
if (filePath.includes('e2e') || content.includes('playwright') || content.includes('cypress')) {
|
|
413
|
+
violations = violations.concat(this.detectE2EViolations(content, filePath));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.warn(`C070 analysis error for ${filePath}:`, error.message);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return violations;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = C070TestRealTimeAnalyzer;
|