@sun-asterisk/sunlint 1.3.5 → 1.3.7

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.
@@ -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;
@@ -0,0 +1,110 @@
1
+ # C073 - Validate Required Configuration on Startup
2
+
3
+ ## Mục tiêu
4
+ Tránh lỗi runtime không rõ nguyên nhân do thiếu hoặc sai cấu hình. Đảm bảo ứng dụng dừng sớm (fail fast) nếu thiếu cấu hình quan trọng.
5
+
6
+ ## Mô tả
7
+ Rule này kiểm tra việc validate cấu hình bắt buộc ngay khi ứng dụng khởi động và yêu cầu hệ thống fail fast nếu có vấn đề với cấu hình.
8
+
9
+ ## Các điều kiện kiểm tra
10
+
11
+ ### 1. Schema Validation hoặc Explicit Checks
12
+ - Tất cả cấu hình bắt buộc phải được validate bằng schema libraries (zod, joi, yup, etc.) hoặc explicit checks
13
+ - Không được truy cập `process.env` mà không có validation
14
+
15
+ ### 2. Fail Fast Behavior
16
+ - Khi phát hiện cấu hình thiếu hoặc không hợp lệ, ứng dụng phải dừng ngay lập tức
17
+ - Sử dụng `process.exit(1)`, `throw Error`, hoặc tương tự
18
+
19
+ ### 3. Centralized Configuration
20
+ - Hạn chế việc truy cập environment variables rải rác trong code
21
+ - Tập trung xử lý cấu hình trong các module chuyên dụng
22
+
23
+ ### 4. No Dangerous Defaults
24
+ - Không sử dụng default values nguy hiểm như empty string, localhost URLs
25
+ - Flag các pattern như `|| ''`, `|| 0`, `|| 'http://localhost'`
26
+
27
+ ### 5. Startup Connectivity Checks
28
+ - Với database hoặc external services, cần có connectivity check khi startup
29
+ - Test kết nối trước khi application bắt đầu phục vụ requests
30
+
31
+ ## Ví dụ
32
+
33
+ ### ❌ Không đúng
34
+ ```typescript
35
+ // Không có validation, dangerous defaults
36
+ const config = {
37
+ apiKey: process.env.API_KEY || 'default-key',
38
+ dbUrl: process.env.DATABASE_URL || '',
39
+ port: process.env.PORT || 3000
40
+ };
41
+
42
+ function startServer() {
43
+ const server = createServer();
44
+ server.listen(config.port); // Có thể fail runtime nếu config sai
45
+ }
46
+ ```
47
+
48
+ ### ✅ Đúng
49
+ ```typescript
50
+ import { z } from 'zod';
51
+
52
+ const configSchema = z.object({
53
+ API_KEY: z.string().min(1, 'API_KEY is required'),
54
+ DATABASE_URL: z.string().url('DATABASE_URL must be valid URL'),
55
+ PORT: z.string().transform(val => parseInt(val, 10))
56
+ });
57
+
58
+ function validateConfig() {
59
+ try {
60
+ return configSchema.parse(process.env);
61
+ } catch (error) {
62
+ console.error('Configuration validation failed:', error.message);
63
+ process.exit(1); // Fail fast
64
+ }
65
+ }
66
+
67
+ export const config = validateConfig();
68
+
69
+ async function checkDatabaseConnection() {
70
+ try {
71
+ const connection = await connectToDatabase(config.DATABASE_URL);
72
+ await connection.ping();
73
+ } catch (error) {
74
+ console.error('Database connection failed:', error.message);
75
+ process.exit(1); // Fail fast on connectivity issues
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Cấu hình
81
+
82
+ ### Schema Validation Libraries
83
+ - **TypeScript/JavaScript**: zod, joi, yup, envalid, dotenv-safe, class-validator
84
+ - **Java**: @ConfigurationProperties, @Validated, jakarta.validation, hibernate.validator
85
+ - **Go**: envconfig, viper
86
+
87
+ ### Fail Fast Mechanisms
88
+ - **TypeScript/JavaScript**: `throw new Error()`, `process.exit(1)`
89
+ - **Java**: `throw new RuntimeException()`, `SpringApplication.exit()`, `System.exit(1)`
90
+ - **Go**: `log.Fatal()`, `panic()`, `os.Exit(1)`
91
+
92
+ ### Policy Options
93
+ - `requireSchemaOrExplicitChecks`: Bắt buộc có validation
94
+ - `requireFailFast`: Bắt buộc có fail fast mechanism
95
+ - `forbidEnvReadsOutsideConfig`: Hạn chế env access ngoài config modules
96
+ - `flagDangerousDefaults`: Cảnh báo về dangerous default values
97
+ - `requireStartupConnectivityChecks`: Yêu cầu connectivity check
98
+
99
+ ## Mức độ nghiêm trọng
100
+ **Error** - Rule này có thể ngăn chặn các lỗi runtime nghiêm trọng và khó debug.
101
+
102
+ ## Ngôn ngữ hỗ trợ
103
+ - TypeScript/JavaScript
104
+ - Java
105
+ - Go
106
+
107
+ ## Tham khảo
108
+ - [Fail Fast Principle](https://en.wikipedia.org/wiki/Fail-fast)
109
+ - [Configuration Validation Best Practices](https://12factor.net/config)
110
+ - [Schema Validation Libraries](https://github.com/colinhacks/zod)