@sun-asterisk/sunlint 1.3.16 → 1.3.17
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/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S055
|
|
3
|
+
* REST services must:
|
|
4
|
+
* 1. Check the Content-Type header in incoming requests.
|
|
5
|
+
* 2. Accept only supported types, such as:
|
|
6
|
+
* application/json
|
|
7
|
+
* application/xml (only if required)
|
|
8
|
+
* 3. Reject unsupported or unexpected types, e.g.:
|
|
9
|
+
* text/plain
|
|
10
|
+
* multipart/form-data (unless explicitly required)
|
|
11
|
+
* 4. Log all rejected requests for security and debugging.
|
|
12
|
+
* 5. Avoid blindly trusting framework parsing (e.g., using body-parser or @Body() decorators without validation).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { SyntaxKind } = require('ts-morph');
|
|
16
|
+
|
|
17
|
+
class S055SymbolBasedAnalyzer {
|
|
18
|
+
constructor(semanticEngine = null) {
|
|
19
|
+
this.ruleId = "S055";
|
|
20
|
+
this.ruleName = 'Validate input Content-Type in REST services';
|
|
21
|
+
this.semanticEngine = semanticEngine;
|
|
22
|
+
this.verbose = false;
|
|
23
|
+
this.skipPatterns = [
|
|
24
|
+
/test\//, /tests\//, /__tests__\//, /\.test\./, /\.spec\./,
|
|
25
|
+
/node_modules\//, /build\//, /dist\//, /\.next\//, /coverage\//,
|
|
26
|
+
/vendor\//, /mocks\//, /\.mock\./,
|
|
27
|
+
/config\//, /configs\//, /\.config\./,
|
|
28
|
+
/public\//, /static\//, /assets\//,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Patterns to identify REST endpoints
|
|
32
|
+
this.restDecorators = ['@Post', '@Put', '@Patch', '@Get', '@Delete'];
|
|
33
|
+
this.expressPatterns = ['app.post', 'app.put', 'app.patch', 'router.post', 'router.put', 'router.patch'];
|
|
34
|
+
|
|
35
|
+
// Content-Type validation patterns
|
|
36
|
+
this.contentTypeChecks = [
|
|
37
|
+
'Content-Type', 'content-type', 'contentType',
|
|
38
|
+
'req.is(', 'request.is(',
|
|
39
|
+
'application/json', 'application/xml'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Patterns for valid exceptions (GOOD cases)
|
|
43
|
+
this.validExceptions = {
|
|
44
|
+
// File upload decorators/middleware
|
|
45
|
+
fileUpload: [
|
|
46
|
+
'@UseInterceptors(FileInterceptor',
|
|
47
|
+
'@UseInterceptors(FilesInterceptor',
|
|
48
|
+
'@UseInterceptors(FileFieldsInterceptor',
|
|
49
|
+
'@UseInterceptors(AnyFilesInterceptor',
|
|
50
|
+
'multer(',
|
|
51
|
+
'upload.single',
|
|
52
|
+
'upload.array',
|
|
53
|
+
'upload.fields',
|
|
54
|
+
'multipart/form-data'
|
|
55
|
+
],
|
|
56
|
+
// Custom interceptors for validation
|
|
57
|
+
customInterceptors: [
|
|
58
|
+
'@UseInterceptors(ContentTypeInterceptor',
|
|
59
|
+
'@UseInterceptors(ValidationInterceptor',
|
|
60
|
+
'@UseInterceptors(ContentTypeValidation',
|
|
61
|
+
'@UseGuards(ContentTypeGuard',
|
|
62
|
+
'ContentTypeValidator',
|
|
63
|
+
'validateContentType'
|
|
64
|
+
],
|
|
65
|
+
// Middleware patterns
|
|
66
|
+
middleware: [
|
|
67
|
+
'contentTypeMiddleware',
|
|
68
|
+
'validateContentType',
|
|
69
|
+
'checkContentType'
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async initialize(semanticEngine = null) {
|
|
75
|
+
if (semanticEngine) {
|
|
76
|
+
this.semanticEngine = semanticEngine;
|
|
77
|
+
}
|
|
78
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
79
|
+
|
|
80
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
81
|
+
console.log(`🔧 [S055 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
86
|
+
// This is the main entry point called by the hybrid analyzer
|
|
87
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
analyzeFileWithSymbols(filePath, options = {}) {
|
|
91
|
+
const violations = [];
|
|
92
|
+
|
|
93
|
+
// Enable verbose mode if requested
|
|
94
|
+
const verbose = options.verbose || this.verbose;
|
|
95
|
+
|
|
96
|
+
if (!this.semanticEngine?.project) {
|
|
97
|
+
if (verbose) {
|
|
98
|
+
console.warn('[S055 Symbol-Based] No semantic engine available, skipping analysis');
|
|
99
|
+
}
|
|
100
|
+
return violations;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
104
|
+
if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
|
|
105
|
+
return violations;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (verbose) {
|
|
109
|
+
console.log(`🔍 [S055 Symbol-Based] Starting analysis for ${filePath}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
114
|
+
if (!sourceFile) {
|
|
115
|
+
return violations;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Find all methods and functions
|
|
119
|
+
const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration);
|
|
120
|
+
const functions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration);
|
|
121
|
+
const arrowFunctions = sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction);
|
|
122
|
+
const functionExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression);
|
|
123
|
+
|
|
124
|
+
// Check all methods
|
|
125
|
+
methods.forEach(method => this.checkMethod(method, sourceFile, violations));
|
|
126
|
+
|
|
127
|
+
// Check all functions
|
|
128
|
+
[...functions, ...arrowFunctions, ...functionExpressions].forEach(func => {
|
|
129
|
+
this.checkFunction(func, sourceFile, violations);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Check Express-style route handlers
|
|
133
|
+
this.checkExpressRoutes(sourceFile, violations);
|
|
134
|
+
|
|
135
|
+
if (verbose) {
|
|
136
|
+
console.log(`🔍 [S055 Symbol-Based] Total violations found: ${violations.length}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return violations;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (verbose) {
|
|
142
|
+
console.warn(`[S055 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return violations;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
checkMethod(method, sourceFile, violations) {
|
|
150
|
+
// Check if it's a REST endpoint (NestJS, Spring-style decorators)
|
|
151
|
+
const decorators = method.getDecorators();
|
|
152
|
+
const isRestEndpoint = decorators.some(dec => {
|
|
153
|
+
const name = dec.getName();
|
|
154
|
+
return this.restDecorators.some(pattern => name.includes(pattern.substring(1)));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!isRestEndpoint) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// GOOD CASE: Check for file upload interceptors
|
|
162
|
+
if (this.hasFileUploadHandling(method, decorators)) {
|
|
163
|
+
if (this.verbose) {
|
|
164
|
+
console.log(`✅ [S055] Method ${method.getName()} has file upload handling - GOOD`);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// GOOD CASE: Check for custom content-type interceptors/guards
|
|
170
|
+
if (this.hasCustomContentTypeValidation(method, decorators)) {
|
|
171
|
+
if (this.verbose) {
|
|
172
|
+
console.log(`✅ [S055] Method ${method.getName()} has custom interceptor - GOOD`);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if POST, PUT, or PATCH (methods that accept body)
|
|
178
|
+
const hasBodyDecorator = decorators.some(dec => {
|
|
179
|
+
const name = dec.getName();
|
|
180
|
+
return ['Post', 'Put', 'Patch'].includes(name);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (hasBodyDecorator) {
|
|
184
|
+
this.checkContentTypeValidation(method, sourceFile, violations);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
checkFunction(func, sourceFile, violations) {
|
|
189
|
+
const parent = func.getParent();
|
|
190
|
+
|
|
191
|
+
// Check if it's a route handler (has req/request parameter)
|
|
192
|
+
const params = func.getParameters();
|
|
193
|
+
const hasRequestParam = params.some(p => {
|
|
194
|
+
const name = p.getName();
|
|
195
|
+
return name === 'req' || name === 'request';
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!hasRequestParam) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// GOOD CASE: Check for middleware usage
|
|
203
|
+
if (this.hasMiddlewareValidation(func, sourceFile)) {
|
|
204
|
+
if (this.verbose) {
|
|
205
|
+
console.log(`✅ [S055] Function has middleware validation - GOOD`);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.checkContentTypeValidation(func, sourceFile, violations);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
checkExpressRoutes(sourceFile, violations) {
|
|
214
|
+
// Find all call expressions like app.post(), router.put(), etc.
|
|
215
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
216
|
+
|
|
217
|
+
callExpressions.forEach(call => {
|
|
218
|
+
const expr = call.getExpression();
|
|
219
|
+
const text = expr.getText();
|
|
220
|
+
|
|
221
|
+
const isRestRoute = this.expressPatterns.some(pattern => text.includes(pattern));
|
|
222
|
+
|
|
223
|
+
if (!isRestRoute) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// GOOD CASE: Check for multer or file upload middleware
|
|
228
|
+
const args = call.getArguments();
|
|
229
|
+
if (this.hasFileUploadMiddleware(args)) {
|
|
230
|
+
if (this.verbose) {
|
|
231
|
+
console.log(`✅ [S055] Express route has file upload middleware - GOOD`);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// GOOD CASE: Check for content-type validation middleware
|
|
237
|
+
if (this.hasContentTypeMiddleware(args)) {
|
|
238
|
+
if (this.verbose) {
|
|
239
|
+
console.log(`✅ [S055] Express route has content-type middleware - GOOD`);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Get the handler function (usually the last argument)
|
|
245
|
+
const handler = args[args.length - 1];
|
|
246
|
+
|
|
247
|
+
if (handler) {
|
|
248
|
+
this.checkContentTypeValidation(handler, sourceFile, violations);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
hasFileUploadHandling(node, decorators) {
|
|
254
|
+
const nodeText = node.getText();
|
|
255
|
+
|
|
256
|
+
// Check decorators
|
|
257
|
+
const hasFileDecorator = decorators?.some(dec => {
|
|
258
|
+
const text = dec.getText();
|
|
259
|
+
return this.validExceptions.fileUpload.some(pattern => text.includes(pattern));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Check method body
|
|
263
|
+
const hasFileInBody = this.validExceptions.fileUpload.some(pattern =>
|
|
264
|
+
nodeText.includes(pattern)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return hasFileDecorator || hasFileInBody;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
hasCustomContentTypeValidation(node, decorators) {
|
|
271
|
+
const nodeText = node.getText();
|
|
272
|
+
|
|
273
|
+
// Check decorators
|
|
274
|
+
const hasInterceptor = decorators?.some(dec => {
|
|
275
|
+
const text = dec.getText();
|
|
276
|
+
return this.validExceptions.customInterceptors.some(pattern => text.includes(pattern));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Check method body
|
|
280
|
+
const hasValidatorInBody = this.validExceptions.customInterceptors.some(pattern =>
|
|
281
|
+
nodeText.includes(pattern)
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return hasInterceptor || hasValidatorInBody;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
hasMiddlewareValidation(func, sourceFile) {
|
|
288
|
+
// Look for middleware usage in the same file or parent scope
|
|
289
|
+
const parentScope = func.getParent();
|
|
290
|
+
const scopeText = parentScope?.getText() || '';
|
|
291
|
+
|
|
292
|
+
return this.validExceptions.middleware.some(pattern =>
|
|
293
|
+
scopeText.includes(pattern)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
hasFileUploadMiddleware(args) {
|
|
298
|
+
return args.some(arg => {
|
|
299
|
+
const text = arg.getText();
|
|
300
|
+
return this.validExceptions.fileUpload.some(pattern => text.includes(pattern));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
hasContentTypeMiddleware(args) {
|
|
305
|
+
// Check if any middleware argument validates content-type
|
|
306
|
+
return args.some(arg => {
|
|
307
|
+
const text = arg.getText();
|
|
308
|
+
return this.validExceptions.middleware.some(pattern => text.includes(pattern)) ||
|
|
309
|
+
this.validExceptions.customInterceptors.some(pattern => text.includes(pattern));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
checkContentTypeValidation(node, sourceFile, violations) {
|
|
314
|
+
const bodyText = node.getText();
|
|
315
|
+
|
|
316
|
+
// Check if Content-Type validation exists
|
|
317
|
+
const hasContentTypeCheck = this.contentTypeChecks.some(pattern =>
|
|
318
|
+
bodyText.includes(pattern)
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (!hasContentTypeCheck) {
|
|
322
|
+
const startLine = node.getStartLineNumber();
|
|
323
|
+
const name = node.getKind() === SyntaxKind.MethodDeclaration
|
|
324
|
+
? node.getName()
|
|
325
|
+
: 'anonymous function';
|
|
326
|
+
|
|
327
|
+
violations.push({
|
|
328
|
+
ruleId: this.ruleId,
|
|
329
|
+
ruleName: this.ruleName,
|
|
330
|
+
severity: 'medium',
|
|
331
|
+
message: `REST endpoint '${name}' does not validate Content-Type header. Add validation for 'application/json' or other expected types.`,
|
|
332
|
+
line: startLine,
|
|
333
|
+
column: node.getStart() - node.getStartLinePos() + 1,
|
|
334
|
+
filePath: sourceFile.getFilePath(),
|
|
335
|
+
type: 'missing_content_type_validation',
|
|
336
|
+
details: 'Consider adding Content-Type validation using req.is("application/json") or checking req.headers["content-type"] before processing request body.'
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
shouldIgnoreFile(filePath) {
|
|
342
|
+
return this.skipPatterns.some((pattern) => pattern.test(filePath));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = S055SymbolBasedAnalyzer;
|
|
@@ -3,48 +3,137 @@
|
|
|
3
3
|
* Tests for heuristic rule analyzer
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const C002_no_duplicate_codeAnalyzer = require('../common/C002_no_duplicate_code/analyzer');
|
|
7
9
|
|
|
8
10
|
describe('C002_no_duplicate_code Heuristic Rule', () => {
|
|
9
11
|
let analyzer;
|
|
12
|
+
const fixturesPath = path.join(__dirname, '../../examples/rule-test-fixtures/rules/C002_no_duplicate_code');
|
|
10
13
|
|
|
11
14
|
beforeEach(() => {
|
|
12
|
-
analyzer = new C002_no_duplicate_codeAnalyzer();
|
|
15
|
+
analyzer = new C002_no_duplicate_codeAnalyzer({ minLines: 10, similarityThreshold: 0.85 });
|
|
13
16
|
});
|
|
14
17
|
|
|
15
|
-
describe('
|
|
16
|
-
test('should
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
describe('Configuration', () => {
|
|
19
|
+
test('should have correct default configuration', () => {
|
|
20
|
+
const config = analyzer.getConfig();
|
|
21
|
+
expect(config.minLines).toBe(10);
|
|
22
|
+
expect(config.similarityThreshold).toBe(0.85);
|
|
23
|
+
expect(config.ignoreComments).toBe(true);
|
|
24
|
+
expect(config.ignoreWhitespace).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
describe('Valid Code - No Duplicates', () => {
|
|
29
|
+
test('should not report violations for clean code with no duplicates', () => {
|
|
30
|
+
const testFile = path.join(fixturesPath, 'clean/no-duplicates.ts');
|
|
31
|
+
if (fs.existsSync(testFile)) {
|
|
32
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
33
|
+
expect(violations).toHaveLength(0);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should not report violations for properly extracted utilities', () => {
|
|
38
|
+
const testFile = path.join(fixturesPath, 'clean/extracted-utilities.ts');
|
|
39
|
+
if (fs.existsSync(testFile)) {
|
|
40
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
41
|
+
expect(violations).toHaveLength(0);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should not report violations for inheritance pattern', () => {
|
|
46
|
+
const testFile = path.join(fixturesPath, 'clean/inheritance-pattern.ts');
|
|
47
|
+
if (fs.existsSync(testFile)) {
|
|
48
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
49
|
+
expect(violations).toHaveLength(0);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should not report violations for composition pattern', () => {
|
|
54
|
+
const testFile = path.join(fixturesPath, 'clean/composition-pattern.ts');
|
|
55
|
+
if (fs.existsSync(testFile)) {
|
|
56
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
57
|
+
expect(violations).toHaveLength(0);
|
|
58
|
+
}
|
|
23
59
|
});
|
|
24
60
|
});
|
|
25
61
|
|
|
26
|
-
describe('Invalid Code', () => {
|
|
27
|
-
test('should report violations for
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
62
|
+
describe('Invalid Code - With Duplicates', () => {
|
|
63
|
+
test('should report violations for duplicate validation logic', () => {
|
|
64
|
+
const testFile = path.join(fixturesPath, 'violations/duplicate-with-comments.ts');
|
|
65
|
+
if (fs.existsSync(testFile)) {
|
|
66
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
67
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
68
|
+
expect(violations[0].ruleId).toBe('C002');
|
|
69
|
+
expect(violations[0].message).toContain('Duplicate');
|
|
70
|
+
expect(violations[0].data.suggestions).toBeDefined();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
31
73
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
74
|
+
test('should report violations for duplicate repository logic', () => {
|
|
75
|
+
const testFile = path.join(fixturesPath, 'violations/duplicate-repository-logic.ts');
|
|
76
|
+
if (fs.existsSync(testFile)) {
|
|
77
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
78
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
79
|
+
expect(violations[0].message).toContain('Duplicate');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should report violations for duplicate error handling', () => {
|
|
84
|
+
const testFile = path.join(fixturesPath, 'violations/duplicate-error-handling.ts');
|
|
85
|
+
if (fs.existsSync(testFile)) {
|
|
86
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
87
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Suggestions', () => {
|
|
93
|
+
test('should provide context-aware suggestions', () => {
|
|
94
|
+
const testFile = path.join(fixturesPath, 'violations/duplicate-with-comments.ts');
|
|
95
|
+
if (fs.existsSync(testFile)) {
|
|
96
|
+
const violations = analyzer.analyze([testFile], 'typescript');
|
|
97
|
+
if (violations.length > 0) {
|
|
98
|
+
expect(violations[0].data.suggestions).toBeDefined();
|
|
99
|
+
expect(Array.isArray(violations[0].data.suggestions)).toBe(true);
|
|
100
|
+
expect(violations[0].data.suggestions.length).toBeGreaterThan(0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
35
103
|
});
|
|
36
104
|
});
|
|
37
105
|
|
|
38
106
|
describe('Edge Cases', () => {
|
|
39
|
-
test('should handle empty
|
|
40
|
-
const violations = analyzer.analyze(
|
|
107
|
+
test('should handle empty file list', () => {
|
|
108
|
+
const violations = analyzer.analyze([], 'typescript');
|
|
41
109
|
expect(violations).toHaveLength(0);
|
|
42
110
|
});
|
|
43
111
|
|
|
44
|
-
test('should handle
|
|
45
|
-
const
|
|
46
|
-
const violations = analyzer.analyze(code, 'test.js');
|
|
112
|
+
test('should handle non-existent files gracefully', () => {
|
|
113
|
+
const violations = analyzer.analyze(['/non/existent/file.ts'], 'typescript');
|
|
47
114
|
expect(Array.isArray(violations)).toBe(true);
|
|
48
115
|
});
|
|
116
|
+
|
|
117
|
+
test('should handle files with only comments', () => {
|
|
118
|
+
const tempFile = path.join(__dirname, 'temp-comments-only.ts');
|
|
119
|
+
fs.writeFileSync(tempFile, '// Only comments\n/* More comments */\n');
|
|
120
|
+
const violations = analyzer.analyze([tempFile], 'typescript');
|
|
121
|
+
expect(violations).toHaveLength(0);
|
|
122
|
+
if (fs.existsSync(tempFile)) {
|
|
123
|
+
fs.unlinkSync(tempFile);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('Cross-file Detection', () => {
|
|
129
|
+
test('should detect duplicates across multiple files', () => {
|
|
130
|
+
const file1 = path.join(fixturesPath, 'violations/duplicate-repository-logic.ts');
|
|
131
|
+
const file2 = path.join(fixturesPath, 'violations/duplicate-error-handling.ts');
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(file1) && fs.existsSync(file2)) {
|
|
134
|
+
const violations = analyzer.analyze([file1, file2], 'typescript');
|
|
135
|
+
expect(Array.isArray(violations)).toBe(true);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
49
138
|
});
|
|
50
139
|
});
|