@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.
Files changed (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. 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
- this.safeStrings = ["UNKNOWN", "N/A"]; // allowlist of special fallback values
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
- // skip ignored files
52
- if (this.isIgnoredFile(filePath)) {
178
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
179
+ if (!sourceFile) {
53
180
  if (verbose) {
54
- console.log(`🔍 [C024 Symbol-Based] Skipping ignored file: ${filePath}`);
181
+ console.log(`⚠️ [C024] Could not load source file: ${filePath}`);
55
182
  }
56
183
  return violations;
57
184
  }
58
185
 
59
- const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
60
- if (!sourceFile) {
61
- return violations;
62
- }
186
+ // Track constants to find duplicates
187
+ const constantUsage = new Map(); // value -> [locations]
63
188
 
64
- // skip constants files
65
- if (this.isConstantsFile(filePath)) return violations;
66
- // Detect hardcoded constants
67
- sourceFile.forEachDescendant((node) => {
68
- this.checkLiterals(node, sourceFile, violations);
69
- this.checkConstDeclaration(node, sourceFile, violations);
70
- this.checkStaticReadonly(node, sourceFile, violations);
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
- // --- push violation object ---
89
- pushViolation(violations, node, filePath, text, message) {
90
- violations.push({
217
+ createViolation(node, sourceFile, message, type, value) {
218
+ return {
91
219
  ruleId: this.ruleId,
92
- severity: "warning",
93
- message: message || `Hardcoded constant found: "${text}"`,
220
+ severity: 'medium',
221
+ message: message,
94
222
  source: this.ruleId,
95
- file: filePath,
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
- suggestion: "Define constants in a dedicated file or section",
101
- category: "constants",
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
- // --- check literals like "ADMIN", 123, true ---
106
- checkLiterals(node, sourceFile, violations) {
107
- const kind = node.getKind();
108
- if (
109
- kind === SyntaxKind.StringLiteral ||
110
- kind === SyntaxKind.NumericLiteral
111
- ) {
112
- const text = node.getText().replace(/['"`]/g, ""); // strip quotes
113
- if (this.isAllowedLiteral(node, text)) return;
114
-
115
- this.pushViolation(
116
- violations,
117
- node,
118
- sourceFile.getFilePath(),
119
- node.getText()
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
- // --- check const declarations outside constants.ts ---
125
- checkConstDeclaration(node, sourceFile, violations) {
126
- const kind = node.getKind();
127
- if (kind === SyntaxKind.VariableDeclaration) {
128
- const parentKind = node.getParent()?.getKind();
129
- // Skip detection for `for ... of` loop variable
130
- const loopAncestor = node.getFirstAncestor((ancestor) => {
131
- const kind = ancestor.getKind?.();
132
- return (
133
- kind === SyntaxKind.ForOfStatement ||
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
- // --- check static readonly properties inside classes ---
163
- checkStaticReadonly(node, sourceFile, violations) {
164
- const kind = node.getKind();
165
- if (kind === SyntaxKind.PropertyDeclaration) {
166
- const modifiers = node.getModifiers().map((m) => m.getText());
167
- if (modifiers.includes("static") && modifiers.includes("readonly")) {
168
- this.pushViolation(
169
- violations,
170
- node,
171
- sourceFile.getFilePath(),
172
- node.getName(),
173
- `Static readonly property "${node.getName()}" should be moved into constants file`
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
- // --- helper: allow safe literals ---
180
- isAllowedLiteral(node, text) {
427
+ isArrayIndexOrLoopCounter(node) {
181
428
  const parent = node.getParent();
429
+ if (!parent) return false;
182
430
 
183
- // 1 Skip imports/exports
184
- if (parent?.getKind() === SyntaxKind.ImportDeclaration) return true;
185
- if (parent?.getKind() === SyntaxKind.ExportDeclaration) return true;
431
+ const kind = parent.getKind();
186
432
 
187
- // 2 Skip literals that are inside call expressions (direct or nested)
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
- if (
196
- parent?.getKind() === SyntaxKind.ElementAccessExpression &&
197
- parent.getArgumentExpression?.() === node
198
- ) {
199
- return true; // skip array/object key
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
- // 3 Allow short strings
203
- if (typeof text === "string" && text.length <= 1) return true;
445
+ return false;
446
+ }
204
447
 
205
- // 4 Allow sentinel numbers
206
- if (text === "0" || text === "1" || text === "-1") return true;
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
- // 5 Allow known safe strings (like "UNKNOWN")
209
- if (this.safeStrings.includes(text)) return true;
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
- // 6 Allow SQL-style placeholders (:variable) inside string/template
212
- if (typeof text === "string" && /:\w+/.test(text)) {
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
- // helper to check if file is a constants file
220
- isConstantsFile(filePath) {
221
- const lower = filePath.toLowerCase();
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
- // 1 direct suffix match
236
- if (ignoredSuffixes.some(suffix => lower.endsWith(suffix))) {
237
- return true;
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
- // 2 matches dto.xxx.ts (multi-dot dto files)
241
- if (/\.dto\.[^.]+\.ts$/.test(lower)) {
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
- // 3 matches folder-based conventions
246
- if (
247
- lower.includes("/constants/") ||
248
- lower.includes("/enums/") ||
249
- lower.includes("/interfaces/")
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
- isIgnoredFile(filePath) {
259
- const ignoredPatterns = [
260
- /\.test\./i,
261
- /\.tests\./i,
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
- return ignoredPatterns.some((regex) => regex.test(filePath));
624
+ shouldIgnoreFile(filePath) {
625
+ return this.ignoredFilePatterns.some((pattern) => pattern.test(filePath));
284
626
  }
285
627
  }
286
628