@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.
- package/CHANGELOG.md +67 -0
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +65 -10
- package/core/analysis-orchestrator.js +9 -5
- package/core/performance-optimizer.js +8 -2
- 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/rules/common/C073_validate_required_config_on_startup/README.md +110 -0
- package/rules/common/C073_validate_required_config_on_startup/analyzer.js +770 -0
- package/rules/common/C073_validate_required_config_on_startup/config.json +46 -0
- package/rules/common/C073_validate_required_config_on_startup/symbol-based-analyzer.js +370 -0
- package/rules/security/S057_utc_logging/README.md +152 -0
- package/rules/security/S057_utc_logging/analyzer.js +457 -0
- package/rules/security/S057_utc_logging/config.json +105 -0
- package/rules/security/S058_no_ssrf/README.md +180 -0
- package/rules/security/S058_no_ssrf/analyzer.js +403 -0
- package/rules/security/S058_no_ssrf/config.json +125 -0
|
@@ -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)
|