@sun-asterisk/sunlint 1.3.26 → 1.3.28

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 (69) hide show
  1. package/config/rules/enhanced-rules-registry.json +101 -17
  2. package/config/rules/rules-registry-generated.json +22 -22
  3. package/origin-rules/security-en.md +351 -338
  4. package/package.json +1 -1
  5. package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
  6. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
  8. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  9. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  10. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
  11. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  12. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  13. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  14. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  15. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  16. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  17. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  18. package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
  19. package/rules/security/S005_no_origin_auth/config.json +28 -67
  20. package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
  21. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
  22. package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
  23. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  24. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  25. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  26. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
  27. package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
  28. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
  29. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  30. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  31. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  32. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
  33. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
  34. package/rules/security/S022_escape_output_context/README.md +254 -0
  35. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  36. package/rules/security/S022_escape_output_context/config.json +229 -0
  37. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  38. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  39. package/rules/security/S023_no_json_injection/config.json +133 -0
  40. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  41. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  42. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  43. package/rules/security/S029_csrf_protection/config.json +127 -0
  44. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  45. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  46. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  47. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  48. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  49. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  50. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  51. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  52. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  53. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  54. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  55. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  56. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  57. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  58. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  59. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
  60. package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
  61. package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
  62. package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
  63. package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
  64. package/docs/COMMAND-EXAMPLES.md +0 -390
  65. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
  66. package/docs/FOLDER_STRUCTURE.md +0 -59
  67. package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
  68. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
  69. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
@@ -12,7 +12,6 @@ class C024SymbolBasedAnalyzer {
12
12
  this.semanticEngine = semanticEngine;
13
13
  this.verbose = false;
14
14
 
15
- // === Files to ignore (constant/config files) ===
16
15
  // === Files to ignore (constant/config files) ===
17
16
  this.ignoredFilePatterns = [
18
17
  /const/i,
@@ -60,6 +59,63 @@ class C024SymbolBasedAnalyzer {
60
59
  SyntaxKind.PropertyDeclaration, // Class properties
61
60
  ];
62
61
 
62
+ // === Frontend-specific JSX/Vue attributes to ignore ===
63
+ this.frontendIgnoredAttributes = [
64
+ 'className',
65
+ 'class',
66
+ 'style',
67
+ 'id',
68
+ 'key',
69
+ 'ref',
70
+ 'data-testid',
71
+ 'data-cy',
72
+ 'aria-label',
73
+ 'aria-labelledby',
74
+ 'aria-describedby',
75
+ 'role',
76
+ 'placeholder',
77
+ 'title',
78
+ 'alt',
79
+ 'src',
80
+ 'href',
81
+ 'type', // for input type="text", button type="submit"
82
+ 'name', // form input names
83
+ 'value', // when used as JSX prop
84
+ 'defaultValue',
85
+ 'label',
86
+ 'htmlFor',
87
+ 'for',
88
+ ];
89
+
90
+ // === Common form/state management function names to ignore ===
91
+ this.frontendIgnoredFunctions = [
92
+ 'setValue',
93
+ 'getValue',
94
+ 'setFieldValue',
95
+ 'getFieldValue',
96
+ 'register',
97
+ 'unregister',
98
+ 'watch',
99
+ 'reset',
100
+ 'resetField',
101
+ 'setError',
102
+ 'clearErrors',
103
+ 'trigger',
104
+ 'control',
105
+ 'handleSubmit',
106
+ // Vue specific
107
+ 'defineProps',
108
+ 'defineEmits',
109
+ 'ref',
110
+ 'computed',
111
+ 'reactive',
112
+ // State management
113
+ 'dispatch',
114
+ 'commit',
115
+ 'useState',
116
+ 'useReducer',
117
+ ];
118
+
63
119
  // === String patterns that are acceptable (not magic strings) ===
64
120
  this.acceptableStringPatterns = [
65
121
  // Empty or very short strings (1-3 chars only)
@@ -89,6 +145,12 @@ class C024SymbolBasedAnalyzer {
89
145
  /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*\s+(LIKE|=|>|<|!=|IS)/i, // SQL conditions
90
146
  /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*\s+is\s+(null|not null)/i, // IS NULL checks
91
147
 
148
+ // table.column AS alias
149
+ /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*\s+as\s+[a-z_][a-z0-9_]*$/i,
150
+
151
+ // table.column alias (implicit alias)
152
+ /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*\s+[a-z_][a-z0-9_]*$/i,
153
+
92
154
  // SQL keywords
93
155
  /^(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|AND|OR|JOIN|ON|AS|LIKE|IN|NOT|IS|NULL)\s/i,
94
156
  /\s+(is null|is not null)$/i, // NULL checks
@@ -128,12 +190,111 @@ class C024SymbolBasedAnalyzer {
128
190
  // SQL parameter placeholders
129
191
  /^:[a-zA-Z_][a-zA-Z0-9_]*$/, // :empNo, :userId
130
192
  /^@[a-zA-Z_][a-zA-Z0-9_]*$/, // @param, @userId
193
+
194
+ // ===== FRONTEND CSS CLASS PATTERNS =====
195
+
196
+ // Tailwind CSS utility classes - comprehensive patterns
197
+ /^[a-z]+-\d+$/, // w-40, h-12, p-4, m-8, text-lg, space-x-4
198
+ /^[a-z]+-\d+\/\d+$/, // w-1/2, w-3/4, col-span-2/3
199
+ /^[a-z]+-\[\d+(\.\d+)?(px|em|rem|%|vh|vw)\]$/, // w-[100px], h-[50vh], p-[1.5rem]
200
+ /^[a-z]+-(xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, // text-sm, rounded-lg, shadow-xl
201
+ /^(sm|md|lg|xl|2xl):[a-z]+-\d+$/, // md:w-40, lg:p-8 (responsive)
202
+ /^(sm|md|lg|xl|2xl):[a-z]+-[a-z0-9]+$/, // md:flex, lg:hidden (responsive variants)
203
+ /^hover:[a-z]+-[a-z0-9]+$/, // hover:bg-blue-500, hover:text-white
204
+ /^focus:[a-z]+-[a-z0-9]+$/, // focus:ring-2, focus:outline-none
205
+ /^active:[a-z]+-[a-z0-9]+$/, // active:bg-gray-700
206
+ /^disabled:[a-z]+-[a-z0-9]+$/, // disabled:opacity-50
207
+ /^dark:[a-z]+-[a-z0-9]+$/, // dark:bg-gray-800, dark:text-white
208
+ /^(bg|text|border|ring|from|to|via)-[a-z]+-\d{2,3}$/, // bg-blue-500, text-gray-700
209
+ /^-?[a-z]+-\d+$/, // -m-4, -top-2 (negative values)
210
+ /^(flex|grid|inline|block|hidden|relative|absolute|fixed|sticky|static)$/i,
211
+ /^justify-(start|end|center|between|around|evenly)$/,
212
+ /^items-(start|end|center|baseline|stretch)$/,
213
+ /^col-span-\d+$/,
214
+ /^row-span-\d+$/,
215
+ /^gap-\d+$/,
216
+ /^space-[xy]-\d+$/,
217
+ /^(min|max)-(w|h)-\d+$/,
218
+ /^(min|max)-(w|h)-(full|screen|fit|min|max)$/,
219
+ /^flex-(row|col|wrap|nowrap|1|auto|initial|none)$/,
220
+ /^grid-(cols|rows)-\d+$/,
221
+ /^overflow-(auto|hidden|visible|scroll|x-auto|y-auto|x-hidden|y-hidden)$/,
222
+ /^cursor-(pointer|default|wait|text|move|not-allowed|help|none|auto)$/,
223
+ /^select-(none|text|all|auto)$/,
224
+ /^pointer-events-(none|auto)$/,
225
+ /^resize-(none|both|x|y)$/,
226
+ /^outline-(none|white|black|\d+)$/,
227
+ /^opacity-\d+$/,
228
+ /^z-\d+$/,
229
+ /^order-\d+$/,
230
+ /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,
231
+ /^tracking-(tighter|tight|normal|wide|wider|widest)$/,
232
+ /^leading-(none|tight|snug|normal|relaxed|loose|\d+)$/,
233
+ /^(top|right|bottom|left|inset)-(0|auto|\d+)$/,
234
+ /^(rounded|border)-(t|r|b|l|tl|tr|br|bl)-\d+$/,
235
+ /^divide-(x|y)-\d+$/,
236
+ /^whitespace-(normal|nowrap|pre|pre-line|pre-wrap)$/,
237
+ /^break-(normal|words|all|keep)$/,
238
+ /^truncate$/,
239
+ /^line-clamp-\d+$/,
240
+ /^(uppercase|lowercase|capitalize|normal-case)$/,
241
+ /^(underline|overline|line-through|no-underline)$/,
242
+ /^(italic|not-italic)$/,
243
+ /^placeholder-[a-z]+-\d{3}$/,
244
+ /^caret-[a-z]+-\d{3}$/,
245
+ /^accent-[a-z]+-\d{3}$/,
246
+ /^aspect-(auto|square|video)$/,
247
+ /^columns-\d+$/,
248
+ /^break-(before|after|inside)-(auto|avoid|all|avoid-page|page|left|right|column)$/,
249
+
250
+ // Multiple CSS classes in one string (space-separated)
251
+ /^([a-z\-]+\s+)+[a-z\-]+$/, // "flex gap-2", "text-sm font-bold"
252
+
253
+ // Bootstrap classes
254
+ /^(container|row|col)(-[a-z]{2})?(-\d+)?$/,
255
+ /^(btn|alert|badge|card|modal|navbar|dropdown|form|input|table|nav|pagination|breadcrumb|tooltip|popover)(-[a-z]+)*$/,
256
+ /^[a-z]+-\d$/,
257
+ /^(d|p|m|pt|pb|pl|pr|px|py|mt|mb|ml|mr|mx|my)-(0|1|2|3|4|5|auto)$/,
258
+ /^text-(left|center|right|justify|primary|secondary|success|danger|warning|info|light|dark|muted|white|body)$/,
259
+ /^bg-(primary|secondary|success|danger|warning|info|light|dark|white|transparent|body)$/,
260
+ /^border-(primary|secondary|success|danger|warning|info|light|dark|white|0)$/,
261
+ /^rounded(-top|-bottom|-left|-right|-circle|-pill|-\d+)?$/,
262
+ /^shadow(-sm|-lg|-none)?$/,
263
+ /^(w|h)-(25|50|75|100|auto)$/,
264
+ /^(float|clearfix)-(left|right|none|start|end)$/,
265
+ /^position-(static|relative|absolute|fixed|sticky)$/,
266
+ /^(show|hide|visible|invisible|sr-only)$/,
267
+
268
+ // Vue.js specific (scoped classes, v-bind classes)
269
+ /^v-[a-z]+(-[a-z]+)*$/,
270
+ /^_[a-z0-9]+_\d+$/,
271
+ /^data-v-[a-f0-9]{8}$/,
272
+
273
+ // React/Vue conditional classes
274
+ /^\{.*\?\.*:.*\}$/,
275
+
276
+ // Common CSS framework patterns
277
+ /^is-[a-z]+$/,
278
+ /^has-[a-z]+$/,
279
+ /^(show|hide|visible|invisible|disabled|enabled|loading|active|inactive)$/i,
280
+
281
+ // Icon classes
282
+ /^(icon|fa|fas|far|fab|fal|fad|material-icons|mi|mdi|bi|ri)(-[a-z0-9]+)*$/,
283
+
284
+ // Animation and transition classes
285
+ /^(animate|animation|transition|transform|duration|delay|ease)(-[a-z0-9]+)*$/,
286
+ /^(fade|slide|zoom|bounce|rotate|scale|spin|ping|pulse)(-[a-z]+)*$/,
287
+
288
+ // Common Japanese/localized placeholders (for your use case)
289
+ /^[ぁ-んァ-ヶー一-龠々〆〤]+を[選択入力記入]/, // 委託元を選択してください
290
+ /^[ぁ-んァ-ヶー一-龠々〆〤]+してください$/, // してください patterns
291
+ /^[ぁ-んァ-ヶー一-龠々〆〤\s]+$/, // Japanese text (UI labels)
131
292
  ];
132
293
 
133
294
  // === Minimum thresholds ===
134
- this.minStringLength = 4; // Strings shorter than this are ignored (but 4+ should be checked)
295
+ this.minStringLength = 4; // Strings shorter than this are ignored
135
296
  this.minNumberValue = 1000; // Numbers less than this need more context
136
- this.minOccurrences = 2; // Need to appear at least this many times
297
+ this.minOccurrences = 5; // Need to appear at least this many times
137
298
  }
138
299
 
139
300
  async initialize(semanticEngine = null) {
@@ -148,14 +309,12 @@ class C024SymbolBasedAnalyzer {
148
309
  }
149
310
 
150
311
  async analyzeFileBasic(filePath, options = {}) {
151
- // This is the main entry point called by the hybrid analyzer
152
312
  return await this.analyzeFileWithSymbols(filePath, options);
153
313
  }
154
314
 
155
315
  async analyzeFileWithSymbols(filePath, options = {}) {
156
316
  const violations = [];
157
317
 
158
- // Enable verbose mode if requested
159
318
  const verbose = options.verbose || this.verbose;
160
319
 
161
320
  if (!this.semanticEngine?.project) {
@@ -183,23 +342,16 @@ class C024SymbolBasedAnalyzer {
183
342
  return violations;
184
343
  }
185
344
 
186
- // Track constants to find duplicates
187
- const constantUsage = new Map(); // value -> [locations]
345
+ const constantUsage = new Map();
188
346
 
189
- // Find all numeric literals in logic
190
347
  this.checkNumericLiterals(sourceFile, violations, constantUsage);
191
-
192
- // Find all string literals in logic
193
348
  this.checkStringLiterals(sourceFile, violations, constantUsage);
194
-
195
- // Check for duplicate constants (same value used multiple times)
196
349
  this.checkDuplicateConstants(constantUsage, sourceFile, violations);
197
350
 
198
351
  if (verbose) {
199
352
  console.log(`✅ [C024] Found ${violations.length} violations in ${filePath}`);
200
353
  }
201
354
 
202
-
203
355
  if (verbose) {
204
356
  console.log(`🔍 [C024 Symbol-Based] Total violations found: ${violations.length}`);
205
357
  }
@@ -237,25 +389,20 @@ class C024SymbolBasedAnalyzer {
237
389
  const value = literal.getLiteralValue();
238
390
  const text = literal.getText();
239
391
 
240
- // Skip safe numbers
241
392
  if (this.safeNumbers.has(value)) {
242
393
  return;
243
394
  }
244
395
 
245
- // Skip if in acceptable context (enum, const declaration, etc.)
246
396
  if (this.isInAcceptableContext(literal)) {
247
397
  return;
248
398
  }
249
399
 
250
- // Skip if it's an array index or simple loop counter
251
400
  if (this.isArrayIndexOrLoopCounter(literal)) {
252
401
  return;
253
402
  }
254
403
 
255
- // Track for duplicate detection
256
404
  this.trackConstant(constantUsage, `number:${value}`, literal);
257
405
 
258
- // Flag as magic number if value is significant
259
406
  if (Math.abs(value) >= this.minNumberValue || this.isLikelyMagicNumber(literal)) {
260
407
  violations.push(this.createViolation(
261
408
  literal,
@@ -275,40 +422,48 @@ class C024SymbolBasedAnalyzer {
275
422
  const value = literal.getLiteralValue();
276
423
  const text = literal.getText();
277
424
 
278
- // Skip short strings
279
425
  if (value.length < this.minStringLength) {
280
426
  return;
281
427
  }
282
428
 
283
- // Skip acceptable string patterns
284
429
  if (this.isAcceptableString(value)) {
285
430
  return;
286
431
  }
287
432
 
288
- // Skip if in acceptable context
289
433
  if (this.isInAcceptableContext(literal)) {
290
434
  return;
291
435
  }
292
436
 
293
- // Skip if it's a property key or object key
437
+ // NEW: Skip if it's a JSX/Vue attribute value (className, placeholder, etc.)
438
+ if (this.isJsxAttributeValue(literal)) {
439
+ return;
440
+ }
441
+
442
+ // NEW: Skip if it's inside a spread operator (...placeholder)
443
+ if (this.isSpreadElement(literal)) {
444
+ return;
445
+ }
446
+
447
+ // NEW: Skip if it's a common frontend function argument
448
+ if (this.isFrontendFunctionArgument(literal)) {
449
+ console.log('Skipping frontend function argument:', literal.getText());
450
+ return;
451
+ }
452
+
294
453
  if (this.isPropertyKey(literal)) {
295
454
  return;
296
455
  }
297
456
 
298
- // Skip if it's in a QueryBuilder pattern
299
457
  if (this.isQueryBuilderPattern(literal)) {
300
458
  return;
301
459
  }
302
460
 
303
- // Skip template literals that are mostly variables
304
461
  if (this.isTemplateWithVariables(literal)) {
305
462
  return;
306
463
  }
307
464
 
308
- // Track for duplicate detection
309
465
  this.trackConstant(constantUsage, `string:${value}`, literal);
310
466
 
311
- // Flag as magic string if it's in logic
312
467
  if (this.isInLogicContext(literal) || this.isInComparison(literal)) {
313
468
  violations.push(this.createViolation(
314
469
  literal,
@@ -327,7 +482,31 @@ class C024SymbolBasedAnalyzer {
327
482
  const [type, value] = key.split(':', 2);
328
483
  const firstLocation = locations[0];
329
484
 
330
- // Only flag if not already flagged as magic number/string
485
+ // NEW: Skip if the first location is in a frontend/JSX context
486
+ if (this.isJsxAttributeValue(firstLocation)) {
487
+ continue;
488
+ }
489
+
490
+ if (this.isSpreadElement(firstLocation)) {
491
+ continue;
492
+ }
493
+
494
+ if (this.isFrontendFunctionArgument(firstLocation)) {
495
+ continue;
496
+ }
497
+
498
+ // NEW: Filter out locations that are in frontend contexts
499
+ const validLocations = locations.filter(loc => {
500
+ return !this.isJsxAttributeValue(loc) &&
501
+ !this.isSpreadElement(loc) &&
502
+ !this.isFrontendFunctionArgument(loc);
503
+ });
504
+
505
+ // Only flag if there are still enough valid occurrences after filtering
506
+ if (validLocations.length < this.minOccurrences) {
507
+ continue;
508
+ }
509
+
331
510
  const alreadyFlagged = violations.some(v =>
332
511
  v.line === firstLocation.getStartLineNumber() &&
333
512
  v.column === firstLocation.getStart() - firstLocation.getStartLinePos() + 1
@@ -337,7 +516,7 @@ class C024SymbolBasedAnalyzer {
337
516
  violations.push(this.createViolation(
338
517
  firstLocation,
339
518
  sourceFile,
340
- `Duplicate constant '${this.truncate(value, 50)}' used ${locations.length} times. Extract to a named constant.`,
519
+ `Duplicate constant '${this.truncate(value, 50)}' used ${validLocations.length} times. Extract to a named constant.`,
341
520
  'duplicate-constant',
342
521
  value
343
522
  ));
@@ -346,6 +525,350 @@ class C024SymbolBasedAnalyzer {
346
525
  }
347
526
  }
348
527
 
528
+ /**
529
+ * NEW: Check if string literal is a JSX/Vue attribute value
530
+ * Example: <div className="flex gap-2"> or placeholder="委託元を選択してください"
531
+ */
532
+ isJsxAttributeValue(node) {
533
+ let parent = node.getParent();
534
+ let depth = 0;
535
+ const maxDepth = 5;
536
+
537
+ while (parent && depth < maxDepth) {
538
+ const kind = parent.getKind();
539
+
540
+ // Direct JsxAttribute: <Component attribute="value" />
541
+ if (kind === SyntaxKind.JsxAttribute) {
542
+ const attrName = parent.getName?.()?.getText();
543
+ // Always ignore ANY JSX attribute value (not just specific ones)
544
+ // This handles className, id, style, data-*, aria-*, etc.
545
+ return true;
546
+
547
+ // Or if you want to be selective:
548
+ // if (this.frontendIgnoredAttributes.includes(attrName) ||
549
+ // attrName.startsWith('data-') ||
550
+ // attrName.startsWith('aria-')) {
551
+ // return true;
552
+ // }
553
+ }
554
+
555
+ // JsxExpression: <Component className={variable} />
556
+ if (kind === SyntaxKind.JsxExpression) {
557
+ const jsxParent = parent.getParent();
558
+ if (jsxParent && jsxParent.getKind() === SyntaxKind.JsxAttribute) {
559
+ return true;
560
+ }
561
+ }
562
+
563
+ // PropertyAssignment in object literal: { className: "flex gap-3" }
564
+ if (kind === SyntaxKind.PropertyAssignment) {
565
+ const propName = parent.getName?.()?.getText();
566
+ if (this.frontendIgnoredAttributes.includes(propName)) {
567
+ // Check if this is inside JSX context or props object
568
+ if (this.isInsideJsxElement(parent) || this.isInsidePropsObject(parent)) {
569
+ return true;
570
+ }
571
+ }
572
+ }
573
+
574
+ // Template literal in JSX: className={`flex gap-${size}`}
575
+ if (kind === SyntaxKind.TemplateExpression || kind === SyntaxKind.TemplateSpan) {
576
+ if (this.isInsideJsxElement(parent)) {
577
+ return true;
578
+ }
579
+ }
580
+
581
+ // ConditionalExpression in JSX: className={isActive ? "active" : "inactive"}
582
+ if (kind === SyntaxKind.ConditionalExpression) {
583
+ if (this.isInsideJsxElement(parent)) {
584
+ return true;
585
+ }
586
+ }
587
+
588
+ // Binary expression in JSX: className={"flex" + " gap-3"}
589
+ if (kind === SyntaxKind.BinaryExpression) {
590
+ const binaryParent = parent.getParent();
591
+ if (binaryParent &&
592
+ (binaryParent.getKind() === SyntaxKind.JsxExpression ||
593
+ binaryParent.getKind() === SyntaxKind.JsxAttribute)) {
594
+ return true;
595
+ }
596
+ }
597
+
598
+ // ObjectLiteralExpression that's passed as props
599
+ if (kind === SyntaxKind.ObjectLiteralExpression) {
600
+ const objParent = parent.getParent();
601
+ if (objParent) {
602
+ // Check if this object is spread into JSX: <Component {...props} />
603
+ if (objParent.getKind() === SyntaxKind.JsxSpreadAttribute) {
604
+ return true;
605
+ }
606
+ // Check if this object is assigned to a props variable
607
+ if (objParent.getKind() === SyntaxKind.VariableDeclaration) {
608
+ const varName = objParent.getName?.()?.getText();
609
+ if (varName && /props|attributes|attrs|componentProps/i.test(varName)) {
610
+ return true;
611
+ }
612
+ }
613
+ }
614
+ }
615
+
616
+ parent = parent.getParent();
617
+ depth++;
618
+ }
619
+
620
+ return false;
621
+ }
622
+
623
+ /**
624
+ * Helper: Check if node is inside a props object being passed to a component
625
+ */
626
+ isInsidePropsObject(node) {
627
+ let parent = node.getParent();
628
+ let depth = 0;
629
+ const maxDepth = 5;
630
+
631
+ while (parent && depth < maxDepth) {
632
+ const kind = parent.getKind();
633
+
634
+ // Variable declaration with props-like name
635
+ if (kind === SyntaxKind.VariableDeclaration) {
636
+ const varName = parent.getName?.()?.getText();
637
+ if (varName && /props|attributes|attrs|config|options|settings/i.test(varName)) {
638
+ return true;
639
+ }
640
+ }
641
+
642
+ // Object being returned or passed as argument
643
+ if (kind === SyntaxKind.ObjectLiteralExpression) {
644
+ const objParent = parent.getParent();
645
+ if (objParent) {
646
+ // Return statement in component
647
+ if (objParent.getKind() === SyntaxKind.ReturnStatement) {
648
+ return true;
649
+ }
650
+ // Call expression argument (passing props to component)
651
+ if (objParent.getKind() === SyntaxKind.CallExpression) {
652
+ return true;
653
+ }
654
+ }
655
+ }
656
+
657
+ // Arrow function or function that likely returns JSX props
658
+ if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) {
659
+ const funcName = this.getFunctionName(parent);
660
+ if (funcName && /props|attributes|config|get.*Props/i.test(funcName)) {
661
+ return true;
662
+ }
663
+ }
664
+
665
+ parent = parent.getParent();
666
+ depth++;
667
+ }
668
+
669
+ return false;
670
+ }
671
+
672
+ /**
673
+ * Helper: Get function name from various function types
674
+ */
675
+ getFunctionName(functionNode) {
676
+ const parent = functionNode.getParent();
677
+ if (!parent) return null;
678
+
679
+ // Variable declaration: const getProps = () => {}
680
+ if (parent.getKind() === SyntaxKind.VariableDeclaration) {
681
+ return parent.getName?.()?.getText();
682
+ }
683
+
684
+ // Function declaration: function getProps() {}
685
+ if (functionNode.getKind() === SyntaxKind.FunctionDeclaration) {
686
+ return functionNode.getName?.()?.getText();
687
+ }
688
+
689
+ // Method: { getProps() {} }
690
+ if (parent.getKind() === SyntaxKind.MethodDeclaration) {
691
+ return parent.getName?.()?.getText();
692
+ }
693
+
694
+ // Property assignment: { getProps: () => {} }
695
+ if (parent.getKind() === SyntaxKind.PropertyAssignment) {
696
+ return parent.getName?.()?.getText();
697
+ }
698
+
699
+ return null;
700
+ }
701
+
702
+ /**
703
+ * NEW: Check if node is inside JSX element
704
+ */
705
+ isInsideJsxElement(node) {
706
+ let parent = node.getParent();
707
+ let depth = 0;
708
+ const maxDepth = 5;
709
+
710
+ while (parent && depth < maxDepth) {
711
+ const kind = parent.getKind();
712
+ if (kind === SyntaxKind.JsxElement ||
713
+ kind === SyntaxKind.JsxSelfClosingElement ||
714
+ kind === SyntaxKind.JsxFragment) {
715
+ return true;
716
+ }
717
+ parent = parent.getParent();
718
+ depth++;
719
+ }
720
+
721
+ return false;
722
+ }
723
+
724
+ /**
725
+ * NEW: Check if string is inside spread element
726
+ * Example: {...field} or {...placeholder}
727
+ */
728
+ isSpreadElement(node) {
729
+ let parent = node.getParent();
730
+ let depth = 0;
731
+ const maxDepth = 5;
732
+
733
+ while (parent && depth < maxDepth) {
734
+ const kind = parent.getKind();
735
+ if (kind === SyntaxKind.SpreadElement ||
736
+ kind === SyntaxKind.SpreadAssignment ||
737
+ kind === SyntaxKind.JsxSpreadAttribute) {
738
+ return true;
739
+ }
740
+ parent = parent.getParent();
741
+ depth++;
742
+ }
743
+
744
+ return false;
745
+ }
746
+
747
+ /**
748
+ * NEW: Check if string is argument to common frontend functions
749
+ * Example: setValue('invoiceSendFlg', false), resetField('leaseInspectionType')
750
+ */
751
+ isFrontendFunctionArgument(node) {
752
+ let parent = node.getParent();
753
+ let depth = 0;
754
+ const maxDepth = 5; // Increased depth to handle nested calls
755
+
756
+ while (parent && depth < maxDepth) {
757
+ // Check if current parent is a CallExpression
758
+ if (parent.getKind() === SyntaxKind.CallExpression) {
759
+ const callExpr = parent;
760
+ const expression = callExpr.getExpression();
761
+ const funcName = expression.getText();
762
+
763
+ // Check if it's one of the ignored frontend functions
764
+ const matchesFrontendFunc = this.frontendIgnoredFunctions.some(ignoredFunc =>
765
+ funcName.includes(ignoredFunc) || funcName.endsWith(ignoredFunc)
766
+ );
767
+
768
+ if (matchesFrontendFunc) {
769
+ // For most functions, check if the string is the first argument
770
+ const args = callExpr.getArguments();
771
+
772
+ // Special handling for array arguments (like trigger(['field1', 'field2']))
773
+ if (args.length > 0) {
774
+ const firstArg = args[0];
775
+
776
+ // Check if node is the first argument directly
777
+ if (firstArg === node) {
778
+ return true;
779
+ }
780
+
781
+ // Check if node is inside an array that is the first argument
782
+ // Example: trigger(['userEmail', 'userFax'])
783
+ if (firstArg.getKind() === SyntaxKind.ArrayLiteralExpression) {
784
+ const arrayElements = firstArg.getElements();
785
+ if (arrayElements.some(el => el === node || this.isDescendantOf(node, el))) {
786
+ return true;
787
+ }
788
+ }
789
+
790
+ // Check if node is anywhere in the arguments for certain functions
791
+ // This handles cases like: onChange: (event) => { setValue('aprch', null); }
792
+ if (args.some(arg => arg === node || this.isDescendantOf(node, arg))) {
793
+ // Only return true if it's a direct argument, not nested deep in logic
794
+ const directArgIndex = args.findIndex(arg => arg === node);
795
+ if (directArgIndex !== -1) {
796
+ return true;
797
+ }
798
+ }
799
+ }
800
+ }
801
+ }
802
+
803
+ // Special case: Check if we're inside an object literal passed to a frontend function
804
+ // Example: register('aprchFlg', { onChange: ... })
805
+ if (parent.getKind() === SyntaxKind.ObjectLiteralExpression) {
806
+ const objectParent = parent.getParent();
807
+ if (objectParent && objectParent.getKind() === SyntaxKind.CallExpression) {
808
+ const callExpr = objectParent;
809
+ const expression = callExpr.getExpression();
810
+ const funcName = expression.getText();
811
+
812
+ const matchesFrontendFunc = this.frontendIgnoredFunctions.some(ignoredFunc =>
813
+ funcName.includes(ignoredFunc) || funcName.endsWith(ignoredFunc)
814
+ );
815
+
816
+ // If we're in an options object for a frontend function, check nested calls
817
+ if (matchesFrontendFunc) {
818
+ // Continue checking - we might be in a callback
819
+ parent = parent.getParent();
820
+ depth++;
821
+ continue;
822
+ }
823
+ }
824
+ }
825
+
826
+ // Special case: Inside arrow function or function expression (callbacks)
827
+ // Example: onChange: (event) => { trigger(['field']) }
828
+ if (parent.getKind() === SyntaxKind.ArrowFunction ||
829
+ parent.getKind() === SyntaxKind.FunctionExpression) {
830
+ // Check if this function is a callback for a frontend function
831
+ const funcParent = parent.getParent();
832
+ if (funcParent && funcParent.getKind() === SyntaxKind.PropertyAssignment) {
833
+ const propName = funcParent.getName?.()?.getText();
834
+ // Common callback property names in React/Vue forms
835
+ if (['onChange', 'onBlur', 'onFocus', 'onClick', 'onSubmit', 'validator', 'transform'].includes(propName)) {
836
+ // Continue up the tree to find the actual frontend function call
837
+ parent = parent.getParent();
838
+ depth++;
839
+ continue;
840
+ }
841
+ }
842
+ }
843
+
844
+ parent = parent.getParent();
845
+ depth++;
846
+ }
847
+
848
+ return false;
849
+ }
850
+
851
+ /**
852
+ * Helper function to check if node is a descendant of parent
853
+ */
854
+ isDescendantOf(node, parent) {
855
+ if (!node || !parent) return false;
856
+
857
+ let current = node.getParent();
858
+ let depth = 0;
859
+ const maxDepth = 10;
860
+
861
+ while (current && depth < maxDepth) {
862
+ if (current === parent) {
863
+ return true;
864
+ }
865
+ current = current.getParent();
866
+ depth++;
867
+ }
868
+
869
+ return false;
870
+ }
871
+
349
872
  isInAcceptableContext(node) {
350
873
  let parent = node.getParent();
351
874
  let depth = 0;
@@ -354,27 +877,21 @@ class C024SymbolBasedAnalyzer {
354
877
  while (parent && depth < maxDepth) {
355
878
  const kind = parent.getKind();
356
879
 
357
- // Top-level const declaration
358
880
  if (kind === SyntaxKind.VariableDeclaration) {
359
881
  const varDecl = parent;
360
882
  const varStatement = varDecl.getParent()?.getParent();
361
883
  if (varStatement && varStatement.getKind() === SyntaxKind.VariableStatement) {
362
884
  const isConst = varStatement.getDeclarationKind() === 'const';
363
- const isTopLevel = varStatement.getParent()?.getKind() === SyntaxKind.SourceFile;
364
-
365
- // Allow const declarations (top-level or in functions)
366
885
  if (isConst) {
367
886
  return true;
368
887
  }
369
888
  }
370
889
  }
371
890
 
372
- // Enum, interface, type alias
373
891
  if (this.acceptableContexts.includes(kind)) {
374
892
  return true;
375
893
  }
376
894
 
377
- // Object literal that's assigned to a const
378
895
  if (kind === SyntaxKind.ObjectLiteralExpression) {
379
896
  const objectParent = parent.getParent();
380
897
  if (objectParent?.getKind() === SyntaxKind.VariableDeclaration) {
@@ -382,7 +899,6 @@ class C024SymbolBasedAnalyzer {
382
899
  }
383
900
  }
384
901
 
385
- // Array literal assigned to a const (for SQL columns, etc.)
386
902
  if (kind === SyntaxKind.ArrayLiteralExpression) {
387
903
  const arrayParent = parent.getParent();
388
904
  if (arrayParent?.getKind() === SyntaxKind.VariableDeclaration) {
@@ -390,12 +906,10 @@ class C024SymbolBasedAnalyzer {
390
906
  }
391
907
  }
392
908
 
393
- // Decorator arguments (NestJS @Post('search'), @Param('id'))
394
909
  if (kind === SyntaxKind.Decorator) {
395
910
  return true;
396
911
  }
397
912
 
398
- // Call expression arguments for decorators
399
913
  if (kind === SyntaxKind.CallExpression) {
400
914
  const callParent = parent.getParent();
401
915
  if (callParent?.getKind() === SyntaxKind.Decorator) {
@@ -403,12 +917,10 @@ class C024SymbolBasedAnalyzer {
403
917
  }
404
918
  }
405
919
 
406
- // TypeOf expression (typeof x === 'string')
407
920
  if (kind === SyntaxKind.TypeOfExpression) {
408
921
  return true;
409
922
  }
410
923
 
411
- // Binary expression with typeof
412
924
  if (kind === SyntaxKind.BinaryExpression) {
413
925
  const binaryExpr = parent;
414
926
  const left = binaryExpr.getLeft();
@@ -430,12 +942,10 @@ class C024SymbolBasedAnalyzer {
430
942
 
431
943
  const kind = parent.getKind();
432
944
 
433
- // Array element access: arr[0], arr[1]
434
945
  if (kind === SyntaxKind.ElementAccessExpression) {
435
946
  return true;
436
947
  }
437
948
 
438
- // Loop increment: i++, i += 1
439
949
  if (kind === SyntaxKind.BinaryExpression ||
440
950
  kind === SyntaxKind.PostfixUnaryExpression ||
441
951
  kind === SyntaxKind.PrefixUnaryExpression) {
@@ -449,7 +959,6 @@ class C024SymbolBasedAnalyzer {
449
959
  const parent = node.getParent();
450
960
  if (!parent) return false;
451
961
 
452
- // Numbers in comparisons are often magic numbers
453
962
  if (parent.getKind() === SyntaxKind.BinaryExpression) {
454
963
  const binaryExpr = parent;
455
964
  const operator = binaryExpr.getOperatorToken().getText();
@@ -458,7 +967,6 @@ class C024SymbolBasedAnalyzer {
458
967
  }
459
968
  }
460
969
 
461
- // Numbers in calculations
462
970
  if (parent.getKind() === SyntaxKind.BinaryExpression) {
463
971
  const binaryExpr = parent;
464
972
  const operator = binaryExpr.getOperatorToken().getText();
@@ -478,18 +986,15 @@ class C024SymbolBasedAnalyzer {
478
986
  const parent = node.getParent();
479
987
  if (!parent) return false;
480
988
 
481
- // Object property key
482
989
  if (parent.getKind() === SyntaxKind.PropertyAssignment) {
483
990
  const prop = parent;
484
991
  return prop.getInitializer() !== node;
485
992
  }
486
993
 
487
- // Dot notation property access
488
994
  if (parent.getKind() === SyntaxKind.PropertyAccessExpression) {
489
995
  return true;
490
996
  }
491
997
 
492
- // Parameter decorator name: @Param('cm_cst_id')
493
998
  if (parent.getKind() === SyntaxKind.CallExpression) {
494
999
  const callParent = parent.getParent();
495
1000
  if (callParent?.getKind() === SyntaxKind.Decorator) {
@@ -505,28 +1010,23 @@ class C024SymbolBasedAnalyzer {
505
1010
  let depth = 0;
506
1011
  const maxDepth = 5;
507
1012
 
508
- // Walk up the tree to find if we're in a QueryBuilder call
509
1013
  while (parent && depth < maxDepth) {
510
1014
  const kind = parent.getKind();
511
1015
 
512
- // Check if part of a call expression
513
1016
  if (kind === SyntaxKind.CallExpression) {
514
1017
  const callExpr = parent;
515
1018
  const expression = callExpr.getExpression();
516
1019
  const exprText = expression.getText();
517
1020
 
518
- // QueryBuilder methods: .where(), .andWhere(), .orWhere(), etc.
519
1021
  if (/\.(where|andWhere|orWhere|having|andHaving|orHaving|select|addSelect|leftJoin|innerJoin|join|orderBy|groupBy|setParameter)$/i.test(exprText)) {
520
1022
  return true;
521
1023
  }
522
1024
 
523
- // Also check for common ORM query builders
524
1025
  if (/(queryBuilder|qb|query)\.(where|andWhere|orWhere)/i.test(exprText)) {
525
1026
  return true;
526
1027
  }
527
1028
  }
528
1029
 
529
- // Skip through conditional expressions (ternary operators)
530
1030
  if (kind === SyntaxKind.ConditionalExpression) {
531
1031
  parent = parent.getParent();
532
1032
  depth++;
@@ -558,13 +1058,11 @@ class C024SymbolBasedAnalyzer {
558
1058
 
559
1059
  while (parent && depth < maxDepth) {
560
1060
  const kind = parent.getKind();
561
- // Skip when part of an element access chain like arr['key1']['key2']
562
- // Walk up any nested ElementAccessExpression hierarchy
1061
+
563
1062
  if (kind === SyntaxKind.ElementAccessExpression) {
564
- return false; // skip, this is not a logic context — it's array/object access
1063
+ return false;
565
1064
  }
566
1065
 
567
- // Inside function body, method, arrow function
568
1066
  if (kind === SyntaxKind.FunctionDeclaration ||
569
1067
  kind === SyntaxKind.MethodDeclaration ||
570
1068
  kind === SyntaxKind.ArrowFunction ||
@@ -572,7 +1070,6 @@ class C024SymbolBasedAnalyzer {
572
1070
  return true;
573
1071
  }
574
1072
 
575
- // Inside if statement, switch, loop
576
1073
  if (kind === SyntaxKind.IfStatement ||
577
1074
  kind === SyntaxKind.SwitchStatement ||
578
1075
  kind === SyntaxKind.ForStatement ||
@@ -592,7 +1089,6 @@ class C024SymbolBasedAnalyzer {
592
1089
  const parent = node.getParent();
593
1090
  if (!parent) return false;
594
1091
 
595
- // Direct comparison: if (type === 'GOLD')
596
1092
  if (parent.getKind() === SyntaxKind.BinaryExpression) {
597
1093
  const binaryExpr = parent;
598
1094
  const operator = binaryExpr.getOperatorToken().getText();
@@ -601,7 +1097,6 @@ class C024SymbolBasedAnalyzer {
601
1097
  }
602
1098
  }
603
1099
 
604
- // Case clause: case 'GOLD':
605
1100
  if (parent.getKind() === SyntaxKind.CaseClause) {
606
1101
  return true;
607
1102
  }