@sun-asterisk/sunlint 1.3.0 → 1.3.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/CHANGELOG.md +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +232 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +6 -1
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
// Enhanced symbol-based analyzer for C014
|
|
2
|
+
const { SyntaxKind } = require('ts-morph');
|
|
3
|
+
|
|
4
|
+
class C014SymbolBasedAnalyzer {
|
|
5
|
+
constructor(semanticEngine = null) {
|
|
6
|
+
this.semanticEngine = semanticEngine;
|
|
7
|
+
this.verbose = false;
|
|
8
|
+
|
|
9
|
+
// Configuration
|
|
10
|
+
this.config = {
|
|
11
|
+
// Built-in classes that are allowed
|
|
12
|
+
allowedBuiltins: [
|
|
13
|
+
'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp',
|
|
14
|
+
'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Error', 'TypeError',
|
|
15
|
+
'FormData', 'Headers', 'Request', 'Response', 'URLSearchParams',
|
|
16
|
+
'URL', 'Blob', 'File', 'Buffer', 'AbortController', 'AbortSignal',
|
|
17
|
+
'TextEncoder', 'TextDecoder', 'MessageChannel', 'MessagePort',
|
|
18
|
+
'Worker', 'SharedWorker', 'EventSource', 'WebSocket'
|
|
19
|
+
],
|
|
20
|
+
|
|
21
|
+
// Value objects/DTOs that are typically safe to instantiate
|
|
22
|
+
allowedValueObjects: [
|
|
23
|
+
'Money', 'Price', 'Currency', 'Quantity', 'Amount',
|
|
24
|
+
'Email', 'Phone', 'Address', 'Name', 'Id', 'UserId',
|
|
25
|
+
'UUID', 'Timestamp', 'Duration', 'Range'
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
// Infrastructure patterns that suggest external dependencies
|
|
29
|
+
infraPatterns: [
|
|
30
|
+
'Client', 'Repository', 'Service', 'Gateway', 'Adapter',
|
|
31
|
+
'Provider', 'Factory', 'Builder', 'Manager', 'Handler',
|
|
32
|
+
'Controller', 'Processor', 'Validator', 'Logger'
|
|
33
|
+
],
|
|
34
|
+
|
|
35
|
+
// DI decorators that indicate proper injection
|
|
36
|
+
diDecorators: [
|
|
37
|
+
'Injectable', 'Inject', 'Autowired', 'Component',
|
|
38
|
+
'Service', 'Repository', 'Controller', 'autoInjectable'
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
// Patterns to exclude from analysis
|
|
42
|
+
excludePatterns: [
|
|
43
|
+
'**/*.test.ts', '**/*.spec.ts', '**/*.test.js', '**/*.spec.js',
|
|
44
|
+
'**/tests/**', '**/test/**', '**/migration/**', '**/scripts/**'
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async initialize(semanticEngine = null) {
|
|
50
|
+
if (semanticEngine) {
|
|
51
|
+
this.semanticEngine = semanticEngine;
|
|
52
|
+
}
|
|
53
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
57
|
+
const violations = [];
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Try different approaches to get the source file
|
|
61
|
+
let sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
62
|
+
|
|
63
|
+
// If not found by full path, try by filename
|
|
64
|
+
if (!sourceFile) {
|
|
65
|
+
const fileName = filePath.split('/').pop();
|
|
66
|
+
const allFiles = this.semanticEngine.project.getSourceFiles();
|
|
67
|
+
sourceFile = allFiles.find(f => f.getBaseName() === fileName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If still not found, try to add the file
|
|
71
|
+
if (!sourceFile) {
|
|
72
|
+
try {
|
|
73
|
+
if (require('fs').existsSync(filePath)) {
|
|
74
|
+
sourceFile = this.semanticEngine.project.addSourceFileAtPath(filePath);
|
|
75
|
+
}
|
|
76
|
+
} catch (addError) {
|
|
77
|
+
// Fall through to error below
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!sourceFile) {
|
|
82
|
+
throw new Error(`Source file not found: ${filePath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.verbose) {
|
|
86
|
+
console.log(`[DEBUG] 🔍 C014: Analyzing DI violations in ${filePath.split('/').pop()}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Skip test files and excluded patterns
|
|
90
|
+
if (this.shouldSkipFile(filePath)) {
|
|
91
|
+
if (this.verbose) {
|
|
92
|
+
console.log(`[DEBUG] 🔍 C014: Skipping excluded file ${filePath.split('/').pop()}`);
|
|
93
|
+
}
|
|
94
|
+
return violations;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find all new expressions that might violate DI principles
|
|
98
|
+
const newExpressions = this.findProblematicNewExpressions(sourceFile);
|
|
99
|
+
|
|
100
|
+
for (const expr of newExpressions) {
|
|
101
|
+
if (this.isDependencyInjectionViolation(expr, sourceFile)) {
|
|
102
|
+
violations.push({
|
|
103
|
+
ruleId: 'C014',
|
|
104
|
+
message: this.buildViolationMessage(expr),
|
|
105
|
+
filePath: filePath,
|
|
106
|
+
line: expr.line,
|
|
107
|
+
column: expr.column,
|
|
108
|
+
severity: 'warning',
|
|
109
|
+
category: 'design',
|
|
110
|
+
confidence: expr.confidence,
|
|
111
|
+
suggestion: this.buildSuggestion(expr)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this.verbose) {
|
|
117
|
+
console.log(`[DEBUG] 🔍 C014: Found ${violations.length} DI violations`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return violations;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (this.verbose) {
|
|
123
|
+
console.error(`[DEBUG] ❌ C014: Symbol analysis error: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
findProblematicNewExpressions(sourceFile) {
|
|
130
|
+
const expressions = [];
|
|
131
|
+
|
|
132
|
+
function traverse(node) {
|
|
133
|
+
if (node.getKind() === SyntaxKind.NewExpression) {
|
|
134
|
+
const newExpr = node;
|
|
135
|
+
const expression = newExpr.getExpression();
|
|
136
|
+
|
|
137
|
+
// Get class name and context information
|
|
138
|
+
const className = this.getClassName(expression);
|
|
139
|
+
const position = sourceFile.getLineAndColumnAtPos(newExpr.getStart());
|
|
140
|
+
const context = this.analyzeContext(newExpr);
|
|
141
|
+
|
|
142
|
+
if (this.verbose) {
|
|
143
|
+
console.log(`[DEBUG] 🔍 C014: Found new expression: ${className} at line ${position.line}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (className) {
|
|
147
|
+
expressions.push({
|
|
148
|
+
node: newExpr,
|
|
149
|
+
className: className,
|
|
150
|
+
line: position.line,
|
|
151
|
+
column: position.column,
|
|
152
|
+
context: context,
|
|
153
|
+
confidence: this.calculateConfidence(className, context)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Traverse children
|
|
159
|
+
node.forEachChild(child => traverse.call(this, child));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
traverse.call(this, sourceFile);
|
|
163
|
+
|
|
164
|
+
if (this.verbose) {
|
|
165
|
+
console.log(`[DEBUG] 🔍 C014: Found ${expressions.length} new expressions total`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return expressions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getClassName(expression) {
|
|
172
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
173
|
+
return expression.getText();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Handle qualified names like MyNamespace.MyClass
|
|
177
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
178
|
+
return expression.getName();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
analyzeContext(newExpressionNode) {
|
|
185
|
+
const context = {
|
|
186
|
+
isInConstructor: false,
|
|
187
|
+
isAssignedToThis: false,
|
|
188
|
+
isInMethod: false,
|
|
189
|
+
isLocalVariable: false,
|
|
190
|
+
isReturnValue: false,
|
|
191
|
+
isImmediateUse: false,
|
|
192
|
+
parentFunction: null,
|
|
193
|
+
hasDecorators: false
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
let current = newExpressionNode.getParent();
|
|
197
|
+
|
|
198
|
+
while (current) {
|
|
199
|
+
switch (current.getKind()) {
|
|
200
|
+
case SyntaxKind.Constructor:
|
|
201
|
+
context.isInConstructor = true;
|
|
202
|
+
context.parentFunction = current;
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case SyntaxKind.MethodDeclaration:
|
|
206
|
+
context.isInMethod = true;
|
|
207
|
+
context.parentFunction = current;
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case SyntaxKind.BinaryExpression:
|
|
211
|
+
// Check if it's assignment to this.property
|
|
212
|
+
const binaryExpr = current;
|
|
213
|
+
if (binaryExpr.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
|
|
214
|
+
const left = binaryExpr.getLeft();
|
|
215
|
+
if (left.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
216
|
+
const propAccess = left;
|
|
217
|
+
if (propAccess.getExpression().getKind() === SyntaxKind.ThisKeyword) {
|
|
218
|
+
context.isAssignedToThis = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
case SyntaxKind.PropertyDeclaration:
|
|
225
|
+
// Check if this new expression is in a class property initializer
|
|
226
|
+
const propDecl = current;
|
|
227
|
+
const initializer = propDecl.getInitializer();
|
|
228
|
+
if (initializer && this.containsNewExpression(initializer, newExpressionNode)) {
|
|
229
|
+
context.isAssignedToThis = true; // Class property is effectively "this.property"
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case SyntaxKind.VariableDeclaration:
|
|
234
|
+
context.isLocalVariable = true;
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case SyntaxKind.ReturnStatement:
|
|
238
|
+
context.isReturnValue = true;
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case SyntaxKind.CallExpression:
|
|
242
|
+
// Check for immediate method call like new Date().getTime()
|
|
243
|
+
const callExpr = current;
|
|
244
|
+
if (callExpr.getExpression() === newExpressionNode) {
|
|
245
|
+
context.isImmediateUse = true;
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case SyntaxKind.ClassDeclaration:
|
|
250
|
+
// Check for DI decorators on the class
|
|
251
|
+
context.hasDecorators = this.hasDecorators(current, this.config.diDecorators);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
current = current.getParent();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return context;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
containsNewExpression(node, targetNewExpr) {
|
|
262
|
+
if (node === targetNewExpr) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let found = false;
|
|
267
|
+
node.forEachChild(child => {
|
|
268
|
+
if (found) return;
|
|
269
|
+
if (this.containsNewExpression(child, targetNewExpr)) {
|
|
270
|
+
found = true;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return found;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
isDependencyInjectionViolation(expr, sourceFile) {
|
|
278
|
+
const { className, context } = expr;
|
|
279
|
+
|
|
280
|
+
if (this.verbose) {
|
|
281
|
+
console.log(`[DEBUG] 🔍 C014: Checking violation for ${className}:`, {
|
|
282
|
+
isAssignedToThis: context.isAssignedToThis,
|
|
283
|
+
isInConstructor: context.isInConstructor,
|
|
284
|
+
isInMethod: context.isInMethod,
|
|
285
|
+
isLocalVariable: context.isLocalVariable,
|
|
286
|
+
isImmediateUse: context.isImmediateUse,
|
|
287
|
+
isReturnValue: context.isReturnValue,
|
|
288
|
+
hasDecorators: context.hasDecorators
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 1. Skip built-in JavaScript/DOM classes
|
|
293
|
+
if (this.config.allowedBuiltins.includes(className)) {
|
|
294
|
+
if (this.verbose) {
|
|
295
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - allowed builtin`);
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 2. Skip exception/error classes
|
|
301
|
+
if (this.isExceptionClass(className, sourceFile)) {
|
|
302
|
+
if (this.verbose) {
|
|
303
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Exception/Error class`);
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 4. Skip entity/model classes (data structures)
|
|
309
|
+
if (this.isEntityClass(className)) {
|
|
310
|
+
if (this.verbose) {
|
|
311
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Entity/Model class`);
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 5. Skip command pattern classes (value objects for operations)
|
|
317
|
+
if (this.isCommandPattern(className)) {
|
|
318
|
+
if (this.verbose) {
|
|
319
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Command pattern class`);
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 6. Skip value objects/DTOs (configurable)
|
|
325
|
+
if (this.config.allowedValueObjects.includes(className)) {
|
|
326
|
+
if (this.verbose) {
|
|
327
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - allowed value object`);
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 3. Skip if it's immediate usage (not stored as dependency)
|
|
333
|
+
if (context.isImmediateUse || context.isReturnValue) {
|
|
334
|
+
if (this.verbose) {
|
|
335
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - immediate use or return value`);
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 4. Skip Singleton pattern (self-instantiation in getInstance-like methods)
|
|
341
|
+
if (this.isSingletonPattern(className, context, sourceFile)) {
|
|
342
|
+
if (this.verbose) {
|
|
343
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Singleton pattern`);
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 5. Skip if it's a local variable in method (not dependency field) UNLESS it's infrastructure
|
|
349
|
+
if (context.isInMethod && context.isLocalVariable && !context.isAssignedToThis) {
|
|
350
|
+
// Exception: Still flag if it's infrastructure dependency even as local variable
|
|
351
|
+
if (this.isLikelyExternalDependency(className, sourceFile)) {
|
|
352
|
+
if (this.verbose) {
|
|
353
|
+
console.log(`[DEBUG] ✅ C014: ${className} is violation - infrastructure dependency even as local variable`);
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (this.verbose) {
|
|
359
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - local variable in method`);
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 6. Main heuristic: Flag if assigned to this.* (field or in constructor/method)
|
|
365
|
+
if (context.isAssignedToThis) {
|
|
366
|
+
// Check if target class suggests external dependency
|
|
367
|
+
if (this.isLikelyExternalDependency(className, sourceFile)) {
|
|
368
|
+
if (this.verbose) {
|
|
369
|
+
console.log(`[DEBUG] ✅ C014: ${className} is violation - assigned to this and external dependency`);
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 7. Skip if it's service locator pattern (centralized API client configuration)
|
|
376
|
+
if (this.isServiceLocatorPattern(context, sourceFile)) {
|
|
377
|
+
if (this.verbose) {
|
|
378
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Service locator pattern`);
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 8. Flag if class has infrastructure patterns and no DI decorators
|
|
384
|
+
if (this.hasInfraPattern(className) && !context.hasDecorators) {
|
|
385
|
+
if (this.verbose) {
|
|
386
|
+
console.log(`[DEBUG] ✅ C014: ${className} is violation - has infra pattern`);
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (this.verbose) {
|
|
392
|
+
console.log(`[DEBUG] 🔍 C014: ${className} is NOT a violation`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
isLikelyExternalDependency(className, sourceFile) {
|
|
399
|
+
if (this.verbose) {
|
|
400
|
+
console.log(`[DEBUG] 🔍 C014: Checking if ${className} is external dependency`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check if class name suggests infrastructure/external service
|
|
404
|
+
const hasInfraPattern = this.config.infraPatterns.some(pattern =>
|
|
405
|
+
className.includes(pattern)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (hasInfraPattern) {
|
|
409
|
+
if (this.verbose) {
|
|
410
|
+
console.log(`[DEBUG] ✅ C014: ${className} has infra pattern`);
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check import statements to see if it's from external module
|
|
416
|
+
const imports = sourceFile.getImportDeclarations();
|
|
417
|
+
for (const importDecl of imports) {
|
|
418
|
+
const namedImports = importDecl.getNamedImports();
|
|
419
|
+
for (const namedImport of namedImports) {
|
|
420
|
+
if (namedImport.getName() === className) {
|
|
421
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
422
|
+
if (this.verbose) {
|
|
423
|
+
console.log(`[DEBUG] 🔍 C014: ${className} imported from: ${moduleSpecifier}`);
|
|
424
|
+
}
|
|
425
|
+
// Check if imported from infrastructure/adapter paths
|
|
426
|
+
if (this.isInfrastructurePath(moduleSpecifier)) {
|
|
427
|
+
if (this.verbose) {
|
|
428
|
+
console.log(`[DEBUG] ✅ C014: ${className} from infrastructure path`);
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (this.verbose) {
|
|
437
|
+
console.log(`[DEBUG] 🔍 C014: ${className} is NOT external dependency`);
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
isInfrastructurePath(modulePath) {
|
|
443
|
+
const infraPaths = [
|
|
444
|
+
'infra', 'infrastructure', 'adapters', 'clients',
|
|
445
|
+
'repositories', 'services', 'gateways', 'providers'
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
// Check explicit infra path keywords
|
|
449
|
+
if (infraPaths.some(path => modulePath.includes(path))) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Check common external infrastructure packages
|
|
454
|
+
const infraPackages = [
|
|
455
|
+
'@aws-sdk/', 'aws-sdk', 'redis', 'mysql', 'postgresql', 'prisma',
|
|
456
|
+
'mongoose', 'sequelize', 'typeorm', 'knex', 'pg', 'mysql2',
|
|
457
|
+
's3-sync-client', 'firebase', 'googleapis', 'stripe',
|
|
458
|
+
'twilio', 'sendgrid', 'nodemailer', 'kafka', 'rabbitmq',
|
|
459
|
+
'elasticsearch', 'mongodb', 'cassandra'
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
return infraPackages.some(pkg => modulePath.includes(pkg));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
hasInfraPattern(className) {
|
|
466
|
+
return this.config.infraPatterns.some(pattern =>
|
|
467
|
+
className.includes(pattern)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
isExceptionClass(className, sourceFile) {
|
|
472
|
+
// First check by naming convention (fast path)
|
|
473
|
+
const errorPatterns = [
|
|
474
|
+
'Error', 'Exception', 'Fault', 'Failure'
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
const hasErrorName = errorPatterns.some(pattern =>
|
|
478
|
+
className.endsWith(pattern) || className.includes(pattern)
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (hasErrorName) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Check inheritance hierarchy using semantic analysis
|
|
486
|
+
if (this.semanticEngine && sourceFile) {
|
|
487
|
+
try {
|
|
488
|
+
return this.inheritsFromErrorClass(className, sourceFile);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (this.verbose) {
|
|
491
|
+
console.log(`[DEBUG] 🔍 C014: Could not check inheritance for ${className}: ${error.message}`);
|
|
492
|
+
}
|
|
493
|
+
// Fall back to name-based check only
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
inheritsFromErrorClass(className, sourceFile) {
|
|
502
|
+
// Find class declaration in current file
|
|
503
|
+
const classDecl = sourceFile.getClasses().find(cls => cls.getName() === className);
|
|
504
|
+
|
|
505
|
+
if (!classDecl) {
|
|
506
|
+
// Class might be imported, try to resolve it
|
|
507
|
+
return this.isImportedErrorClass(className, sourceFile);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Check direct inheritance
|
|
511
|
+
const extendsClauses = classDecl.getExtends();
|
|
512
|
+
if (!extendsClauses) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const baseClassName = extendsClauses.getExpression().getText();
|
|
517
|
+
|
|
518
|
+
// Check if directly extends Error-like class
|
|
519
|
+
const errorBaseClasses = [
|
|
520
|
+
'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
|
|
521
|
+
'RangeError', 'EvalError', 'URIError', 'AggregateError'
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
if (errorBaseClasses.includes(baseClassName)) {
|
|
525
|
+
if (this.verbose) {
|
|
526
|
+
console.log(`[DEBUG] 🔍 C014: ${className} extends ${baseClassName} (Error class)`);
|
|
527
|
+
}
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Recursively check inheritance chain
|
|
532
|
+
return this.inheritsFromErrorClass(baseClassName, sourceFile);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
isImportedErrorClass(className, sourceFile) {
|
|
536
|
+
// Check imports to see if className is imported from error/exception modules
|
|
537
|
+
const imports = sourceFile.getImportDeclarations();
|
|
538
|
+
|
|
539
|
+
for (const importDecl of imports) {
|
|
540
|
+
const namedImports = importDecl.getNamedImports();
|
|
541
|
+
for (const namedImport of namedImports) {
|
|
542
|
+
if (namedImport.getName() === className) {
|
|
543
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
544
|
+
|
|
545
|
+
// Check if imported from error/exception related modules
|
|
546
|
+
const errorModulePatterns = [
|
|
547
|
+
'error', 'exception', 'http-exception', 'custom-error',
|
|
548
|
+
'../exceptions', './exceptions', '/errors/', '/exceptions/'
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
const isFromErrorModule = errorModulePatterns.some(pattern =>
|
|
552
|
+
moduleSpecifier.toLowerCase().includes(pattern)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (isFromErrorModule) {
|
|
556
|
+
if (this.verbose) {
|
|
557
|
+
console.log(`[DEBUG] 🔍 C014: ${className} imported from error module: ${moduleSpecifier}`);
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
isEntityClass(className) {
|
|
569
|
+
// Common entity/model class patterns
|
|
570
|
+
const entityPatterns = [
|
|
571
|
+
'Entity', 'Model', 'Schema', 'Document', 'Dto', 'DTO'
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
return entityPatterns.some(pattern =>
|
|
575
|
+
className.endsWith(pattern)
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
isCommandPattern(className) {
|
|
580
|
+
// Command pattern classes (value objects for operations)
|
|
581
|
+
const commandPatterns = [
|
|
582
|
+
'Command', 'Request', 'Query', 'Operation',
|
|
583
|
+
'Action', 'Task', 'Job'
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
return commandPatterns.some(pattern =>
|
|
587
|
+
className.endsWith(pattern)
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
isServiceLocatorPattern(context, sourceFile) {
|
|
592
|
+
// Check if we're in an object literal assignment that looks like service locator
|
|
593
|
+
if (!context.isLocalVariable) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Additional check: if file contains many similar API instantiations,
|
|
598
|
+
// it's likely a service locator pattern
|
|
599
|
+
const fileText = sourceFile.getFullText();
|
|
600
|
+
const newExpressionCount = (fileText.match(/new \w+Api\(\)/g) || []).length;
|
|
601
|
+
if (newExpressionCount >= 5) {
|
|
602
|
+
// Many API instantiations suggest service locator pattern
|
|
603
|
+
if (this.verbose) {
|
|
604
|
+
console.log(`[DEBUG] 🔍 C014: Found ${newExpressionCount} API instantiations - likely service locator`);
|
|
605
|
+
}
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Check for service locator variable names in file text
|
|
610
|
+
const serviceLocatorPatterns = [
|
|
611
|
+
'apiClient', 'serviceContainer', 'container', 'services',
|
|
612
|
+
'clients', 'providers', 'factories', 'registry'
|
|
613
|
+
];
|
|
614
|
+
|
|
615
|
+
const isServiceLocator = serviceLocatorPatterns.some(pattern =>
|
|
616
|
+
fileText.includes(`export const ${pattern}`) ||
|
|
617
|
+
fileText.includes(`const ${pattern}`)
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
if (isServiceLocator && this.verbose) {
|
|
621
|
+
console.log(`[DEBUG] 🔍 C014: Detected service locator pattern from variable name`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return isServiceLocator;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
hasDecorators(node, decoratorNames) {
|
|
628
|
+
const decorators = node.getDecorators?.() || [];
|
|
629
|
+
return decorators.some(decorator => {
|
|
630
|
+
const decoratorText = decorator.getText();
|
|
631
|
+
return decoratorNames.some(name => decoratorText.includes(name));
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
shouldSkipFile(filePath) {
|
|
636
|
+
return this.config.excludePatterns.some(pattern => {
|
|
637
|
+
const regex = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
|
|
638
|
+
return new RegExp(regex).test(filePath);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Check if this is a Singleton pattern (self-instantiation)
|
|
644
|
+
*/
|
|
645
|
+
isSingletonPattern(className, context, sourceFile) {
|
|
646
|
+
// Must be in a method (not constructor)
|
|
647
|
+
if (!context.isInMethod || context.isInConstructor) {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Must be instantiating the same class we're in
|
|
652
|
+
const classDeclaration = this.findContainingClass(context, sourceFile);
|
|
653
|
+
if (!classDeclaration) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const currentClassName = classDeclaration.getName();
|
|
658
|
+
if (currentClassName !== className) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Method name should suggest singleton (getInstance, instance, create, etc.)
|
|
663
|
+
const methodName = context.parentFunction?.getName?.() || '';
|
|
664
|
+
const singletonMethods = [
|
|
665
|
+
'getInstance', 'instance', 'getinstance', 'create', 'createInstance',
|
|
666
|
+
'singleton', 'getSingleton', 'getSharedInstance', 'shared'
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
const isSingletonMethod = singletonMethods.some(pattern =>
|
|
670
|
+
methodName.toLowerCase().includes(pattern.toLowerCase())
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
// Must be a static method
|
|
674
|
+
const isStaticMethod = context.parentFunction?.getModifiers?.()
|
|
675
|
+
?.some(modifier => modifier.getKind() === SyntaxKind.StaticKeyword) || false;
|
|
676
|
+
|
|
677
|
+
if (this.verbose && isSingletonMethod && isStaticMethod) {
|
|
678
|
+
console.log(`[DEBUG] 🔍 C014: Detected Singleton pattern: ${currentClassName}.${methodName}()`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return isSingletonMethod && isStaticMethod;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Find the containing class declaration
|
|
686
|
+
*/
|
|
687
|
+
findContainingClass(context, sourceFile) {
|
|
688
|
+
let current = context.parentFunction?.getParent();
|
|
689
|
+
|
|
690
|
+
while (current) {
|
|
691
|
+
if (current.getKind() === SyntaxKind.ClassDeclaration) {
|
|
692
|
+
return current;
|
|
693
|
+
}
|
|
694
|
+
current = current.getParent();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
calculateConfidence(className, context) {
|
|
701
|
+
let confidence = 0.6; // Base confidence
|
|
702
|
+
|
|
703
|
+
// Increase confidence for infrastructure patterns
|
|
704
|
+
if (this.hasInfraPattern(className)) {
|
|
705
|
+
confidence += 0.2;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Increase confidence if assigned to this.* (dependency field)
|
|
709
|
+
if (context.isAssignedToThis) {
|
|
710
|
+
confidence += 0.2;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Decrease confidence for value objects
|
|
714
|
+
if (this.config.allowedValueObjects.includes(className)) {
|
|
715
|
+
confidence -= 0.3;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Decrease confidence if has DI decorators
|
|
719
|
+
if (context.hasDecorators) {
|
|
720
|
+
confidence -= 0.4;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return Math.max(0.3, Math.min(1.0, confidence));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
buildViolationMessage(expr) {
|
|
727
|
+
const { className, context } = expr;
|
|
728
|
+
|
|
729
|
+
if (context.isInConstructor && context.isAssignedToThis) {
|
|
730
|
+
return `Direct instantiation of '${className}' in constructor. Consider injecting this dependency instead of creating it directly.`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (context.isInMethod && context.isAssignedToThis) {
|
|
734
|
+
return `Direct instantiation of '${className}' assigned to instance field. Consider injecting this dependency.`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return `Direct instantiation of '${className}'. Consider using dependency injection or factory pattern.`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
buildSuggestion(expr) {
|
|
741
|
+
const { className, context } = expr;
|
|
742
|
+
|
|
743
|
+
if (context.isInConstructor) {
|
|
744
|
+
return `Inject ${className} via constructor parameter: constructor(private ${className.toLowerCase()}: ${className})`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return `Consider injecting ${className} as a dependency or using a factory pattern`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
module.exports = C014SymbolBasedAnalyzer;
|