@sun-asterisk/sunlint 1.3.4 → 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 +62 -0
- package/config/presets/all.json +49 -48
- package/config/presets/beginner.json +7 -18
- package/config/presets/ci.json +63 -27
- package/config/presets/maintainability.json +6 -4
- package/config/presets/performance.json +4 -3
- package/config/presets/quality.json +11 -50
- package/config/presets/recommended.json +83 -10
- package/config/presets/security.json +20 -19
- package/config/presets/strict.json +6 -13
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +87 -7
- package/core/config-preset-resolver.js +7 -2
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
- package/rules/common/C067_no_hardcoded_config/config.json +81 -0
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
- 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/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
- package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
- package/rules/security/S025_server_side_validation/README.md +179 -0
- package/rules/security/S025_server_side_validation/analyzer.js +242 -0
- package/rules/security/S025_server_side_validation/config.json +111 -0
- package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
- package/scripts/README.md +83 -0
- package/scripts/analyze-core-rules.js +151 -0
- package/scripts/generate-presets.js +202 -0
|
@@ -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
|
+
}
|