@sun-asterisk/sunlint 1.3.16 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -11,7 +11,129 @@ class C024SymbolBasedAnalyzer {
|
|
|
11
11
|
this.ruleName = 'Error Scatter hardcoded constants throughout the logic (Symbol-Based)';
|
|
12
12
|
this.semanticEngine = semanticEngine;
|
|
13
13
|
this.verbose = false;
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
// === Files to ignore (constant/config files) ===
|
|
16
|
+
// === Files to ignore (constant/config files) ===
|
|
17
|
+
this.ignoredFilePatterns = [
|
|
18
|
+
/const/i,
|
|
19
|
+
/constants/i,
|
|
20
|
+
/enum/i,
|
|
21
|
+
/enums/i,
|
|
22
|
+
/interface/i,
|
|
23
|
+
/interfaces/i,
|
|
24
|
+
/type/i,
|
|
25
|
+
/types/i,
|
|
26
|
+
/dto/i,
|
|
27
|
+
/model/i,
|
|
28
|
+
/response/i,
|
|
29
|
+
/request/i,
|
|
30
|
+
/\.res\./i,
|
|
31
|
+
/\.req\./i,
|
|
32
|
+
/schema/i,
|
|
33
|
+
/definition/i,
|
|
34
|
+
/config/i,
|
|
35
|
+
/configuration/i,
|
|
36
|
+
// Test files
|
|
37
|
+
/test\//,
|
|
38
|
+
/tests\//,
|
|
39
|
+
/__tests__\//,
|
|
40
|
+
/\.test\./,
|
|
41
|
+
/\.spec\./,
|
|
42
|
+
/node_modules\//,
|
|
43
|
+
/mock/i,
|
|
44
|
+
/fixture/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// === Safe numeric values (not magic numbers) ===
|
|
48
|
+
this.safeNumbers = new Set([
|
|
49
|
+
-1, 0, 1, 2, 10, 100, 1000, // Common values
|
|
50
|
+
24, 60, // Time-related
|
|
51
|
+
100, // Percentages
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// === Contexts where constants are acceptable ===
|
|
55
|
+
this.acceptableContexts = [
|
|
56
|
+
SyntaxKind.EnumDeclaration,
|
|
57
|
+
SyntaxKind.InterfaceDeclaration,
|
|
58
|
+
SyntaxKind.TypeAliasDeclaration,
|
|
59
|
+
SyntaxKind.VariableDeclaration, // Top-level constants are OK
|
|
60
|
+
SyntaxKind.PropertyDeclaration, // Class properties
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// === String patterns that are acceptable (not magic strings) ===
|
|
64
|
+
this.acceptableStringPatterns = [
|
|
65
|
+
// Empty or very short strings (1-3 chars only)
|
|
66
|
+
/^$/,
|
|
67
|
+
/^.$/,
|
|
68
|
+
/^..$/,
|
|
69
|
+
/^...$/,
|
|
70
|
+
|
|
71
|
+
// Common delimiters and formatting
|
|
72
|
+
/^[,;:\.\-_\/\\|]+$/,
|
|
73
|
+
/^\s+$/,
|
|
74
|
+
/^[\r\n]+$/,
|
|
75
|
+
|
|
76
|
+
// HTML/XML tags
|
|
77
|
+
/^<[^>]+>$/,
|
|
78
|
+
|
|
79
|
+
// Common boolean-like words (exact match only)
|
|
80
|
+
/^(true|false|yes|no|ok|error|success|fail|null|undefined)$/i,
|
|
81
|
+
|
|
82
|
+
// Property access patterns
|
|
83
|
+
/^\./,
|
|
84
|
+
/^\[.*\]$/,
|
|
85
|
+
|
|
86
|
+
// SQL/Query patterns - table.column notation
|
|
87
|
+
/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/i, // table.column
|
|
88
|
+
/\.[a-z_][a-z0-9_]*$/i, // .column_name
|
|
89
|
+
/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*\s+(LIKE|=|>|<|!=|IS)/i, // SQL conditions
|
|
90
|
+
/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*\s+is\s+(null|not null)/i, // IS NULL checks
|
|
91
|
+
|
|
92
|
+
// SQL keywords
|
|
93
|
+
/^(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|AND|OR|JOIN|ON|AS|LIKE|IN|NOT|IS|NULL)\s/i,
|
|
94
|
+
/\s+(is null|is not null)$/i, // NULL checks
|
|
95
|
+
|
|
96
|
+
// TypeScript typeof checks
|
|
97
|
+
/^(string|number|boolean|object|function|symbol|bigint|undefined)$/,
|
|
98
|
+
|
|
99
|
+
// Common file extensions
|
|
100
|
+
/^\.[a-z]{2,4}$/i,
|
|
101
|
+
|
|
102
|
+
// HTTP methods and REST paths
|
|
103
|
+
/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/i,
|
|
104
|
+
/^\/[a-z0-9\-_\/]*$/i, // URL paths: /api/users, /search
|
|
105
|
+
|
|
106
|
+
// Event names (kebab-case or camelCase with common prefixes)
|
|
107
|
+
/^(on|handle|click|change|submit|load|error|success)/i,
|
|
108
|
+
/-activity$/i,
|
|
109
|
+
/-service$/i,
|
|
110
|
+
/-handler$/i,
|
|
111
|
+
/-event$/i,
|
|
112
|
+
|
|
113
|
+
// Status codes (lowercase or kebab-case only, not SCREAMING_CASE)
|
|
114
|
+
/^(pending|active|inactive|completed|failed|success|error)$/i,
|
|
115
|
+
/^[a-z]+(-[a-z]+)*$/, // kebab-case only
|
|
116
|
+
|
|
117
|
+
// Common paths
|
|
118
|
+
/^[\.\/]/,
|
|
119
|
+
|
|
120
|
+
// Variable interpolation
|
|
121
|
+
/\$\{.*\}/,
|
|
122
|
+
|
|
123
|
+
// Error messages (human-readable sentences with spaces)
|
|
124
|
+
/^[A-Z][a-z]+\s+.+/, // "User not found", "Invalid parameter" (must have space and more text)
|
|
125
|
+
/Exception$/,
|
|
126
|
+
/Error$/,
|
|
127
|
+
|
|
128
|
+
// SQL parameter placeholders
|
|
129
|
+
/^:[a-zA-Z_][a-zA-Z0-9_]*$/, // :empNo, :userId
|
|
130
|
+
/^@[a-zA-Z_][a-zA-Z0-9_]*$/, // @param, @userId
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// === Minimum thresholds ===
|
|
134
|
+
this.minStringLength = 4; // Strings shorter than this are ignored (but 4+ should be checked)
|
|
135
|
+
this.minNumberValue = 1000; // Numbers less than this need more context
|
|
136
|
+
this.minOccurrences = 2; // Need to appear at least this many times
|
|
15
137
|
}
|
|
16
138
|
|
|
17
139
|
async initialize(semanticEngine = null) {
|
|
@@ -43,32 +165,39 @@ class C024SymbolBasedAnalyzer {
|
|
|
43
165
|
return violations;
|
|
44
166
|
}
|
|
45
167
|
|
|
168
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
169
|
+
if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
|
|
170
|
+
return violations;
|
|
171
|
+
}
|
|
172
|
+
|
|
46
173
|
if (verbose) {
|
|
47
174
|
console.log(`🔍 [C024 Symbol-Based] Starting analysis for ${filePath}`);
|
|
48
175
|
}
|
|
49
176
|
|
|
50
177
|
try {
|
|
51
|
-
|
|
52
|
-
if (
|
|
178
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
179
|
+
if (!sourceFile) {
|
|
53
180
|
if (verbose) {
|
|
54
|
-
console.log(
|
|
181
|
+
console.log(`⚠️ [C024] Could not load source file: ${filePath}`);
|
|
55
182
|
}
|
|
56
183
|
return violations;
|
|
57
184
|
}
|
|
58
185
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return violations;
|
|
62
|
-
}
|
|
186
|
+
// Track constants to find duplicates
|
|
187
|
+
const constantUsage = new Map(); // value -> [locations]
|
|
63
188
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
189
|
+
// Find all numeric literals in logic
|
|
190
|
+
this.checkNumericLiterals(sourceFile, violations, constantUsage);
|
|
191
|
+
|
|
192
|
+
// Find all string literals in logic
|
|
193
|
+
this.checkStringLiterals(sourceFile, violations, constantUsage);
|
|
194
|
+
|
|
195
|
+
// Check for duplicate constants (same value used multiple times)
|
|
196
|
+
this.checkDuplicateConstants(constantUsage, sourceFile, violations);
|
|
197
|
+
|
|
198
|
+
if (verbose) {
|
|
199
|
+
console.log(`✅ [C024] Found ${violations.length} violations in ${filePath}`);
|
|
200
|
+
}
|
|
72
201
|
|
|
73
202
|
|
|
74
203
|
if (verbose) {
|
|
@@ -85,202 +214,415 @@ class C024SymbolBasedAnalyzer {
|
|
|
85
214
|
}
|
|
86
215
|
}
|
|
87
216
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
violations.push({
|
|
217
|
+
createViolation(node, sourceFile, message, type, value) {
|
|
218
|
+
return {
|
|
91
219
|
ruleId: this.ruleId,
|
|
92
|
-
severity:
|
|
93
|
-
message: message
|
|
220
|
+
severity: 'medium',
|
|
221
|
+
message: message,
|
|
94
222
|
source: this.ruleId,
|
|
95
|
-
file:
|
|
223
|
+
file: sourceFile.getFilePath(),
|
|
96
224
|
line: node.getStartLineNumber(),
|
|
97
|
-
column: node.getStart() - node.getStartLinePos(),
|
|
225
|
+
column: node.getStart() - node.getStartLinePos() + 1,
|
|
98
226
|
description:
|
|
99
227
|
"[SYMBOL-BASED] Hardcoded constants should be defined in a single place to improve maintainability.",
|
|
100
|
-
|
|
101
|
-
|
|
228
|
+
category: "maintainability",
|
|
229
|
+
suggestion: `Define the ${type} '${value}' in a dedicated constants file or section`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
checkNumericLiterals(sourceFile, violations, constantUsage) {
|
|
234
|
+
const numericLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.NumericLiteral);
|
|
235
|
+
|
|
236
|
+
numericLiterals.forEach(literal => {
|
|
237
|
+
const value = literal.getLiteralValue();
|
|
238
|
+
const text = literal.getText();
|
|
239
|
+
|
|
240
|
+
// Skip safe numbers
|
|
241
|
+
if (this.safeNumbers.has(value)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Skip if in acceptable context (enum, const declaration, etc.)
|
|
246
|
+
if (this.isInAcceptableContext(literal)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Skip if it's an array index or simple loop counter
|
|
251
|
+
if (this.isArrayIndexOrLoopCounter(literal)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Track for duplicate detection
|
|
256
|
+
this.trackConstant(constantUsage, `number:${value}`, literal);
|
|
257
|
+
|
|
258
|
+
// Flag as magic number if value is significant
|
|
259
|
+
if (Math.abs(value) >= this.minNumberValue || this.isLikelyMagicNumber(literal)) {
|
|
260
|
+
violations.push(this.createViolation(
|
|
261
|
+
literal,
|
|
262
|
+
sourceFile,
|
|
263
|
+
`Magic number '${text}' should be extracted as a named constant`,
|
|
264
|
+
'magic-number',
|
|
265
|
+
value
|
|
266
|
+
));
|
|
267
|
+
}
|
|
102
268
|
});
|
|
103
269
|
}
|
|
104
270
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (this.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
271
|
+
checkStringLiterals(sourceFile, violations, constantUsage) {
|
|
272
|
+
const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral);
|
|
273
|
+
|
|
274
|
+
stringLiterals.forEach(literal => {
|
|
275
|
+
const value = literal.getLiteralValue();
|
|
276
|
+
const text = literal.getText();
|
|
277
|
+
|
|
278
|
+
// Skip short strings
|
|
279
|
+
if (value.length < this.minStringLength) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Skip acceptable string patterns
|
|
284
|
+
if (this.isAcceptableString(value)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Skip if in acceptable context
|
|
289
|
+
if (this.isInAcceptableContext(literal)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Skip if it's a property key or object key
|
|
294
|
+
if (this.isPropertyKey(literal)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Skip if it's in a QueryBuilder pattern
|
|
299
|
+
if (this.isQueryBuilderPattern(literal)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Skip template literals that are mostly variables
|
|
304
|
+
if (this.isTemplateWithVariables(literal)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Track for duplicate detection
|
|
309
|
+
this.trackConstant(constantUsage, `string:${value}`, literal);
|
|
310
|
+
|
|
311
|
+
// Flag as magic string if it's in logic
|
|
312
|
+
if (this.isInLogicContext(literal) || this.isInComparison(literal)) {
|
|
313
|
+
violations.push(this.createViolation(
|
|
314
|
+
literal,
|
|
315
|
+
sourceFile,
|
|
316
|
+
`Magic string '${this.truncate(value, 50)}' should be extracted as a named constant`,
|
|
317
|
+
'magic-string',
|
|
318
|
+
value
|
|
319
|
+
));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
122
322
|
}
|
|
123
323
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
kind === SyntaxKind.ForInStatement ||
|
|
135
|
-
kind === SyntaxKind.ForStatement ||
|
|
136
|
-
kind === SyntaxKind.WhileStatement ||
|
|
137
|
-
kind === SyntaxKind.DoStatement ||
|
|
138
|
-
kind === SyntaxKind.SwitchStatement
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if (loopAncestor) {
|
|
143
|
-
return; // skip for all loop/switch contexts, no matter how nested
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
parentKind === SyntaxKind.VariableDeclarationList &&
|
|
148
|
-
node.getParent().getDeclarationKind() === "const" &&
|
|
149
|
-
!node.getInitializer()
|
|
150
|
-
) {
|
|
151
|
-
this.pushViolation(
|
|
152
|
-
violations,
|
|
153
|
-
node,
|
|
154
|
-
sourceFile.getFilePath(),
|
|
155
|
-
node.getName(),
|
|
156
|
-
`Const declaration "${node.getName()}" should be moved into constants file`
|
|
324
|
+
checkDuplicateConstants(constantUsage, sourceFile, violations) {
|
|
325
|
+
for (const [key, locations] of constantUsage.entries()) {
|
|
326
|
+
if (locations.length >= this.minOccurrences) {
|
|
327
|
+
const [type, value] = key.split(':', 2);
|
|
328
|
+
const firstLocation = locations[0];
|
|
329
|
+
|
|
330
|
+
// Only flag if not already flagged as magic number/string
|
|
331
|
+
const alreadyFlagged = violations.some(v =>
|
|
332
|
+
v.line === firstLocation.getStartLineNumber() &&
|
|
333
|
+
v.column === firstLocation.getStart() - firstLocation.getStartLinePos() + 1
|
|
157
334
|
);
|
|
335
|
+
|
|
336
|
+
if (!alreadyFlagged) {
|
|
337
|
+
violations.push(this.createViolation(
|
|
338
|
+
firstLocation,
|
|
339
|
+
sourceFile,
|
|
340
|
+
`Duplicate constant '${this.truncate(value, 50)}' used ${locations.length} times. Extract to a named constant.`,
|
|
341
|
+
'duplicate-constant',
|
|
342
|
+
value
|
|
343
|
+
));
|
|
344
|
+
}
|
|
158
345
|
}
|
|
159
346
|
}
|
|
160
347
|
}
|
|
161
348
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
349
|
+
isInAcceptableContext(node) {
|
|
350
|
+
let parent = node.getParent();
|
|
351
|
+
let depth = 0;
|
|
352
|
+
const maxDepth = 5;
|
|
353
|
+
|
|
354
|
+
while (parent && depth < maxDepth) {
|
|
355
|
+
const kind = parent.getKind();
|
|
356
|
+
|
|
357
|
+
// Top-level const declaration
|
|
358
|
+
if (kind === SyntaxKind.VariableDeclaration) {
|
|
359
|
+
const varDecl = parent;
|
|
360
|
+
const varStatement = varDecl.getParent()?.getParent();
|
|
361
|
+
if (varStatement && varStatement.getKind() === SyntaxKind.VariableStatement) {
|
|
362
|
+
const isConst = varStatement.getDeclarationKind() === 'const';
|
|
363
|
+
const isTopLevel = varStatement.getParent()?.getKind() === SyntaxKind.SourceFile;
|
|
364
|
+
|
|
365
|
+
// Allow const declarations (top-level or in functions)
|
|
366
|
+
if (isConst) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Enum, interface, type alias
|
|
373
|
+
if (this.acceptableContexts.includes(kind)) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Object literal that's assigned to a const
|
|
378
|
+
if (kind === SyntaxKind.ObjectLiteralExpression) {
|
|
379
|
+
const objectParent = parent.getParent();
|
|
380
|
+
if (objectParent?.getKind() === SyntaxKind.VariableDeclaration) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Array literal assigned to a const (for SQL columns, etc.)
|
|
386
|
+
if (kind === SyntaxKind.ArrayLiteralExpression) {
|
|
387
|
+
const arrayParent = parent.getParent();
|
|
388
|
+
if (arrayParent?.getKind() === SyntaxKind.VariableDeclaration) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Decorator arguments (NestJS @Post('search'), @Param('id'))
|
|
394
|
+
if (kind === SyntaxKind.Decorator) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Call expression arguments for decorators
|
|
399
|
+
if (kind === SyntaxKind.CallExpression) {
|
|
400
|
+
const callParent = parent.getParent();
|
|
401
|
+
if (callParent?.getKind() === SyntaxKind.Decorator) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// TypeOf expression (typeof x === 'string')
|
|
407
|
+
if (kind === SyntaxKind.TypeOfExpression) {
|
|
408
|
+
return true;
|
|
175
409
|
}
|
|
410
|
+
|
|
411
|
+
// Binary expression with typeof
|
|
412
|
+
if (kind === SyntaxKind.BinaryExpression) {
|
|
413
|
+
const binaryExpr = parent;
|
|
414
|
+
const left = binaryExpr.getLeft();
|
|
415
|
+
if (left.getKind() === SyntaxKind.TypeOfExpression) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
parent = parent.getParent();
|
|
421
|
+
depth++;
|
|
176
422
|
}
|
|
423
|
+
|
|
424
|
+
return false;
|
|
177
425
|
}
|
|
178
426
|
|
|
179
|
-
|
|
180
|
-
isAllowedLiteral(node, text) {
|
|
427
|
+
isArrayIndexOrLoopCounter(node) {
|
|
181
428
|
const parent = node.getParent();
|
|
429
|
+
if (!parent) return false;
|
|
182
430
|
|
|
183
|
-
|
|
184
|
-
if (parent?.getKind() === SyntaxKind.ImportDeclaration) return true;
|
|
185
|
-
if (parent?.getKind() === SyntaxKind.ExportDeclaration) return true;
|
|
431
|
+
const kind = parent.getKind();
|
|
186
432
|
|
|
187
|
-
//
|
|
188
|
-
if (
|
|
189
|
-
parent?.getKind() === SyntaxKind.CallExpression ||
|
|
190
|
-
parent?.getFirstAncestorByKind(SyntaxKind.CallExpression)
|
|
191
|
-
) {
|
|
433
|
+
// Array element access: arr[0], arr[1]
|
|
434
|
+
if (kind === SyntaxKind.ElementAccessExpression) {
|
|
192
435
|
return true;
|
|
193
436
|
}
|
|
194
437
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return true;
|
|
438
|
+
// Loop increment: i++, i += 1
|
|
439
|
+
if (kind === SyntaxKind.BinaryExpression ||
|
|
440
|
+
kind === SyntaxKind.PostfixUnaryExpression ||
|
|
441
|
+
kind === SyntaxKind.PrefixUnaryExpression) {
|
|
442
|
+
return true;
|
|
200
443
|
}
|
|
201
444
|
|
|
202
|
-
|
|
203
|
-
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
204
447
|
|
|
205
|
-
|
|
206
|
-
|
|
448
|
+
isLikelyMagicNumber(node) {
|
|
449
|
+
const parent = node.getParent();
|
|
450
|
+
if (!parent) return false;
|
|
451
|
+
|
|
452
|
+
// Numbers in comparisons are often magic numbers
|
|
453
|
+
if (parent.getKind() === SyntaxKind.BinaryExpression) {
|
|
454
|
+
const binaryExpr = parent;
|
|
455
|
+
const operator = binaryExpr.getOperatorToken().getText();
|
|
456
|
+
if (['>', '<', '>=', '<=', '===', '!==', '==', '!='].includes(operator)) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
207
460
|
|
|
208
|
-
//
|
|
209
|
-
if (
|
|
461
|
+
// Numbers in calculations
|
|
462
|
+
if (parent.getKind() === SyntaxKind.BinaryExpression) {
|
|
463
|
+
const binaryExpr = parent;
|
|
464
|
+
const operator = binaryExpr.getOperatorToken().getText();
|
|
465
|
+
if (['*', '/', '%'].includes(operator)) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
210
469
|
|
|
211
|
-
|
|
212
|
-
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
isAcceptableString(value) {
|
|
474
|
+
return this.acceptableStringPatterns.some(pattern => pattern.test(value));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
isPropertyKey(node) {
|
|
478
|
+
const parent = node.getParent();
|
|
479
|
+
if (!parent) return false;
|
|
480
|
+
|
|
481
|
+
// Object property key
|
|
482
|
+
if (parent.getKind() === SyntaxKind.PropertyAssignment) {
|
|
483
|
+
const prop = parent;
|
|
484
|
+
return prop.getInitializer() !== node;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Dot notation property access
|
|
488
|
+
if (parent.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
213
489
|
return true;
|
|
214
490
|
}
|
|
215
491
|
|
|
492
|
+
// Parameter decorator name: @Param('cm_cst_id')
|
|
493
|
+
if (parent.getKind() === SyntaxKind.CallExpression) {
|
|
494
|
+
const callParent = parent.getParent();
|
|
495
|
+
if (callParent?.getKind() === SyntaxKind.Decorator) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
216
500
|
return false;
|
|
217
501
|
}
|
|
218
502
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// common suffixes/patterns for utility or structural files
|
|
224
|
-
const ignoredSuffixes = [
|
|
225
|
-
".constants.ts",
|
|
226
|
-
".const.ts",
|
|
227
|
-
".enum.ts",
|
|
228
|
-
".interface.ts",
|
|
229
|
-
".response.ts",
|
|
230
|
-
".request.ts",
|
|
231
|
-
".res.ts",
|
|
232
|
-
".req.ts",
|
|
233
|
-
];
|
|
503
|
+
isQueryBuilderPattern(node) {
|
|
504
|
+
let parent = node.getParent();
|
|
505
|
+
let depth = 0;
|
|
506
|
+
const maxDepth = 5;
|
|
234
507
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
508
|
+
// Walk up the tree to find if we're in a QueryBuilder call
|
|
509
|
+
while (parent && depth < maxDepth) {
|
|
510
|
+
const kind = parent.getKind();
|
|
511
|
+
|
|
512
|
+
// Check if part of a call expression
|
|
513
|
+
if (kind === SyntaxKind.CallExpression) {
|
|
514
|
+
const callExpr = parent;
|
|
515
|
+
const expression = callExpr.getExpression();
|
|
516
|
+
const exprText = expression.getText();
|
|
517
|
+
|
|
518
|
+
// QueryBuilder methods: .where(), .andWhere(), .orWhere(), etc.
|
|
519
|
+
if (/\.(where|andWhere|orWhere|having|andHaving|orHaving|select|addSelect|leftJoin|innerJoin|join|orderBy|groupBy|setParameter)$/i.test(exprText)) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Also check for common ORM query builders
|
|
524
|
+
if (/(queryBuilder|qb|query)\.(where|andWhere|orWhere)/i.test(exprText)) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Skip through conditional expressions (ternary operators)
|
|
530
|
+
if (kind === SyntaxKind.ConditionalExpression) {
|
|
531
|
+
parent = parent.getParent();
|
|
532
|
+
depth++;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
parent = parent.getParent();
|
|
537
|
+
depth++;
|
|
238
538
|
}
|
|
239
539
|
|
|
240
|
-
|
|
241
|
-
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
isTemplateWithVariables(node) {
|
|
544
|
+
const parent = node.getParent();
|
|
545
|
+
if (!parent) return false;
|
|
546
|
+
|
|
547
|
+
if (parent.getKind() === SyntaxKind.TemplateExpression) {
|
|
242
548
|
return true;
|
|
243
549
|
}
|
|
244
550
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
isInLogicContext(node) {
|
|
555
|
+
let parent = node.getParent();
|
|
556
|
+
let depth = 0;
|
|
557
|
+
const maxDepth = 5;
|
|
558
|
+
|
|
559
|
+
while (parent && depth < maxDepth) {
|
|
560
|
+
const kind = parent.getKind();
|
|
561
|
+
// Skip when part of an element access chain like arr['key1']['key2']
|
|
562
|
+
// Walk up any nested ElementAccessExpression hierarchy
|
|
563
|
+
if (kind === SyntaxKind.ElementAccessExpression) {
|
|
564
|
+
return false; // skip, this is not a logic context — it's array/object access
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Inside function body, method, arrow function
|
|
568
|
+
if (kind === SyntaxKind.FunctionDeclaration ||
|
|
569
|
+
kind === SyntaxKind.MethodDeclaration ||
|
|
570
|
+
kind === SyntaxKind.ArrowFunction ||
|
|
571
|
+
kind === SyntaxKind.FunctionExpression) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Inside if statement, switch, loop
|
|
576
|
+
if (kind === SyntaxKind.IfStatement ||
|
|
577
|
+
kind === SyntaxKind.SwitchStatement ||
|
|
578
|
+
kind === SyntaxKind.ForStatement ||
|
|
579
|
+
kind === SyntaxKind.WhileStatement ||
|
|
580
|
+
kind === SyntaxKind.DoStatement) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
parent = parent.getParent();
|
|
585
|
+
depth++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
isInComparison(node) {
|
|
592
|
+
const parent = node.getParent();
|
|
593
|
+
if (!parent) return false;
|
|
594
|
+
|
|
595
|
+
// Direct comparison: if (type === 'GOLD')
|
|
596
|
+
if (parent.getKind() === SyntaxKind.BinaryExpression) {
|
|
597
|
+
const binaryExpr = parent;
|
|
598
|
+
const operator = binaryExpr.getOperatorToken().getText();
|
|
599
|
+
if (['===', '!==', '==', '!=', '>', '<', '>=', '<='].includes(operator)) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Case clause: case 'GOLD':
|
|
605
|
+
if (parent.getKind() === SyntaxKind.CaseClause) {
|
|
251
606
|
return true;
|
|
252
607
|
}
|
|
253
608
|
|
|
254
609
|
return false;
|
|
255
610
|
}
|
|
256
611
|
|
|
612
|
+
trackConstant(constantUsage, key, node) {
|
|
613
|
+
if (!constantUsage.has(key)) {
|
|
614
|
+
constantUsage.set(key, []);
|
|
615
|
+
}
|
|
616
|
+
constantUsage.get(key).push(node);
|
|
617
|
+
}
|
|
257
618
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
/\.spec\./i,
|
|
263
|
-
/\.mock\./i,
|
|
264
|
-
/\.css$/i,
|
|
265
|
-
/\.scss$/i,
|
|
266
|
-
/\.html$/i,
|
|
267
|
-
/\.json$/i,
|
|
268
|
-
/\.md$/i,
|
|
269
|
-
/\.svg$/i,
|
|
270
|
-
/\.png$/i,
|
|
271
|
-
/\.jpg$/i,
|
|
272
|
-
/\.jpeg$/i,
|
|
273
|
-
/\.gif$/i,
|
|
274
|
-
/\.bmp$/i,
|
|
275
|
-
/\.ico$/i,
|
|
276
|
-
/\.lock$/i,
|
|
277
|
-
/\.log$/i,
|
|
278
|
-
/\/test\//i,
|
|
279
|
-
/\/tests\//i,
|
|
280
|
-
/\/spec\//i
|
|
281
|
-
];
|
|
619
|
+
truncate(str, maxLength) {
|
|
620
|
+
if (str.length <= maxLength) return str;
|
|
621
|
+
return str.substring(0, maxLength) + '...';
|
|
622
|
+
}
|
|
282
623
|
|
|
283
|
-
|
|
624
|
+
shouldIgnoreFile(filePath) {
|
|
625
|
+
return this.ignoredFilePatterns.some((pattern) => pattern.test(filePath));
|
|
284
626
|
}
|
|
285
627
|
}
|
|
286
628
|
|