@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.
- package/config/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- 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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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 ${
|
|
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
|
-
|
|
562
|
-
// Walk up any nested ElementAccessExpression hierarchy
|
|
1061
|
+
|
|
563
1062
|
if (kind === SyntaxKind.ElementAccessExpression) {
|
|
564
|
-
return false;
|
|
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
|
}
|