@sun-asterisk/sunlint 1.3.18 → 1.3.19
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/rules/enhanced-rules-registry.json +77 -18
- package/core/cli-program.js +2 -1
- package/core/github-annotate-service.js +89 -0
- package/core/output-service.js +25 -0
- package/core/summary-report-service.js +30 -30
- package/package.json +3 -2
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -5,7 +5,7 @@ class C014SymbolBasedAnalyzer {
|
|
|
5
5
|
constructor(semanticEngine = null) {
|
|
6
6
|
this.semanticEngine = semanticEngine;
|
|
7
7
|
this.verbose = false;
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
// Configuration
|
|
10
10
|
this.config = {
|
|
11
11
|
// Built-in classes that are allowed
|
|
@@ -17,27 +17,34 @@ class C014SymbolBasedAnalyzer {
|
|
|
17
17
|
'TextEncoder', 'TextDecoder', 'MessageChannel', 'MessagePort',
|
|
18
18
|
'Worker', 'SharedWorker', 'EventSource', 'WebSocket'
|
|
19
19
|
],
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
// Value objects/DTOs that are typically safe to instantiate
|
|
22
22
|
allowedValueObjects: [
|
|
23
23
|
'Money', 'Price', 'Currency', 'Quantity', 'Amount',
|
|
24
24
|
'Email', 'Phone', 'Address', 'Name', 'Id', 'UserId',
|
|
25
25
|
'UUID', 'Timestamp', 'Duration', 'Range'
|
|
26
26
|
],
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
// Query builder and utility classes that are safe to instantiate
|
|
29
|
+
allowedQueryBuilders: [
|
|
30
|
+
'Brackets', 'SelectQueryBuilder', 'QueryBuilder',
|
|
31
|
+
'WhereExpression', 'OrderByCondition', 'JoinAttribute',
|
|
32
|
+
'Subquery', 'CTE', 'Raw'
|
|
33
|
+
],
|
|
34
|
+
|
|
28
35
|
// Infrastructure patterns that suggest external dependencies
|
|
29
36
|
infraPatterns: [
|
|
30
37
|
'Client', 'Repository', 'Service', 'Gateway', 'Adapter',
|
|
31
38
|
'Provider', 'Factory', 'Builder', 'Manager', 'Handler',
|
|
32
39
|
'Controller', 'Processor', 'Validator', 'Logger'
|
|
33
40
|
],
|
|
34
|
-
|
|
41
|
+
|
|
35
42
|
// DI decorators that indicate proper injection
|
|
36
43
|
diDecorators: [
|
|
37
44
|
'Injectable', 'Inject', 'Autowired', 'Component',
|
|
38
45
|
'Service', 'Repository', 'Controller', 'autoInjectable'
|
|
39
46
|
],
|
|
40
|
-
|
|
47
|
+
|
|
41
48
|
// Patterns to exclude from analysis
|
|
42
49
|
excludePatterns: [
|
|
43
50
|
'**/*.test.ts', '**/*.spec.ts', '**/*.test.js', '**/*.spec.js',
|
|
@@ -55,29 +62,37 @@ class C014SymbolBasedAnalyzer {
|
|
|
55
62
|
|
|
56
63
|
async analyzeFileBasic(filePath, options = {}) {
|
|
57
64
|
const violations = [];
|
|
58
|
-
|
|
65
|
+
|
|
59
66
|
try {
|
|
67
|
+
// Check if semantic engine is initialized
|
|
68
|
+
if (!this.semanticEngine || !this.semanticEngine.project) {
|
|
69
|
+
throw new Error('Semantic engine not initialized');
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
// Try different approaches to get the source file
|
|
61
73
|
let sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
62
|
-
|
|
74
|
+
|
|
63
75
|
// If not found by full path, try by filename
|
|
64
76
|
if (!sourceFile) {
|
|
65
77
|
const fileName = filePath.split('/').pop();
|
|
66
78
|
const allFiles = this.semanticEngine.project.getSourceFiles();
|
|
67
79
|
sourceFile = allFiles.find(f => f.getBaseName() === fileName);
|
|
68
80
|
}
|
|
69
|
-
|
|
81
|
+
|
|
70
82
|
// If still not found, try to add the file
|
|
71
83
|
if (!sourceFile) {
|
|
72
84
|
try {
|
|
73
|
-
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
if (fs.existsSync(filePath)) {
|
|
74
87
|
sourceFile = this.semanticEngine.project.addSourceFileAtPath(filePath);
|
|
75
88
|
}
|
|
76
89
|
} catch (addError) {
|
|
77
|
-
|
|
90
|
+
if (this.verbose) {
|
|
91
|
+
console.error(`[DEBUG] ❌ C014: Failed to add source file: ${addError.message}`);
|
|
92
|
+
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
|
-
|
|
95
|
+
|
|
81
96
|
if (!sourceFile) {
|
|
82
97
|
throw new Error(`Source file not found: ${filePath}`);
|
|
83
98
|
}
|
|
@@ -96,7 +111,7 @@ class C014SymbolBasedAnalyzer {
|
|
|
96
111
|
|
|
97
112
|
// Find all new expressions that might violate DI principles
|
|
98
113
|
const newExpressions = this.findProblematicNewExpressions(sourceFile);
|
|
99
|
-
|
|
114
|
+
|
|
100
115
|
for (const expr of newExpressions) {
|
|
101
116
|
if (this.isDependencyInjectionViolation(expr, sourceFile)) {
|
|
102
117
|
violations.push({
|
|
@@ -128,21 +143,21 @@ class C014SymbolBasedAnalyzer {
|
|
|
128
143
|
|
|
129
144
|
findProblematicNewExpressions(sourceFile) {
|
|
130
145
|
const expressions = [];
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
|
|
147
|
+
const traverse = (node) => {
|
|
133
148
|
if (node.getKind() === SyntaxKind.NewExpression) {
|
|
134
149
|
const newExpr = node;
|
|
135
150
|
const expression = newExpr.getExpression();
|
|
136
|
-
|
|
151
|
+
|
|
137
152
|
// Get class name and context information
|
|
138
153
|
const className = this.getClassName(expression);
|
|
139
154
|
const position = sourceFile.getLineAndColumnAtPos(newExpr.getStart());
|
|
140
155
|
const context = this.analyzeContext(newExpr);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
|
|
157
|
+
if (this.verbose) {
|
|
158
|
+
console.log(`[DEBUG] 🔍 C014: Found new expression: ${className} at line ${position.line}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
if (className) {
|
|
147
162
|
expressions.push({
|
|
148
163
|
node: newExpr,
|
|
@@ -156,28 +171,34 @@ class C014SymbolBasedAnalyzer {
|
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
// Traverse children
|
|
159
|
-
node.forEachChild(child => traverse
|
|
160
|
-
}
|
|
174
|
+
node.forEachChild(child => traverse(child));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
traverse(sourceFile);
|
|
161
178
|
|
|
162
|
-
traverse.call(this, sourceFile);
|
|
163
|
-
|
|
164
179
|
if (this.verbose) {
|
|
165
180
|
console.log(`[DEBUG] 🔍 C014: Found ${expressions.length} new expressions total`);
|
|
166
181
|
}
|
|
167
|
-
|
|
182
|
+
|
|
168
183
|
return expressions;
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
getClassName(expression) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
try {
|
|
188
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
189
|
+
return expression.getText();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle qualified names like MyNamespace.MyClass
|
|
193
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
194
|
+
return expression.getName();
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (this.verbose) {
|
|
198
|
+
console.error(`[DEBUG] ❌ C014: Error getting class name: ${error.message}`);
|
|
199
|
+
}
|
|
179
200
|
}
|
|
180
|
-
|
|
201
|
+
|
|
181
202
|
return null;
|
|
182
203
|
}
|
|
183
204
|
|
|
@@ -189,70 +210,86 @@ class C014SymbolBasedAnalyzer {
|
|
|
189
210
|
isLocalVariable: false,
|
|
190
211
|
isReturnValue: false,
|
|
191
212
|
isImmediateUse: false,
|
|
213
|
+
isCallArgument: false,
|
|
192
214
|
parentFunction: null,
|
|
193
215
|
hasDecorators: false
|
|
194
216
|
};
|
|
195
217
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
try {
|
|
219
|
+
let current = newExpressionNode.getParent();
|
|
220
|
+
|
|
221
|
+
while (current) {
|
|
222
|
+
switch (current.getKind()) {
|
|
223
|
+
case SyntaxKind.Constructor:
|
|
224
|
+
context.isInConstructor = true;
|
|
225
|
+
context.parentFunction = current;
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case SyntaxKind.MethodDeclaration:
|
|
229
|
+
context.isInMethod = true;
|
|
230
|
+
context.parentFunction = current;
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case SyntaxKind.BinaryExpression:
|
|
234
|
+
// Check if it's assignment to this.property
|
|
235
|
+
const binaryExpr = current;
|
|
236
|
+
if (binaryExpr.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
|
|
237
|
+
const left = binaryExpr.getLeft();
|
|
238
|
+
if (left.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
239
|
+
const propAccess = left;
|
|
240
|
+
if (propAccess.getExpression().getKind() === SyntaxKind.ThisKeyword) {
|
|
241
|
+
context.isAssignedToThis = true;
|
|
242
|
+
}
|
|
219
243
|
}
|
|
220
244
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case SyntaxKind.PropertyDeclaration:
|
|
248
|
+
// Check if this new expression is in a class property initializer
|
|
249
|
+
const propDecl = current;
|
|
250
|
+
const initializer = propDecl.getInitializer();
|
|
251
|
+
if (initializer && this.containsNewExpression(initializer, newExpressionNode)) {
|
|
252
|
+
context.isAssignedToThis = true; // Class property is effectively "this.property"
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case SyntaxKind.VariableDeclaration:
|
|
257
|
+
context.isLocalVariable = true;
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case SyntaxKind.ReturnStatement:
|
|
261
|
+
context.isReturnValue = true;
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case SyntaxKind.CallExpression:
|
|
265
|
+
const callExpr = current;
|
|
266
|
+
// Check for immediate method call like new Date().getTime()
|
|
267
|
+
if (callExpr.getExpression() === newExpressionNode) {
|
|
268
|
+
context.isImmediateUse = true;
|
|
269
|
+
} else {
|
|
270
|
+
// Check if new expression is passed as argument to a function call
|
|
271
|
+
const args = callExpr.getArguments();
|
|
272
|
+
for (const arg of args) {
|
|
273
|
+
if (this.containsNewExpression(arg, newExpressionNode)) {
|
|
274
|
+
context.isCallArgument = true;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case SyntaxKind.ClassDeclaration:
|
|
282
|
+
// Check for DI decorators on the class
|
|
283
|
+
context.hasDecorators = this.hasDecorators(current, this.config.diDecorators);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
current = current.getParent();
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (this.verbose) {
|
|
291
|
+
console.error(`[DEBUG] ❌ C014: Error analyzing context: ${error.message}`);
|
|
253
292
|
}
|
|
254
|
-
|
|
255
|
-
current = current.getParent();
|
|
256
293
|
}
|
|
257
294
|
|
|
258
295
|
return context;
|
|
@@ -262,15 +299,19 @@ class C014SymbolBasedAnalyzer {
|
|
|
262
299
|
if (node === targetNewExpr) {
|
|
263
300
|
return true;
|
|
264
301
|
}
|
|
265
|
-
|
|
302
|
+
|
|
266
303
|
let found = false;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
304
|
+
try {
|
|
305
|
+
node.forEachChild(child => {
|
|
306
|
+
if (found) return;
|
|
307
|
+
if (this.containsNewExpression(child, targetNewExpr)) {
|
|
308
|
+
found = true;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
// Silently handle errors in tree traversal
|
|
313
|
+
}
|
|
314
|
+
|
|
274
315
|
return found;
|
|
275
316
|
}
|
|
276
317
|
|
|
@@ -285,6 +326,7 @@ class C014SymbolBasedAnalyzer {
|
|
|
285
326
|
isLocalVariable: context.isLocalVariable,
|
|
286
327
|
isImmediateUse: context.isImmediateUse,
|
|
287
328
|
isReturnValue: context.isReturnValue,
|
|
329
|
+
isCallArgument: context.isCallArgument,
|
|
288
330
|
hasDecorators: context.hasDecorators
|
|
289
331
|
});
|
|
290
332
|
}
|
|
@@ -297,7 +339,23 @@ class C014SymbolBasedAnalyzer {
|
|
|
297
339
|
return false;
|
|
298
340
|
}
|
|
299
341
|
|
|
300
|
-
// 2. Skip
|
|
342
|
+
// 2. Skip query builder classes (TypeORM Brackets, etc.)
|
|
343
|
+
if (this.config.allowedQueryBuilders.includes(className)) {
|
|
344
|
+
if (this.verbose) {
|
|
345
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - query builder class`);
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 3. Skip if it's passed as an argument to a function (like query builder methods)
|
|
351
|
+
if (context.isCallArgument) {
|
|
352
|
+
if (this.verbose) {
|
|
353
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - passed as function argument`);
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 4. Skip exception/error classes
|
|
301
359
|
if (this.isExceptionClass(className, sourceFile)) {
|
|
302
360
|
if (this.verbose) {
|
|
303
361
|
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Exception/Error class`);
|
|
@@ -305,7 +363,23 @@ class C014SymbolBasedAnalyzer {
|
|
|
305
363
|
return false;
|
|
306
364
|
}
|
|
307
365
|
|
|
308
|
-
//
|
|
366
|
+
// 5. Skip if it's immediate usage (not stored as dependency)
|
|
367
|
+
if (context.isImmediateUse || context.isReturnValue) {
|
|
368
|
+
if (this.verbose) {
|
|
369
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - immediate use or return value`);
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 6. Skip Singleton pattern (self-instantiation in getInstance-like methods)
|
|
375
|
+
if (this.isSingletonPattern(className, context, sourceFile)) {
|
|
376
|
+
if (this.verbose) {
|
|
377
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Singleton pattern`);
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 7. Skip entity/model classes (data structures)
|
|
309
383
|
if (this.isEntityClass(className)) {
|
|
310
384
|
if (this.verbose) {
|
|
311
385
|
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Entity/Model class`);
|
|
@@ -313,7 +387,7 @@ class C014SymbolBasedAnalyzer {
|
|
|
313
387
|
return false;
|
|
314
388
|
}
|
|
315
389
|
|
|
316
|
-
//
|
|
390
|
+
// 8. Skip command pattern classes (value objects for operations)
|
|
317
391
|
if (this.isCommandPattern(className)) {
|
|
318
392
|
if (this.verbose) {
|
|
319
393
|
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Command pattern class`);
|
|
@@ -321,7 +395,7 @@ class C014SymbolBasedAnalyzer {
|
|
|
321
395
|
return false;
|
|
322
396
|
}
|
|
323
397
|
|
|
324
|
-
//
|
|
398
|
+
// 9. Skip value objects/DTOs (configurable)
|
|
325
399
|
if (this.config.allowedValueObjects.includes(className)) {
|
|
326
400
|
if (this.verbose) {
|
|
327
401
|
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - allowed value object`);
|
|
@@ -329,23 +403,15 @@ class C014SymbolBasedAnalyzer {
|
|
|
329
403
|
return false;
|
|
330
404
|
}
|
|
331
405
|
|
|
332
|
-
//
|
|
333
|
-
if (context
|
|
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)) {
|
|
406
|
+
// 10. Skip service locator pattern (centralized API client configuration)
|
|
407
|
+
if (this.isServiceLocatorPattern(context, sourceFile)) {
|
|
342
408
|
if (this.verbose) {
|
|
343
|
-
console.log(`[DEBUG] 🔍 C014: Skipping ${className} -
|
|
409
|
+
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Service locator pattern`);
|
|
344
410
|
}
|
|
345
411
|
return false;
|
|
346
412
|
}
|
|
347
413
|
|
|
348
|
-
//
|
|
414
|
+
// 11. Skip if it's a local variable in method (not dependency field) UNLESS it's infrastructure
|
|
349
415
|
if (context.isInMethod && context.isLocalVariable && !context.isAssignedToThis) {
|
|
350
416
|
// Exception: Still flag if it's infrastructure dependency even as local variable
|
|
351
417
|
if (this.isLikelyExternalDependency(className, sourceFile)) {
|
|
@@ -354,14 +420,14 @@ class C014SymbolBasedAnalyzer {
|
|
|
354
420
|
}
|
|
355
421
|
return true;
|
|
356
422
|
}
|
|
357
|
-
|
|
423
|
+
|
|
358
424
|
if (this.verbose) {
|
|
359
425
|
console.log(`[DEBUG] 🔍 C014: Skipping ${className} - local variable in method`);
|
|
360
426
|
}
|
|
361
427
|
return false;
|
|
362
428
|
}
|
|
363
429
|
|
|
364
|
-
//
|
|
430
|
+
// 12. Main heuristic: Flag if assigned to this.* (field or in constructor/method)
|
|
365
431
|
if (context.isAssignedToThis) {
|
|
366
432
|
// Check if target class suggests external dependency
|
|
367
433
|
if (this.isLikelyExternalDependency(className, sourceFile)) {
|
|
@@ -372,16 +438,8 @@ class C014SymbolBasedAnalyzer {
|
|
|
372
438
|
}
|
|
373
439
|
}
|
|
374
440
|
|
|
375
|
-
//
|
|
376
|
-
if (this.
|
|
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) {
|
|
441
|
+
// 13. Flag if class has infrastructure patterns and no DI decorators
|
|
442
|
+
if (this.hasInfraPattern(className) && !context.hasDecorators && context.isAssignedToThis) {
|
|
385
443
|
if (this.verbose) {
|
|
386
444
|
console.log(`[DEBUG] ✅ C014: ${className} is violation - has infra pattern`);
|
|
387
445
|
}
|
|
@@ -399,9 +457,9 @@ class C014SymbolBasedAnalyzer {
|
|
|
399
457
|
if (this.verbose) {
|
|
400
458
|
console.log(`[DEBUG] 🔍 C014: Checking if ${className} is external dependency`);
|
|
401
459
|
}
|
|
402
|
-
|
|
460
|
+
|
|
403
461
|
// Check if class name suggests infrastructure/external service
|
|
404
|
-
const hasInfraPattern = this.config.infraPatterns.some(pattern =>
|
|
462
|
+
const hasInfraPattern = this.config.infraPatterns.some(pattern =>
|
|
405
463
|
className.includes(pattern)
|
|
406
464
|
);
|
|
407
465
|
|
|
@@ -413,24 +471,30 @@ class C014SymbolBasedAnalyzer {
|
|
|
413
471
|
}
|
|
414
472
|
|
|
415
473
|
// Check import statements to see if it's from external module
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
console.log(`[DEBUG] 🔍 C014: ${className} imported from: ${moduleSpecifier}`);
|
|
424
|
-
}
|
|
425
|
-
// Check if imported from infrastructure/adapter paths
|
|
426
|
-
if (this.isInfrastructurePath(moduleSpecifier)) {
|
|
474
|
+
try {
|
|
475
|
+
const imports = sourceFile.getImportDeclarations();
|
|
476
|
+
for (const importDecl of imports) {
|
|
477
|
+
const namedImports = importDecl.getNamedImports();
|
|
478
|
+
for (const namedImport of namedImports) {
|
|
479
|
+
if (namedImport.getName() === className) {
|
|
480
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
427
481
|
if (this.verbose) {
|
|
428
|
-
console.log(`[DEBUG]
|
|
482
|
+
console.log(`[DEBUG] 🔍 C014: ${className} imported from: ${moduleSpecifier}`);
|
|
483
|
+
}
|
|
484
|
+
// Check if imported from infrastructure/adapter paths
|
|
485
|
+
if (this.isInfrastructurePath(moduleSpecifier)) {
|
|
486
|
+
if (this.verbose) {
|
|
487
|
+
console.log(`[DEBUG] ✅ C014: ${className} from infrastructure path`);
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
429
490
|
}
|
|
430
|
-
return true;
|
|
431
491
|
}
|
|
432
492
|
}
|
|
433
493
|
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (this.verbose) {
|
|
496
|
+
console.error(`[DEBUG] ❌ C014: Error checking imports: ${error.message}`);
|
|
497
|
+
}
|
|
434
498
|
}
|
|
435
499
|
|
|
436
500
|
if (this.verbose) {
|
|
@@ -441,15 +505,15 @@ class C014SymbolBasedAnalyzer {
|
|
|
441
505
|
|
|
442
506
|
isInfrastructurePath(modulePath) {
|
|
443
507
|
const infraPaths = [
|
|
444
|
-
'infra', 'infrastructure', 'adapters', 'clients',
|
|
508
|
+
'infra', 'infrastructure', 'adapters', 'clients',
|
|
445
509
|
'repositories', 'services', 'gateways', 'providers'
|
|
446
510
|
];
|
|
447
|
-
|
|
511
|
+
|
|
448
512
|
// Check explicit infra path keywords
|
|
449
513
|
if (infraPaths.some(path => modulePath.includes(path))) {
|
|
450
514
|
return true;
|
|
451
515
|
}
|
|
452
|
-
|
|
516
|
+
|
|
453
517
|
// Check common external infrastructure packages
|
|
454
518
|
const infraPackages = [
|
|
455
519
|
'@aws-sdk/', 'aws-sdk', 'redis', 'mysql', 'postgresql', 'prisma',
|
|
@@ -458,12 +522,12 @@ class C014SymbolBasedAnalyzer {
|
|
|
458
522
|
'twilio', 'sendgrid', 'nodemailer', 'kafka', 'rabbitmq',
|
|
459
523
|
'elasticsearch', 'mongodb', 'cassandra'
|
|
460
524
|
];
|
|
461
|
-
|
|
525
|
+
|
|
462
526
|
return infraPackages.some(pkg => modulePath.includes(pkg));
|
|
463
527
|
}
|
|
464
528
|
|
|
465
529
|
hasInfraPattern(className) {
|
|
466
|
-
return this.config.infraPatterns.some(pattern =>
|
|
530
|
+
return this.config.infraPatterns.some(pattern =>
|
|
467
531
|
className.includes(pattern)
|
|
468
532
|
);
|
|
469
533
|
}
|
|
@@ -473,11 +537,11 @@ class C014SymbolBasedAnalyzer {
|
|
|
473
537
|
const errorPatterns = [
|
|
474
538
|
'Error', 'Exception', 'Fault', 'Failure'
|
|
475
539
|
];
|
|
476
|
-
|
|
477
|
-
const hasErrorName = errorPatterns.some(pattern =>
|
|
540
|
+
|
|
541
|
+
const hasErrorName = errorPatterns.some(pattern =>
|
|
478
542
|
className.endsWith(pattern) || className.includes(pattern)
|
|
479
543
|
);
|
|
480
|
-
|
|
544
|
+
|
|
481
545
|
if (hasErrorName) {
|
|
482
546
|
return true;
|
|
483
547
|
}
|
|
@@ -494,74 +558,87 @@ class C014SymbolBasedAnalyzer {
|
|
|
494
558
|
return false;
|
|
495
559
|
}
|
|
496
560
|
}
|
|
497
|
-
|
|
561
|
+
|
|
498
562
|
return false;
|
|
499
563
|
}
|
|
500
564
|
|
|
501
565
|
inheritsFromErrorClass(className, sourceFile) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (!classDecl) {
|
|
506
|
-
// Class might be imported, try to resolve it
|
|
507
|
-
return this.isImportedErrorClass(className, sourceFile);
|
|
508
|
-
}
|
|
566
|
+
try {
|
|
567
|
+
// Find class declaration in current file
|
|
568
|
+
const classDecl = sourceFile.getClasses().find(cls => cls.getName() === className);
|
|
509
569
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
570
|
+
if (!classDecl) {
|
|
571
|
+
// Class might be imported, try to resolve it
|
|
572
|
+
return this.isImportedErrorClass(className, sourceFile);
|
|
573
|
+
}
|
|
515
574
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
575
|
+
// Check direct inheritance
|
|
576
|
+
const extendsClauses = classDecl.getExtends();
|
|
577
|
+
if (!extendsClauses) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const baseClassName = extendsClauses.getExpression().getText();
|
|
582
|
+
|
|
583
|
+
// Check if directly extends Error-like class
|
|
584
|
+
const errorBaseClasses = [
|
|
585
|
+
'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
|
|
586
|
+
'RangeError', 'EvalError', 'URIError', 'AggregateError'
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
if (errorBaseClasses.includes(baseClassName)) {
|
|
590
|
+
if (this.verbose) {
|
|
591
|
+
console.log(`[DEBUG] 🔍 C014: ${className} extends ${baseClassName} (Error class)`);
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Recursively check inheritance chain
|
|
597
|
+
return this.inheritsFromErrorClass(baseClassName, sourceFile);
|
|
598
|
+
} catch (error) {
|
|
525
599
|
if (this.verbose) {
|
|
526
|
-
console.
|
|
600
|
+
console.error(`[DEBUG] ❌ C014: Error checking inheritance: ${error.message}`);
|
|
527
601
|
}
|
|
528
|
-
return
|
|
602
|
+
return false;
|
|
529
603
|
}
|
|
530
|
-
|
|
531
|
-
// Recursively check inheritance chain
|
|
532
|
-
return this.inheritsFromErrorClass(baseClassName, sourceFile);
|
|
533
604
|
}
|
|
534
605
|
|
|
535
606
|
isImportedErrorClass(className, sourceFile) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
|
|
607
|
+
try {
|
|
608
|
+
// Check imports to see if className is imported from error/exception modules
|
|
609
|
+
const imports = sourceFile.getImportDeclarations();
|
|
610
|
+
|
|
611
|
+
for (const importDecl of imports) {
|
|
612
|
+
const namedImports = importDecl.getNamedImports();
|
|
613
|
+
for (const namedImport of namedImports) {
|
|
614
|
+
if (namedImport.getName() === className) {
|
|
615
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
616
|
+
|
|
617
|
+
// Check if imported from error/exception related modules
|
|
618
|
+
const errorModulePatterns = [
|
|
619
|
+
'error', 'exception', 'http-exception', 'custom-error',
|
|
620
|
+
'../exceptions', './exceptions', '/errors/', '/exceptions/'
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
const isFromErrorModule = errorModulePatterns.some(pattern =>
|
|
624
|
+
moduleSpecifier.toLowerCase().includes(pattern)
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
if (isFromErrorModule) {
|
|
628
|
+
if (this.verbose) {
|
|
629
|
+
console.log(`[DEBUG] 🔍 C014: ${className} imported from error module: ${moduleSpecifier}`);
|
|
630
|
+
}
|
|
631
|
+
return true;
|
|
558
632
|
}
|
|
559
|
-
return true;
|
|
560
633
|
}
|
|
561
634
|
}
|
|
562
635
|
}
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (this.verbose) {
|
|
638
|
+
console.error(`[DEBUG] ❌ C014: Error checking imported error class: ${error.message}`);
|
|
639
|
+
}
|
|
563
640
|
}
|
|
564
|
-
|
|
641
|
+
|
|
565
642
|
return false;
|
|
566
643
|
}
|
|
567
644
|
|
|
@@ -570,8 +647,8 @@ class C014SymbolBasedAnalyzer {
|
|
|
570
647
|
const entityPatterns = [
|
|
571
648
|
'Entity', 'Model', 'Schema', 'Document', 'Dto', 'DTO'
|
|
572
649
|
];
|
|
573
|
-
|
|
574
|
-
return entityPatterns.some(pattern =>
|
|
650
|
+
|
|
651
|
+
return entityPatterns.some(pattern =>
|
|
575
652
|
className.endsWith(pattern)
|
|
576
653
|
);
|
|
577
654
|
}
|
|
@@ -579,57 +656,71 @@ class C014SymbolBasedAnalyzer {
|
|
|
579
656
|
isCommandPattern(className) {
|
|
580
657
|
// Command pattern classes (value objects for operations)
|
|
581
658
|
const commandPatterns = [
|
|
582
|
-
'Command', 'Request', 'Query', 'Operation',
|
|
659
|
+
'Command', 'Request', 'Query', 'Operation',
|
|
583
660
|
'Action', 'Task', 'Job'
|
|
584
661
|
];
|
|
585
|
-
|
|
586
|
-
return commandPatterns.some(pattern =>
|
|
662
|
+
|
|
663
|
+
return commandPatterns.some(pattern =>
|
|
587
664
|
className.endsWith(pattern)
|
|
588
665
|
);
|
|
589
666
|
}
|
|
590
667
|
|
|
591
668
|
isServiceLocatorPattern(context, sourceFile) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
669
|
+
try {
|
|
670
|
+
// Check if we're in an object literal assignment that looks like service locator
|
|
671
|
+
if (!context.isLocalVariable) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
596
674
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
675
|
+
// Additional check: if file contains many similar API instantiations,
|
|
676
|
+
// it's likely a service locator pattern
|
|
677
|
+
const fileText = sourceFile.getFullText();
|
|
678
|
+
const newExpressionCount = (fileText.match(/new \w+Api\(\)/g) || []).length;
|
|
679
|
+
if (newExpressionCount >= 5) {
|
|
680
|
+
// Many API instantiations suggest service locator pattern
|
|
681
|
+
if (this.verbose) {
|
|
682
|
+
console.log(`[DEBUG] 🔍 C014: Found ${newExpressionCount} API instantiations - likely service locator`);
|
|
683
|
+
}
|
|
684
|
+
return true;
|
|
605
685
|
}
|
|
606
|
-
return true;
|
|
607
|
-
}
|
|
608
686
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
687
|
+
// Check for service locator variable names in file text
|
|
688
|
+
const serviceLocatorPatterns = [
|
|
689
|
+
'apiClient', 'serviceContainer', 'container', 'services',
|
|
690
|
+
'clients', 'providers', 'factories', 'registry'
|
|
691
|
+
];
|
|
614
692
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
693
|
+
const isServiceLocator = serviceLocatorPatterns.some(pattern =>
|
|
694
|
+
fileText.includes(`export const ${pattern}`) ||
|
|
695
|
+
fileText.includes(`const ${pattern}`)
|
|
696
|
+
);
|
|
619
697
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
698
|
+
if (isServiceLocator && this.verbose) {
|
|
699
|
+
console.log(`[DEBUG] 🔍 C014: Detected service locator pattern from variable name`);
|
|
700
|
+
}
|
|
623
701
|
|
|
624
|
-
|
|
702
|
+
return isServiceLocator;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (this.verbose) {
|
|
705
|
+
console.error(`[DEBUG] ❌ C014: Error checking service locator pattern: ${error.message}`);
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
625
709
|
}
|
|
626
710
|
|
|
627
711
|
hasDecorators(node, decoratorNames) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
712
|
+
try {
|
|
713
|
+
const decorators = node.getDecorators?.() || [];
|
|
714
|
+
return decorators.some(decorator => {
|
|
715
|
+
const decoratorText = decorator.getText();
|
|
716
|
+
return decoratorNames.some(name => decoratorText.includes(name));
|
|
717
|
+
});
|
|
718
|
+
} catch (error) {
|
|
719
|
+
if (this.verbose) {
|
|
720
|
+
console.error(`[DEBUG] ❌ C014: Error checking decorators: ${error.message}`);
|
|
721
|
+
}
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
633
724
|
}
|
|
634
725
|
|
|
635
726
|
shouldSkipFile(filePath) {
|
|
@@ -643,57 +734,70 @@ class C014SymbolBasedAnalyzer {
|
|
|
643
734
|
* Check if this is a Singleton pattern (self-instantiation)
|
|
644
735
|
*/
|
|
645
736
|
isSingletonPattern(className, context, sourceFile) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
737
|
+
try {
|
|
738
|
+
// Must be in a method (not constructor)
|
|
739
|
+
if (!context.isInMethod || context.isInConstructor) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
650
742
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
743
|
+
// Must be instantiating the same class we're in
|
|
744
|
+
const classDeclaration = this.findContainingClass(context, sourceFile);
|
|
745
|
+
if (!classDeclaration) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
656
748
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
749
|
+
const currentClassName = classDeclaration.getName();
|
|
750
|
+
if (currentClassName !== className) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
661
753
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const isSingletonMethod = singletonMethods.some(pattern =>
|
|
670
|
-
methodName.toLowerCase().includes(pattern.toLowerCase())
|
|
671
|
-
);
|
|
754
|
+
// Method name should suggest singleton (getInstance, instance, create, etc.)
|
|
755
|
+
const methodName = context.parentFunction?.getName?.() || '';
|
|
756
|
+
const singletonMethods = [
|
|
757
|
+
'getInstance', 'instance', 'getinstance', 'create', 'createInstance',
|
|
758
|
+
'singleton', 'getSingleton', 'getSharedInstance', 'shared'
|
|
759
|
+
];
|
|
672
760
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
761
|
+
const isSingletonMethod = singletonMethods.some(pattern =>
|
|
762
|
+
methodName.toLowerCase().includes(pattern.toLowerCase())
|
|
763
|
+
);
|
|
676
764
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
765
|
+
// Must be a static method
|
|
766
|
+
const isStaticMethod = context.parentFunction?.getModifiers?.()
|
|
767
|
+
?.some(modifier => modifier.getKind() === SyntaxKind.StaticKeyword) || false;
|
|
680
768
|
|
|
681
|
-
|
|
769
|
+
if (this.verbose && isSingletonMethod && isStaticMethod) {
|
|
770
|
+
console.log(`[DEBUG] 🔍 C014: Detected Singleton pattern: ${currentClassName}.${methodName}()`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return isSingletonMethod && isStaticMethod;
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (this.verbose) {
|
|
776
|
+
console.error(`[DEBUG] ❌ C014: Error checking singleton pattern: ${error.message}`);
|
|
777
|
+
}
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
682
780
|
}
|
|
683
781
|
|
|
684
782
|
/**
|
|
685
783
|
* Find the containing class declaration
|
|
686
784
|
*/
|
|
687
785
|
findContainingClass(context, sourceFile) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
786
|
+
try {
|
|
787
|
+
let current = context.parentFunction?.getParent();
|
|
788
|
+
|
|
789
|
+
while (current) {
|
|
790
|
+
if (current.getKind() === SyntaxKind.ClassDeclaration) {
|
|
791
|
+
return current;
|
|
792
|
+
}
|
|
793
|
+
current = current.getParent();
|
|
794
|
+
}
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (this.verbose) {
|
|
797
|
+
console.error(`[DEBUG] ❌ C014: Error finding containing class: ${error.message}`);
|
|
693
798
|
}
|
|
694
|
-
current = current.getParent();
|
|
695
799
|
}
|
|
696
|
-
|
|
800
|
+
|
|
697
801
|
return null;
|
|
698
802
|
}
|
|
699
803
|
|
|
@@ -725,27 +829,35 @@ class C014SymbolBasedAnalyzer {
|
|
|
725
829
|
|
|
726
830
|
buildViolationMessage(expr) {
|
|
727
831
|
const { className, context } = expr;
|
|
728
|
-
|
|
832
|
+
|
|
729
833
|
if (context.isInConstructor && context.isAssignedToThis) {
|
|
730
834
|
return `Direct instantiation of '${className}' in constructor. Consider injecting this dependency instead of creating it directly.`;
|
|
731
835
|
}
|
|
732
|
-
|
|
836
|
+
|
|
733
837
|
if (context.isInMethod && context.isAssignedToThis) {
|
|
734
838
|
return `Direct instantiation of '${className}' assigned to instance field. Consider injecting this dependency.`;
|
|
735
839
|
}
|
|
736
|
-
|
|
840
|
+
|
|
737
841
|
return `Direct instantiation of '${className}'. Consider using dependency injection or factory pattern.`;
|
|
738
842
|
}
|
|
739
843
|
|
|
740
844
|
buildSuggestion(expr) {
|
|
741
845
|
const { className, context } = expr;
|
|
742
|
-
|
|
846
|
+
|
|
743
847
|
if (context.isInConstructor) {
|
|
744
|
-
return `Inject ${className} via constructor parameter: constructor(private ${
|
|
848
|
+
return `Inject ${className} via constructor parameter: constructor(private ${this.toLowerCamelCase(className)}: ${className})`;
|
|
745
849
|
}
|
|
746
|
-
|
|
850
|
+
|
|
747
851
|
return `Consider injecting ${className} as a dependency or using a factory pattern`;
|
|
748
852
|
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Convert class name to lower camel case for parameter names
|
|
856
|
+
*/
|
|
857
|
+
toLowerCamelCase(str) {
|
|
858
|
+
if (!str || str.length === 0) return str;
|
|
859
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
860
|
+
}
|
|
749
861
|
}
|
|
750
862
|
|
|
751
863
|
module.exports = C014SymbolBasedAnalyzer;
|