@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,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C012 AST Analyzer - Command Query Separation
|
|
3
|
+
*
|
|
4
|
+
* Uses AST parsing to detect violations of the Command Query Separation principle:
|
|
5
|
+
* - Commands (modify state) should not return values
|
|
6
|
+
* - Queries (return data) should not have side effects
|
|
7
|
+
* - Functions that both modify state and return meaningful values violate CQS
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
class C012ASTAnalyzer {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.ruleId = 'C012';
|
|
16
|
+
this.ruleName = 'Command Query Separation';
|
|
17
|
+
this.description = 'Separate commands (modify state) from queries (return data)';
|
|
18
|
+
this.severity = 'warning';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async analyze(files, language, config = {}) {
|
|
22
|
+
const violations = [];
|
|
23
|
+
|
|
24
|
+
for (const filePath of files) {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
27
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, config);
|
|
28
|
+
violations.push(...fileViolations);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.warn(`C012 AST analysis failed for ${filePath}:`, error.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return violations;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async analyzeFile(filePath, content, language, config) {
|
|
38
|
+
const violations = [];
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
let ast;
|
|
42
|
+
|
|
43
|
+
if (language === 'typescript' || filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
|
|
44
|
+
ast = await this.parseTypeScript(content);
|
|
45
|
+
} else if (language === 'javascript' || filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
|
|
46
|
+
ast = await this.parseJavaScript(content);
|
|
47
|
+
} else {
|
|
48
|
+
// Fallback to regex analysis
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (ast) {
|
|
53
|
+
this.traverseAST(ast, (node) => {
|
|
54
|
+
const violation = this.checkCQSViolation(node, content, filePath);
|
|
55
|
+
if (violation) {
|
|
56
|
+
violations.push(violation);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(`C012 AST parsing failed for ${filePath}:`, error.message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return violations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async parseTypeScript(content) {
|
|
68
|
+
try {
|
|
69
|
+
const babel = require('@babel/parser');
|
|
70
|
+
return babel.parse(content, {
|
|
71
|
+
sourceType: 'module',
|
|
72
|
+
allowImportExportEverywhere: true,
|
|
73
|
+
allowReturnOutsideFunction: true,
|
|
74
|
+
plugins: [
|
|
75
|
+
'typescript',
|
|
76
|
+
'jsx',
|
|
77
|
+
'decorators-legacy',
|
|
78
|
+
'classProperties',
|
|
79
|
+
'asyncGenerators',
|
|
80
|
+
'functionBind',
|
|
81
|
+
'exportDefaultFrom',
|
|
82
|
+
'exportNamespaceFrom',
|
|
83
|
+
'dynamicImport',
|
|
84
|
+
'nullishCoalescingOperator',
|
|
85
|
+
'optionalChaining'
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async parseJavaScript(content) {
|
|
94
|
+
try {
|
|
95
|
+
const babel = require('@babel/parser');
|
|
96
|
+
return babel.parse(content, {
|
|
97
|
+
sourceType: 'module',
|
|
98
|
+
allowImportExportEverywhere: true,
|
|
99
|
+
allowReturnOutsideFunction: true,
|
|
100
|
+
plugins: [
|
|
101
|
+
'jsx',
|
|
102
|
+
'decorators-legacy',
|
|
103
|
+
'classProperties',
|
|
104
|
+
'asyncGenerators',
|
|
105
|
+
'functionBind',
|
|
106
|
+
'exportDefaultFrom',
|
|
107
|
+
'exportNamespaceFrom',
|
|
108
|
+
'dynamicImport',
|
|
109
|
+
'nullishCoalescingOperator',
|
|
110
|
+
'optionalChaining'
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
traverseAST(node, callback) {
|
|
119
|
+
if (!node || typeof node !== 'object') return;
|
|
120
|
+
|
|
121
|
+
callback(node);
|
|
122
|
+
|
|
123
|
+
for (const key in node) {
|
|
124
|
+
if (key === 'parent' || key === 'loc' || key === 'range') continue;
|
|
125
|
+
|
|
126
|
+
const child = node[key];
|
|
127
|
+
if (Array.isArray(child)) {
|
|
128
|
+
child.forEach(item => this.traverseAST(item, callback));
|
|
129
|
+
} else if (child && typeof child === 'object') {
|
|
130
|
+
this.traverseAST(child, callback);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
checkCQSViolation(node, content, filePath) {
|
|
136
|
+
// Check function declarations, methods, and arrow functions
|
|
137
|
+
if (!this.isFunctionNode(node)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const functionName = this.getFunctionName(node);
|
|
142
|
+
if (!functionName || this.isAllowedFunction(functionName)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hasStateModification = this.hasStateModification(node);
|
|
147
|
+
const hasReturnValue = this.hasReturnValue(node);
|
|
148
|
+
|
|
149
|
+
// CQS violation: function both modifies state AND returns meaningful value
|
|
150
|
+
if (hasStateModification && hasReturnValue) {
|
|
151
|
+
// NEW: Check if this is an acceptable pattern
|
|
152
|
+
if (this.isAcceptablePattern(node, functionName)) {
|
|
153
|
+
return null; // Allow acceptable patterns
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const line = node.loc ? node.loc.start.line : 1;
|
|
157
|
+
const column = node.loc ? node.loc.start.column + 1 : 1;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
ruleId: this.ruleId,
|
|
161
|
+
file: filePath,
|
|
162
|
+
line,
|
|
163
|
+
column,
|
|
164
|
+
message: `Function '${functionName}' violates Command Query Separation: both modifies state and returns value`,
|
|
165
|
+
severity: this.severity,
|
|
166
|
+
code: this.getNodeCode(node, content),
|
|
167
|
+
type: 'cqs_violation',
|
|
168
|
+
confidence: this.calculateConfidence(hasStateModification, hasReturnValue),
|
|
169
|
+
suggestion: this.getSuggestion(functionName, hasStateModification, hasReturnValue)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
isFunctionNode(node) {
|
|
177
|
+
return [
|
|
178
|
+
'FunctionDeclaration',
|
|
179
|
+
'FunctionExpression',
|
|
180
|
+
'ArrowFunctionExpression',
|
|
181
|
+
'MethodDefinition',
|
|
182
|
+
'ObjectMethod',
|
|
183
|
+
'ClassMethod'
|
|
184
|
+
].includes(node.type);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getFunctionName(node) {
|
|
188
|
+
if (node.key && node.key.name) {
|
|
189
|
+
return node.key.name; // Method
|
|
190
|
+
}
|
|
191
|
+
if (node.id && node.id.name) {
|
|
192
|
+
return node.id.name; // Function declaration
|
|
193
|
+
}
|
|
194
|
+
if (node.type === 'ArrowFunctionExpression' && node.parent) {
|
|
195
|
+
// Arrow function assigned to variable
|
|
196
|
+
if (node.parent.type === 'VariableDeclarator' && node.parent.id) {
|
|
197
|
+
return node.parent.id.name;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return 'anonymous';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
isAllowedFunction(functionName) {
|
|
204
|
+
// Allowed patterns that don't violate CQS
|
|
205
|
+
const allowedPatterns = [
|
|
206
|
+
// Constructor and lifecycle
|
|
207
|
+
/^constructor$/,
|
|
208
|
+
/^componentDidMount$/,
|
|
209
|
+
/^componentWillUnmount$/,
|
|
210
|
+
/^useEffect$/,
|
|
211
|
+
|
|
212
|
+
// Test functions
|
|
213
|
+
/^test_/,
|
|
214
|
+
/^it$/,
|
|
215
|
+
/^describe$/,
|
|
216
|
+
/^beforeEach$/,
|
|
217
|
+
/^afterEach$/,
|
|
218
|
+
|
|
219
|
+
// Getters/setters (have special semantics)
|
|
220
|
+
/^get\w+$/,
|
|
221
|
+
/^set\w+$/,
|
|
222
|
+
|
|
223
|
+
// Factory/builder patterns (expected to create and return)
|
|
224
|
+
/^create\w+$/,
|
|
225
|
+
/^build\w+$/,
|
|
226
|
+
/^make\w+$/,
|
|
227
|
+
/^new\w+$/,
|
|
228
|
+
|
|
229
|
+
// Initialization (setup state and return success)
|
|
230
|
+
/^init\w+$/,
|
|
231
|
+
/^setup\w+$/,
|
|
232
|
+
/^configure\w+$/,
|
|
233
|
+
|
|
234
|
+
// Toggle operations (modify state and return new state)
|
|
235
|
+
/^toggle\w+$/,
|
|
236
|
+
/^switch\w+$/,
|
|
237
|
+
|
|
238
|
+
// Array operations that modify and return
|
|
239
|
+
/^push$/,
|
|
240
|
+
/^pop$/,
|
|
241
|
+
/^shift$/,
|
|
242
|
+
/^unshift$/,
|
|
243
|
+
/^splice$/,
|
|
244
|
+
|
|
245
|
+
// Built-in operations
|
|
246
|
+
/^toString$/,
|
|
247
|
+
/^valueOf$/,
|
|
248
|
+
/^render$/
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
return allowedPatterns.some(pattern => pattern.test(functionName));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
hasStateModification(node) {
|
|
255
|
+
let hasModification = false;
|
|
256
|
+
|
|
257
|
+
this.traverseAST(node.body, (innerNode) => {
|
|
258
|
+
if (hasModification) return;
|
|
259
|
+
|
|
260
|
+
// Assignment operations
|
|
261
|
+
if (innerNode.type === 'AssignmentExpression') {
|
|
262
|
+
// Check if assigning to object property or variable
|
|
263
|
+
if (innerNode.left.type === 'MemberExpression' ||
|
|
264
|
+
innerNode.left.type === 'Identifier') {
|
|
265
|
+
hasModification = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update expressions (++, --)
|
|
270
|
+
if (innerNode.type === 'UpdateExpression') {
|
|
271
|
+
hasModification = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Method calls that likely modify state
|
|
275
|
+
if (innerNode.type === 'CallExpression' && innerNode.callee) {
|
|
276
|
+
const callName = this.getCallName(innerNode.callee);
|
|
277
|
+
if (this.isStateModifyingCall(callName)) {
|
|
278
|
+
hasModification = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Property mutations
|
|
283
|
+
if (innerNode.type === 'MemberExpression' &&
|
|
284
|
+
innerNode.parent &&
|
|
285
|
+
innerNode.parent.type === 'AssignmentExpression' &&
|
|
286
|
+
innerNode.parent.left === innerNode) {
|
|
287
|
+
hasModification = true;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return hasModification;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
hasReturnValue(node) {
|
|
295
|
+
let hasReturn = false;
|
|
296
|
+
|
|
297
|
+
this.traverseAST(node.body, (innerNode) => {
|
|
298
|
+
if (hasReturn) return;
|
|
299
|
+
|
|
300
|
+
// Return statements with value
|
|
301
|
+
if (innerNode.type === 'ReturnStatement' && innerNode.argument) {
|
|
302
|
+
// Ignore simple boolean returns (success/failure indicators)
|
|
303
|
+
if (!this.isSimpleBooleanReturn(innerNode.argument)) {
|
|
304
|
+
hasReturn = true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Arrow function with expression body
|
|
309
|
+
if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') {
|
|
310
|
+
if (!this.isSimpleBooleanReturn(node.body)) {
|
|
311
|
+
hasReturn = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return hasReturn;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
isSimpleBooleanReturn(argument) {
|
|
320
|
+
// Simple boolean literals
|
|
321
|
+
if (argument.type === 'BooleanLiteral' ||
|
|
322
|
+
(argument.type === 'Literal' && typeof argument.value === 'boolean')) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Simple boolean expressions
|
|
327
|
+
if (argument.type === 'UnaryExpression' && argument.operator === '!') {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Comparison operations (often return success/failure)
|
|
332
|
+
if (argument.type === 'BinaryExpression' &&
|
|
333
|
+
['==', '===', '!=', '!==', '<', '>', '<=', '>='].includes(argument.operator)) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
getCallName(callee) {
|
|
341
|
+
if (callee.type === 'Identifier') {
|
|
342
|
+
return callee.name;
|
|
343
|
+
}
|
|
344
|
+
if (callee.type === 'MemberExpression' && callee.property) {
|
|
345
|
+
return callee.property.name;
|
|
346
|
+
}
|
|
347
|
+
return '';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
isStateModifyingCall(callName) {
|
|
351
|
+
const modifyingCalls = [
|
|
352
|
+
'push', 'pop', 'shift', 'unshift', 'splice',
|
|
353
|
+
'sort', 'reverse', 'fill',
|
|
354
|
+
'set', 'delete', 'clear',
|
|
355
|
+
'add', 'remove', 'update',
|
|
356
|
+
'save', 'store', 'persist',
|
|
357
|
+
'increment', 'decrement',
|
|
358
|
+
'modify', 'change', 'alter',
|
|
359
|
+
'append', 'prepend', 'insert',
|
|
360
|
+
'setState', 'dispatch'
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
return modifyingCalls.includes(callName) ||
|
|
364
|
+
/^set[A-Z]/.test(callName) ||
|
|
365
|
+
/^update[A-Z]/.test(callName) ||
|
|
366
|
+
/^modify[A-Z]/.test(callName);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
calculateConfidence(hasStateModification, hasReturnValue) {
|
|
370
|
+
let confidence = 0.7;
|
|
371
|
+
|
|
372
|
+
// Higher confidence if both conditions are clearly present
|
|
373
|
+
if (hasStateModification && hasReturnValue) {
|
|
374
|
+
confidence = 0.9;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return confidence;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
isAcceptablePattern(node, functionName) {
|
|
381
|
+
// NEW: Practical CQS - Allow acceptable patterns per strategy
|
|
382
|
+
|
|
383
|
+
// 1. CRUD Operations (single operation + return)
|
|
384
|
+
const crudPatterns = [
|
|
385
|
+
/^(create|insert|add|save|store)\w*$/i,
|
|
386
|
+
/^(update|modify|edit|change)\w*$/i,
|
|
387
|
+
/^(upsert|merge)\w*$/i,
|
|
388
|
+
/^(delete|remove|destroy)\w*$/i
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
if (crudPatterns.some(pattern => pattern.test(functionName))) {
|
|
392
|
+
// Check if it's a simple CRUD - single operation
|
|
393
|
+
const queryCount = this.countDatabaseOperations(node);
|
|
394
|
+
if (queryCount <= 1) {
|
|
395
|
+
return true; // Single query + return is acceptable
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 2. Transaction-based Operations
|
|
400
|
+
if (this.isTransactionBased(node)) {
|
|
401
|
+
return true; // Multiple operations in transaction are atomic
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 3. ORM Standard Patterns
|
|
405
|
+
const ormPatterns = [
|
|
406
|
+
/^findOrCreate\w*$/i,
|
|
407
|
+
/^findAndUpdate\w*$/i,
|
|
408
|
+
/^findAndModify\w*$/i,
|
|
409
|
+
/^saveAndReturn\w*$/i,
|
|
410
|
+
/^selectForUpdate\w*$/i
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
if (ormPatterns.some(pattern => pattern.test(functionName))) {
|
|
414
|
+
return true; // Standard ORM patterns including selectForUpdate
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 4. Factory patterns (create and return by design)
|
|
418
|
+
const factoryPatterns = [
|
|
419
|
+
/^(build|construct|generate|produce)\w*$/i,
|
|
420
|
+
/^(transform|convert|map)\w*$/i
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
if (factoryPatterns.some(pattern => pattern.test(functionName))) {
|
|
424
|
+
return true; // Factory patterns expected to create and return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return false; // Not an acceptable pattern - flag as violation
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
countDatabaseOperations(node) {
|
|
431
|
+
let count = 0;
|
|
432
|
+
|
|
433
|
+
this.traverseAST(node.body, (innerNode) => {
|
|
434
|
+
if (innerNode.type === 'CallExpression' && innerNode.callee) {
|
|
435
|
+
const callName = this.getCallName(innerNode.callee);
|
|
436
|
+
|
|
437
|
+
// Database operation patterns
|
|
438
|
+
const dbOperations = [
|
|
439
|
+
'save', 'insert', 'create', 'update', 'delete', 'remove',
|
|
440
|
+
'find', 'findOne', 'findBy', 'query', 'execute',
|
|
441
|
+
'upsert', 'merge', 'replace'
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
if (dbOperations.some(op => callName.toLowerCase().includes(op))) {
|
|
445
|
+
count++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return count;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
isTransactionBased(node) {
|
|
454
|
+
let isTransaction = false;
|
|
455
|
+
|
|
456
|
+
this.traverseAST(node.body, (innerNode) => {
|
|
457
|
+
if (innerNode.type === 'CallExpression' && innerNode.callee) {
|
|
458
|
+
const callName = this.getCallName(innerNode.callee);
|
|
459
|
+
|
|
460
|
+
// Transaction indicators
|
|
461
|
+
const transactionPatterns = [
|
|
462
|
+
'transaction', 'withTransaction', 'runInTransaction',
|
|
463
|
+
'beginTransaction', 'commit', 'rollback',
|
|
464
|
+
'manager.transaction', 'queryRunner.startTransaction'
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
if (transactionPatterns.some(pattern =>
|
|
468
|
+
callName.toLowerCase().includes(pattern.toLowerCase()))) {
|
|
469
|
+
isTransaction = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
return isTransaction;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
getSuggestion(functionName, hasStateModification, hasReturnValue) {
|
|
478
|
+
if (hasStateModification && hasReturnValue) {
|
|
479
|
+
return `Split '${functionName}' into separate command (modify state) and query (return data) functions`;
|
|
480
|
+
}
|
|
481
|
+
return `Follow Command Query Separation principle for '${functionName}'`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getNodeCode(node, content) {
|
|
485
|
+
if (node.loc) {
|
|
486
|
+
const lines = content.split('\n');
|
|
487
|
+
const startLine = node.loc.start.line - 1;
|
|
488
|
+
const endLine = Math.min(node.loc.end.line - 1, startLine + 2); // Limit to 3 lines
|
|
489
|
+
return lines.slice(startLine, endLine + 1).join('\n').trim();
|
|
490
|
+
}
|
|
491
|
+
return 'Unknown code';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = C012ASTAnalyzer;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic analyzer for: C013 – Do not leave dead code
|
|
3
|
+
* Purpose: Detect unreachable code after return, throw, break, continue statements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class C013Analyzer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.ruleId = 'C013';
|
|
9
|
+
this.ruleName = 'No Dead Code';
|
|
10
|
+
this.description = 'Avoid unreachable code after return, throw, break, continue statements';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async analyze(files, language, options = {}) {
|
|
14
|
+
const violations = [];
|
|
15
|
+
|
|
16
|
+
for (const filePath of files) {
|
|
17
|
+
if (options.verbose) {
|
|
18
|
+
console.log(`🔍 Running C013 analysis on ${require('path').basename(filePath)}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
23
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
24
|
+
violations.push(...fileViolations);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return violations;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
analyzeFile(content, filePath) {
|
|
34
|
+
const violations = [];
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i].trim();
|
|
39
|
+
|
|
40
|
+
// Skip empty lines and comments
|
|
41
|
+
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Only look for return statements (not throw in catch blocks)
|
|
46
|
+
if (this.isSimpleReturn(line)) {
|
|
47
|
+
// Check if there are non-comment, non-empty lines after this return
|
|
48
|
+
// within the same function scope
|
|
49
|
+
const unreachableLines = this.findUnreachableCodeSimple(lines, i);
|
|
50
|
+
|
|
51
|
+
for (const unreachableLine of unreachableLines) {
|
|
52
|
+
violations.push({
|
|
53
|
+
file: filePath,
|
|
54
|
+
line: unreachableLine + 1,
|
|
55
|
+
column: 1,
|
|
56
|
+
message: `Unreachable code detected after return statement. Remove dead code or restructure logic.`,
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
ruleId: this.ruleId
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return violations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isSimpleReturn(line) {
|
|
68
|
+
// Only detect simple return statements that actually complete, not multiline returns
|
|
69
|
+
const cleanLine = line.replace(/;?\s*$/, '');
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
// Complete return statements with semicolon or at end of line
|
|
73
|
+
/^return\s*;?\s*$/.test(cleanLine) ||
|
|
74
|
+
/^return\s+[^{}\[\(]+;?\s*$/.test(cleanLine) ||
|
|
75
|
+
// Single-line returns with simple values
|
|
76
|
+
/^return\s+(true|false|null|undefined|\d+|"[^"]*"|'[^']*')\s*;?\s*$/.test(cleanLine) ||
|
|
77
|
+
// Handle single-line conditional returns
|
|
78
|
+
/}\s*else\s*return\s+[^{}\[\(]+;?\s*$/.test(line) ||
|
|
79
|
+
/}\s*return\s+[^{}\[\(]+;?\s*$/.test(line)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
findUnreachableCodeSimple(lines, terminatingLineIndex) {
|
|
84
|
+
const unreachableLines = [];
|
|
85
|
+
|
|
86
|
+
// Look for code after the return statement until we hit a closing brace or new function
|
|
87
|
+
for (let i = terminatingLineIndex + 1; i < lines.length; i++) {
|
|
88
|
+
const line = lines[i].trim();
|
|
89
|
+
|
|
90
|
+
// Skip empty lines and comments
|
|
91
|
+
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stop if we hit a closing brace (end of function/block)
|
|
96
|
+
if (line === '}' || line === '};' || line.startsWith('} ')) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Stop if we hit catch/finally (these are reachable)
|
|
101
|
+
if (line.includes('catch') || line.includes('finally')) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Stop if we hit a new function or method definition
|
|
106
|
+
if (line.includes('function') || line.includes('=>') || line.match(/^\w+\s*\(/)) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// This looks like unreachable code
|
|
111
|
+
if (this.isExecutableCode(line)) {
|
|
112
|
+
unreachableLines.push(i);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return unreachableLines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
findUnreachableCode(lines, terminatingLineIndex, content) {
|
|
120
|
+
const unreachableLines = [];
|
|
121
|
+
let braceDepth = this.getCurrentBraceDepth(lines, terminatingLineIndex, content);
|
|
122
|
+
let currentDepth = braceDepth;
|
|
123
|
+
let inTryCatchFinally = false;
|
|
124
|
+
|
|
125
|
+
// Check if we're inside a try-catch-finally context
|
|
126
|
+
const contextBefore = lines.slice(0, terminatingLineIndex).join(' ');
|
|
127
|
+
if (contextBefore.includes('try') || contextBefore.includes('catch') || contextBefore.includes('finally')) {
|
|
128
|
+
// Need more sophisticated detection of try-catch-finally blocks
|
|
129
|
+
inTryCatchFinally = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Look for unreachable code after the terminating statement
|
|
133
|
+
for (let i = terminatingLineIndex + 1; i < lines.length; i++) {
|
|
134
|
+
const line = lines[i].trim();
|
|
135
|
+
|
|
136
|
+
// Skip empty lines and comments
|
|
137
|
+
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Special handling for try-catch-finally: finally blocks are always reachable
|
|
142
|
+
if (line.includes('finally') || (inTryCatchFinally && line === '}')) {
|
|
143
|
+
// Don't mark finally blocks or their closing braces as unreachable
|
|
144
|
+
if (line.includes('finally')) {
|
|
145
|
+
inTryCatchFinally = false; // Reset after seeing finally
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Update brace depth
|
|
151
|
+
const openBraces = (line.match(/{/g) || []).length;
|
|
152
|
+
const closeBraces = (line.match(/}/g) || []).length;
|
|
153
|
+
currentDepth += openBraces - closeBraces;
|
|
154
|
+
|
|
155
|
+
// If we've exited the current block scope, stop looking
|
|
156
|
+
if (currentDepth < braceDepth) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If we're still in the same block scope, this is potentially unreachable
|
|
161
|
+
if (currentDepth === braceDepth) {
|
|
162
|
+
// Check if this line contains executable code (not just closing braces)
|
|
163
|
+
if (this.isExecutableCode(line)) {
|
|
164
|
+
unreachableLines.push(i);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return unreachableLines;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getCurrentBraceDepth(lines, lineIndex, content) {
|
|
173
|
+
// Calculate the brace depth at the given line
|
|
174
|
+
let depth = 0;
|
|
175
|
+
const textUpToLine = lines.slice(0, lineIndex + 1).join('\n');
|
|
176
|
+
|
|
177
|
+
for (let char of textUpToLine) {
|
|
178
|
+
if (char === '{') depth++;
|
|
179
|
+
if (char === '}') depth--;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return depth;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
isExecutableCode(line) {
|
|
186
|
+
// Exclude lines that are just structural (closing braces, etc.)
|
|
187
|
+
if (line === '}' || line === '};' || line === '},' || line.match(/^\s*}\s*$/)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Exclude catch/finally blocks (they are reachable)
|
|
192
|
+
if (line.includes('catch') || line.includes('finally')) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Exclude case/default statements (they might be reachable)
|
|
197
|
+
if (line.startsWith('case ') || line.startsWith('default:')) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// This looks like executable code
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = C013Analyzer;
|