@sun-asterisk/sunlint 1.2.1 → 1.2.2
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 +18 -2
- package/engines/eslint-engine.js +9 -11
- package/engines/heuristic-engine.js +55 -31
- package/package.json +2 -1
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/common/C006_function_naming/analyzer.js +504 -0
- package/rules/common/C006_function_naming/config.json +86 -0
- package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C012_command_query_separation/analyzer.js +481 -0
- package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +362 -0
- package/rules/common/C019_log_level_usage/config.json +121 -0
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
- package/rules/common/C031_validation_separation/analyzer.js +186 -0
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/docs/C031_validation_separation.md +72 -0
- package/rules/index.js +155 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/parser/constants.js +31 -0
- package/rules/parser/file-config.js +80 -0
- package/rules/parser/rule-parser-simple.js +305 -0
- package/rules/parser/rule-parser.js +527 -0
- package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
- package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
- package/rules/security/S023_no_json_injection/analyzer.js +278 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +330 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/generate_insights.js +188 -0
- package/scripts/merge-reports.js +0 -424
- package/scripts/test-scripts/README.md +0 -22
- package/scripts/test-scripts/test-c041-comparison.js +0 -114
- package/scripts/test-scripts/test-c041-eslint.js +0 -67
- package/scripts/test-scripts/test-eslint-rules.js +0 -146
- package/scripts/test-scripts/test-real-world.js +0 -44
- package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class C014Analyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'C014';
|
|
7
|
+
this.ruleName = 'Dependency Injection Pattern';
|
|
8
|
+
this.description = 'Dùng Dependency Injection thay vì new trực tiếp trong logic';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze(files, language, options = {}) {
|
|
12
|
+
const violations = [];
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
if (options.verbose) {
|
|
16
|
+
console.log(`🔍 Running C014 analysis on ${path.basename(filePath)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
22
|
+
violations.push(...fileViolations);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return violations;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async analyzeFile(filePath, content, language, config) {
|
|
32
|
+
switch (language) {
|
|
33
|
+
case 'typescript':
|
|
34
|
+
case 'javascript':
|
|
35
|
+
return this.analyzeTypeScript(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeTypeScript(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
|
|
45
|
+
lines.forEach((line, index) => {
|
|
46
|
+
const lineNumber = index + 1;
|
|
47
|
+
const trimmedLine = line.trim();
|
|
48
|
+
|
|
49
|
+
// Skip comments and imports
|
|
50
|
+
if (this.isCommentOrImport(trimmedLine)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find new expressions
|
|
55
|
+
const newExpressions = this.findNewExpressions(trimmedLine, line);
|
|
56
|
+
|
|
57
|
+
newExpressions.forEach(expr => {
|
|
58
|
+
if (this.shouldFlag(expr.className, line, filePath)) {
|
|
59
|
+
violations.push({
|
|
60
|
+
ruleId: this.ruleId,
|
|
61
|
+
file: filePath,
|
|
62
|
+
line: lineNumber,
|
|
63
|
+
column: expr.column,
|
|
64
|
+
message: `Avoid direct instantiation of '${expr.className}'. Use dependency injection or factory pattern instead`,
|
|
65
|
+
severity: 'warning',
|
|
66
|
+
code: trimmedLine,
|
|
67
|
+
type: 'direct_instantiation',
|
|
68
|
+
confidence: expr.confidence,
|
|
69
|
+
suggestion: `Consider injecting ${expr.className} as a dependency or using a factory`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return violations;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isCommentOrImport(line) {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
return trimmed.startsWith('//') ||
|
|
81
|
+
trimmed.startsWith('/*') ||
|
|
82
|
+
trimmed.startsWith('*') ||
|
|
83
|
+
trimmed.startsWith('import ') ||
|
|
84
|
+
trimmed.startsWith('export ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
findNewExpressions(line, originalLine) {
|
|
88
|
+
const expressions = [];
|
|
89
|
+
const regex = /\bnew\s+([A-Z][a-zA-Z0-9_]*)\s*\(/g;
|
|
90
|
+
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = regex.exec(line)) !== null) {
|
|
93
|
+
expressions.push({
|
|
94
|
+
className: match[1],
|
|
95
|
+
column: match.index + 1,
|
|
96
|
+
confidence: this.calculateConfidence(match[1], line)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return expressions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
shouldFlag(className, line, filePath) {
|
|
104
|
+
// Don't flag built-in JavaScript/TypeScript classes and common patterns
|
|
105
|
+
const allowedClasses = [
|
|
106
|
+
// Built-in JavaScript classes
|
|
107
|
+
'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp',
|
|
108
|
+
'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Error', 'TypeError',
|
|
109
|
+
'RangeError', 'SyntaxError', 'ReferenceError', 'EvalError', 'URIError',
|
|
110
|
+
'Function', 'Symbol', 'BigInt', 'ArrayBuffer', 'DataView',
|
|
111
|
+
'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array',
|
|
112
|
+
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array',
|
|
113
|
+
|
|
114
|
+
// DOM classes (Frontend)
|
|
115
|
+
'FormData', 'Headers', 'Request', 'Response', 'URLSearchParams',
|
|
116
|
+
'URL', 'Blob', 'File', 'FileReader', 'Image', 'Audio', 'Video',
|
|
117
|
+
'XMLHttpRequest', 'WebSocket', 'Worker', 'MessageChannel',
|
|
118
|
+
'MutationObserver', 'ResizeObserver', 'IntersectionObserver',
|
|
119
|
+
'AbortController', 'CustomEvent', 'Event', 'ErrorEvent',
|
|
120
|
+
|
|
121
|
+
// Node.js classes
|
|
122
|
+
'Buffer', 'URL', 'URLSearchParams', 'ReadableStream', 'WritableStream',
|
|
123
|
+
|
|
124
|
+
// React/Next.js common patterns
|
|
125
|
+
'NextResponse', 'Response', 'Headers', 'FormData',
|
|
126
|
+
|
|
127
|
+
// Common library classes that are okay to instantiate directly
|
|
128
|
+
'Axios', 'HttpClient', 'Logger', 'Configuration', 'Config', 'AppConfig', 'UserSettings',
|
|
129
|
+
'AppConfiguration', 'SystemLogger', 'Cache', 'Monitor', 'Connection', 'DatabaseConfig'
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
if (allowedClasses.includes(className)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Don't flag if it's clearly for testing
|
|
137
|
+
if (this.isTestContext(line, filePath)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Don't flag if it's in React hooks or state management
|
|
142
|
+
if (this.isReactHookContext(line)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Don't flag if it's in a factory pattern or builder pattern
|
|
147
|
+
if (this.isFactoryOrBuilderPattern(line)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Don't flag if it's a utility class or static helper
|
|
152
|
+
if (this.isUtilityClass(className, line)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Don't flag if it's in constructor or initialization context
|
|
157
|
+
if (this.isConstructorOrInitContext(line)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Don't flag if it's immediate usage pattern (e.g., new Date().getTime())
|
|
162
|
+
if (this.isImmediateUsagePattern(line)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isTestContext(line, filePath) {
|
|
170
|
+
// Check if file is a test file
|
|
171
|
+
const testPatterns = ['.test.', '.spec.', '__tests__', '/tests/'];
|
|
172
|
+
const isTestFile = testPatterns.some(pattern => filePath.includes(pattern));
|
|
173
|
+
|
|
174
|
+
if (isTestFile) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check if line contains test-related keywords - use word boundaries to avoid false matches
|
|
179
|
+
const testKeywords = ['test', 'describe', 'expect', 'mock', 'spy', 'stub'];
|
|
180
|
+
const lowerLine = line.toLowerCase();
|
|
181
|
+
|
|
182
|
+
// Use word boundaries to avoid false positives like "Repository" containing "it"
|
|
183
|
+
const hasTestKeywords = testKeywords.some(keyword => {
|
|
184
|
+
const wordBoundaryRegex = new RegExp(`\\b${keyword}\\b`);
|
|
185
|
+
return wordBoundaryRegex.test(lowerLine);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Special case for "it" - only match as standalone word or in specific contexts
|
|
189
|
+
const hasItKeyword = /\bit\s*\(/.test(lowerLine); // it( function calls
|
|
190
|
+
|
|
191
|
+
return hasTestKeywords || hasItKeyword;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
isFactoryOrBuilderPattern(line) {
|
|
195
|
+
const lowerLine = line.toLowerCase();
|
|
196
|
+
const patterns = [
|
|
197
|
+
'factory',
|
|
198
|
+
'builder',
|
|
199
|
+
'create',
|
|
200
|
+
'build',
|
|
201
|
+
'make',
|
|
202
|
+
'construct',
|
|
203
|
+
'getinstance',
|
|
204
|
+
'newinstance',
|
|
205
|
+
'=> new',
|
|
206
|
+
'return new'
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
return patterns.some(pattern => lowerLine.includes(pattern));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
isUtilityClass(className, line) {
|
|
213
|
+
// Common utility class patterns based on name - but exclude business logic handlers
|
|
214
|
+
const utilityPatterns = [
|
|
215
|
+
/Helper$/,
|
|
216
|
+
/Util$/,
|
|
217
|
+
/Utils$/,
|
|
218
|
+
/Manager$/,
|
|
219
|
+
/Processor$/,
|
|
220
|
+
/Validator$/,
|
|
221
|
+
/Parser$/,
|
|
222
|
+
/Formatter$/,
|
|
223
|
+
/Converter$/
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
// Business logic handlers should not be considered utilities
|
|
227
|
+
const businessLogicExceptions = [
|
|
228
|
+
'EventHandler',
|
|
229
|
+
'CommandHandler',
|
|
230
|
+
'RequestHandler',
|
|
231
|
+
'ActionHandler',
|
|
232
|
+
'MessageHandler',
|
|
233
|
+
'HttpHandler',
|
|
234
|
+
'ApiHandler'
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
if (businessLogicExceptions.includes(className)) {
|
|
238
|
+
return false; // These should be flagged as DI violations
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const isUtilityName = utilityPatterns.some(pattern => pattern.test(className));
|
|
242
|
+
|
|
243
|
+
if (isUtilityName) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if it's used as return value (factory pattern)
|
|
248
|
+
const factoryContext = [
|
|
249
|
+
'return new',
|
|
250
|
+
'export new',
|
|
251
|
+
'export default new'
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
return factoryContext.some(context => line.includes(context));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
isConstructorOrInitContext(line) {
|
|
258
|
+
const lowerLine = line.toLowerCase();
|
|
259
|
+
const patterns = [
|
|
260
|
+
'constructor',
|
|
261
|
+
'init',
|
|
262
|
+
'setup',
|
|
263
|
+
'configure',
|
|
264
|
+
'this.',
|
|
265
|
+
'super(',
|
|
266
|
+
'initialize'
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
return patterns.some(pattern => lowerLine.includes(pattern));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
isReactHookContext(line) {
|
|
273
|
+
// Check for React hooks patterns
|
|
274
|
+
const reactHookPatterns = [
|
|
275
|
+
'useState(',
|
|
276
|
+
'useEffect(',
|
|
277
|
+
'useMemo(',
|
|
278
|
+
'useCallback(',
|
|
279
|
+
'useRef(',
|
|
280
|
+
'useContext(',
|
|
281
|
+
'useReducer(',
|
|
282
|
+
'useImperativeHandle(',
|
|
283
|
+
'useLayoutEffect(',
|
|
284
|
+
'useDebugValue(',
|
|
285
|
+
// Custom hooks
|
|
286
|
+
'use[A-Z]'
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
return reactHookPatterns.some(pattern => {
|
|
290
|
+
if (pattern.includes('[A-Z]')) {
|
|
291
|
+
return new RegExp(pattern).test(line);
|
|
292
|
+
}
|
|
293
|
+
return line.includes(pattern);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
isImmediateUsagePattern(line) {
|
|
298
|
+
// Check for immediate usage patterns like new Date().getTime()
|
|
299
|
+
const immediatePatterns = [
|
|
300
|
+
/new\s+\w+\(\)\./, // new Date().getTime(), new Error().stack
|
|
301
|
+
/new\s+\w+\([^)]*\)\./, // new Date(value).getTime()
|
|
302
|
+
/window\.dispatchEvent\(new\s+/, // window.dispatchEvent(new Event())
|
|
303
|
+
/document\.dispatchEvent\(new\s+/, // document.dispatchEvent(new Event())
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
// Special case: standalone built-in class instantiation (not assignment)
|
|
307
|
+
const standaloneBuiltinPattern = /(?:^|\s|;)\s*new\s+(Date|Error|Array|Set|Map|Promise|RegExp|URL|FormData|Event|FileReader|XMLHttpRequest)\(\)\s*;?\s*$/;
|
|
308
|
+
if (standaloneBuiltinPattern.test(line)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return immediatePatterns.some(pattern => pattern.test(line));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
calculateConfidence(className, line) {
|
|
316
|
+
let confidence = 0.7; // Base confidence
|
|
317
|
+
|
|
318
|
+
// Increase confidence for business logic classes
|
|
319
|
+
if (className.includes('Service') || className.includes('Repository') ||
|
|
320
|
+
className.includes('Controller') || className.includes('Component')) {
|
|
321
|
+
confidence += 0.2;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Decrease confidence if in constructor or initialization context
|
|
325
|
+
if (line.includes('constructor') || line.includes('init') || line.includes('setup')) {
|
|
326
|
+
confidence -= 0.1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Decrease confidence if it looks like configuration
|
|
330
|
+
if (line.includes('config') || line.includes('options') || line.includes('settings')) {
|
|
331
|
+
confidence -= 0.1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return Math.max(0.3, Math.min(1.0, confidence));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = new C014Analyzer();
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class C017Analyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'C017';
|
|
7
|
+
this.ruleName = 'Constructor Logic Limitation';
|
|
8
|
+
this.description = 'Không gán logic xử lý vào constructor';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze(files, language, options = {}) {
|
|
12
|
+
const violations = [];
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
if (options.verbose) {
|
|
16
|
+
console.log(`🔍 Running C017 analysis on ${path.basename(filePath)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
22
|
+
violations.push(...fileViolations);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return violations;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async analyzeFile(filePath, content, language, config) {
|
|
32
|
+
switch (language) {
|
|
33
|
+
case 'typescript':
|
|
34
|
+
case 'javascript':
|
|
35
|
+
return this.analyzeTypeScript(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeTypeScript(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
|
|
45
|
+
// Find constructor blocks and analyze their content
|
|
46
|
+
lines.forEach((line, index) => {
|
|
47
|
+
const lineNumber = index + 1;
|
|
48
|
+
const trimmedLine = line.trim();
|
|
49
|
+
|
|
50
|
+
// Detect constructor start
|
|
51
|
+
if (this.isConstructorStart(trimmedLine)) {
|
|
52
|
+
const constructorInfo = this.extractConstructorInfo(lines, index);
|
|
53
|
+
|
|
54
|
+
const complexLogic = this.findComplexLogic(constructorInfo.body);
|
|
55
|
+
|
|
56
|
+
complexLogic.forEach(logic => {
|
|
57
|
+
violations.push({
|
|
58
|
+
ruleId: this.ruleId,
|
|
59
|
+
file: filePath,
|
|
60
|
+
line: lineNumber + logic.lineOffset,
|
|
61
|
+
column: logic.column,
|
|
62
|
+
message: `Constructor contains complex logic: ${logic.description}. Move to initialization methods`,
|
|
63
|
+
severity: 'warning',
|
|
64
|
+
code: logic.code,
|
|
65
|
+
type: logic.type,
|
|
66
|
+
confidence: logic.confidence,
|
|
67
|
+
suggestion: 'Move complex logic to separate initialization methods or lifecycle hooks'
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return violations;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isConstructorStart(line) {
|
|
77
|
+
return line.includes('constructor(') || line.match(/constructor\s*\(/);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
extractConstructorInfo(lines, startIndex) {
|
|
81
|
+
const constructorLines = [];
|
|
82
|
+
let braceDepth = 0;
|
|
83
|
+
let foundConstructorBrace = false;
|
|
84
|
+
|
|
85
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
86
|
+
const line = lines[i];
|
|
87
|
+
constructorLines.push(line);
|
|
88
|
+
|
|
89
|
+
// Check if this is the constructor line with opening brace
|
|
90
|
+
if (this.isConstructorStart(line.trim())) {
|
|
91
|
+
// Count braces in the constructor line itself
|
|
92
|
+
for (const char of line) {
|
|
93
|
+
if (char === '{') {
|
|
94
|
+
foundConstructorBrace = true;
|
|
95
|
+
braceDepth = 1; // Start counting from 1 for the constructor block
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else if (foundConstructorBrace) {
|
|
99
|
+
// Count braces in subsequent lines
|
|
100
|
+
for (const char of line) {
|
|
101
|
+
if (char === '{') {
|
|
102
|
+
braceDepth++;
|
|
103
|
+
} else if (char === '}') {
|
|
104
|
+
braceDepth--;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If we've closed all braces, we're done
|
|
109
|
+
if (braceDepth === 0) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
body: constructorLines,
|
|
117
|
+
startLine: startIndex
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
findComplexLogic(constructorBody) {
|
|
122
|
+
const complexLogic = [];
|
|
123
|
+
|
|
124
|
+
constructorBody.forEach((line, index) => {
|
|
125
|
+
const trimmedLine = line.trim();
|
|
126
|
+
|
|
127
|
+
// Skip comments, empty lines, and constructor declaration
|
|
128
|
+
if (!trimmedLine ||
|
|
129
|
+
trimmedLine.startsWith('//') ||
|
|
130
|
+
trimmedLine.startsWith('/*') ||
|
|
131
|
+
trimmedLine.startsWith('*') ||
|
|
132
|
+
this.isConstructorStart(trimmedLine) ||
|
|
133
|
+
trimmedLine === '{' ||
|
|
134
|
+
trimmedLine === '}') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Analyze line for complex logic patterns
|
|
139
|
+
const logicType = this.analyzeLineComplexity(trimmedLine);
|
|
140
|
+
|
|
141
|
+
if (logicType) {
|
|
142
|
+
complexLogic.push({
|
|
143
|
+
lineOffset: index,
|
|
144
|
+
column: line.indexOf(trimmedLine) + 1,
|
|
145
|
+
code: trimmedLine,
|
|
146
|
+
type: logicType.type,
|
|
147
|
+
description: logicType.description,
|
|
148
|
+
confidence: logicType.confidence
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return complexLogic;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
analyzeLineComplexity(line) {
|
|
157
|
+
// Simple property assignments are OK
|
|
158
|
+
if (this.isSimpleAssignment(line)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Super calls are OK
|
|
163
|
+
if (this.isSuperCall(line)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parameter assignments are OK
|
|
168
|
+
if (this.isParameterAssignment(line)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MobX decorators/observables setup are OK
|
|
173
|
+
if (this.isMobXSetup(line)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Allowed method calls are OK
|
|
178
|
+
if (this.isAllowedMethodCall(line)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Detect complex logic patterns
|
|
183
|
+
const complexPatterns = [
|
|
184
|
+
{
|
|
185
|
+
pattern: /\bif\s*\(|\belse\s|\bswitch\s*\(/,
|
|
186
|
+
type: 'conditional_logic',
|
|
187
|
+
description: 'conditional statements (if/else/switch)',
|
|
188
|
+
confidence: 0.9
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
pattern: /\bfor\s*\(|\bwhile\s*\(|\bdo\s+/,
|
|
192
|
+
type: 'loop_logic',
|
|
193
|
+
description: 'loops (for/while)',
|
|
194
|
+
confidence: 0.95
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
pattern: /\btry\s*{|\bcatch\s*\(|\bfinally\s*{/,
|
|
198
|
+
type: 'exception_handling',
|
|
199
|
+
description: 'exception handling (try/catch/finally)',
|
|
200
|
+
confidence: 0.8
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
pattern: /\.then\s*\(|\.catch\s*\(|await\s+/,
|
|
204
|
+
type: 'async_logic',
|
|
205
|
+
description: 'asynchronous operations',
|
|
206
|
+
confidence: 0.9
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
pattern: /\w+\s*\(\s*[^)]*\)\s*[;{]/,
|
|
210
|
+
type: 'method_call',
|
|
211
|
+
description: 'complex method calls',
|
|
212
|
+
confidence: 0.6 // Lower confidence, only flag complex calls
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
pattern: /new\s+\w+\s*\([^)]*\)\s*\./,
|
|
216
|
+
type: 'chained_instantiation',
|
|
217
|
+
description: 'chained object instantiation',
|
|
218
|
+
confidence: 0.8
|
|
219
|
+
}
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
for (const { pattern, type, description, confidence } of complexPatterns) {
|
|
223
|
+
if (pattern.test(line)) {
|
|
224
|
+
return { type, description, confidence };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check for complex expressions
|
|
229
|
+
if (this.hasComplexExpression(line)) {
|
|
230
|
+
return {
|
|
231
|
+
type: 'complex_expression',
|
|
232
|
+
description: 'complex expression or calculation',
|
|
233
|
+
confidence: 0.6
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
isSimpleAssignment(line) {
|
|
241
|
+
// Patterns for simple assignments
|
|
242
|
+
const simplePatterns = [
|
|
243
|
+
/^this\.\w+\s*=\s*[^;]+;?$/, // this.property = value;
|
|
244
|
+
/^this\.\w+\s*=\s*\w+;?$/, // this.property = parameter;
|
|
245
|
+
/^this\.\w+\s*=\s*(null|undefined|true|false|\d+|'[^']*'|"[^"]*");?$/, // this.property = literal;
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return simplePatterns.some(pattern => pattern.test(line));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
isSuperCall(line) {
|
|
252
|
+
return line.includes('super(') || line.startsWith('super.');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
isParameterAssignment(line) {
|
|
256
|
+
// Check if it's just assigning constructor parameters to properties
|
|
257
|
+
return /^this\.\w+\s*=\s*\w+;?$/.test(line);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
isMobXSetup(line) {
|
|
261
|
+
// MobX patterns that are OK in constructor
|
|
262
|
+
const mobxPatterns = [
|
|
263
|
+
'makeObservable',
|
|
264
|
+
'makeAutoObservable',
|
|
265
|
+
'observable',
|
|
266
|
+
'action',
|
|
267
|
+
'computed',
|
|
268
|
+
'reaction',
|
|
269
|
+
'autorun',
|
|
270
|
+
'@observable',
|
|
271
|
+
'@action',
|
|
272
|
+
'@computed'
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const lowerLine = line.toLowerCase();
|
|
276
|
+
return mobxPatterns.some(pattern =>
|
|
277
|
+
lowerLine.includes(pattern.toLowerCase())
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
isAllowedMethodCall(line) {
|
|
282
|
+
// Method calls that are typically OK in constructors
|
|
283
|
+
const allowedMethods = [
|
|
284
|
+
'makeObservable',
|
|
285
|
+
'makeAutoObservable',
|
|
286
|
+
'bind',
|
|
287
|
+
'addEventListener',
|
|
288
|
+
'removeEventListener',
|
|
289
|
+
'Object.assign',
|
|
290
|
+
'Object.defineProperty',
|
|
291
|
+
'console.log',
|
|
292
|
+
'console.warn'
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return allowedMethods.some(method =>
|
|
296
|
+
line.includes(method)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
hasComplexExpression(line) {
|
|
301
|
+
// Check for complex expressions (multiple operators, complex calculations)
|
|
302
|
+
const complexityIndicators = [
|
|
303
|
+
/[+\-*/]\s*[+\-*/]/, // Multiple arithmetic operators
|
|
304
|
+
/\?\s*.*\s*:/, // Ternary operators
|
|
305
|
+
/&&|\|\|/, // Logical operators
|
|
306
|
+
/\[[^\]]*\]/, // Array access
|
|
307
|
+
/\.[^.]*\./ // Chained property access
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
return complexityIndicators.some(pattern => pattern.test(line));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = new C017Analyzer();
|