@sun-asterisk/sunlint 1.2.2 → 1.3.1

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 (124) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/CONTRIBUTING.md +1654 -66
  3. package/README.md +19 -6
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/engines/engines-enhanced.json +86 -0
  7. package/config/engines/semantic-config.json +114 -0
  8. package/config/eslint-rule-mapping.json +50 -38
  9. package/config/large-project.json +143 -0
  10. package/config/presets/all.json +0 -1
  11. package/config/release.json +70 -0
  12. package/config/rule-analysis-strategies.js +23 -4
  13. package/config/rules/S027-categories.json +122 -0
  14. package/config/rules/enhanced-rules-registry.json +2564 -0
  15. package/config/rules/rules-registry-generated.json +785 -837
  16. package/config/rules/rules-registry.json +13 -1
  17. package/core/adapters/sunlint-rule-adapter.js +25 -30
  18. package/core/analysis-orchestrator.js +42 -2
  19. package/core/categories.js +52 -0
  20. package/core/category-constants.js +39 -0
  21. package/core/cli-action-handler.js +53 -32
  22. package/core/cli-program.js +11 -3
  23. package/core/config-manager.js +111 -0
  24. package/core/config-merger.js +88 -0
  25. package/core/constants/categories.js +168 -0
  26. package/core/constants/defaults.js +165 -0
  27. package/core/constants/engines.js +185 -0
  28. package/core/constants/index.js +30 -0
  29. package/core/constants/rules.js +215 -0
  30. package/core/enhanced-rules-registry.js +3 -3
  31. package/core/file-targeting-service.js +128 -7
  32. package/core/interfaces/rule-plugin.interface.js +207 -0
  33. package/core/plugin-manager.js +448 -0
  34. package/core/rule-selection-service.js +42 -15
  35. package/core/semantic-engine.js +658 -0
  36. package/core/semantic-rule-base.js +433 -0
  37. package/core/unified-rule-registry.js +484 -0
  38. package/docs/COMMAND-EXAMPLES.md +134 -0
  39. package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
  40. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  41. package/engines/core/base-engine.js +249 -0
  42. package/engines/engine-factory.js +275 -0
  43. package/engines/eslint-engine.js +171 -19
  44. package/engines/heuristic-engine.js +569 -78
  45. package/integrations/eslint/plugin/index.js +26 -28
  46. package/origin-rules/common-en.md +8 -8
  47. package/package.json +10 -6
  48. package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
  49. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  50. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  51. package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
  52. package/rules/common/C033_separate_service_repository/README.md +78 -0
  53. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  54. package/rules/common/C033_separate_service_repository/config.json +50 -0
  55. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  56. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  57. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  58. package/rules/common/C035_error_logging_context/analyzer.js +230 -0
  59. package/rules/common/C035_error_logging_context/config.json +54 -0
  60. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  61. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  62. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  63. package/rules/common/C040_centralized_validation/config.json +46 -0
  64. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  65. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  66. package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
  67. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
  68. package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
  69. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  70. package/rules/common/C076_explicit_function_types/README.md +30 -0
  71. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  72. package/rules/common/C076_explicit_function_types/config.json +15 -0
  73. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  74. package/rules/index.js +8 -0
  75. package/rules/parser/rule-parser.js +13 -2
  76. package/rules/security/S005_no_origin_auth/README.md +226 -0
  77. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  78. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  79. package/rules/security/S005_no_origin_auth/config.json +85 -0
  80. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  81. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  82. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  83. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  84. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  85. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  86. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  87. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  88. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  89. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  90. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  91. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  92. package/scripts/category-manager.js +150 -0
  93. package/scripts/generate-rules-registry.js +88 -0
  94. package/scripts/migrate-rule-registry.js +157 -0
  95. package/scripts/prepare-release.sh +1 -1
  96. package/scripts/validate-system.js +48 -0
  97. package/.sunlint.json +0 -35
  98. package/config/README.md +0 -88
  99. package/config/engines/eslint-rule-mapping.json +0 -74
  100. package/config/schemas/sunlint-schema.json +0 -0
  101. package/config/testing/test-s005-working.ts +0 -22
  102. package/core/multi-rule-runner.js +0 -0
  103. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  104. package/docs/FUTURE_PACKAGES.md +0 -83
  105. package/docs/HEURISTIC_VS_AI.md +0 -113
  106. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  107. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  108. package/docs/RELEASE_GUIDE.md +0 -230
  109. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  110. package/engines/tree-sitter-parser.js +0 -0
  111. package/engines/universal-ast-engine.js +0 -0
  112. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  113. package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
  114. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
  115. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
  116. package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
  117. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
  118. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
  119. package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
  120. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
  121. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
  122. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
  123. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
  124. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
@@ -0,0 +1,454 @@
1
+ /**
2
+ * C035 Symbol-based Analyzer - Advanced Error Logging Context Analysis
3
+ * Purpose: Use AST + Symbol Resolution to analyze log content quality in catch blocks
4
+ */
5
+
6
+ const { SyntaxKind } = require('ts-morph');
7
+
8
+ class C035SymbolBasedAnalyzer {
9
+ constructor(semanticEngine = null) {
10
+ this.ruleId = 'C035';
11
+ this.ruleName = 'Error Logging Context Analysis (Symbol-Based)';
12
+ this.semanticEngine = semanticEngine;
13
+ this.verbose = false;
14
+
15
+ // Logger method patterns (extensible)
16
+ this.loggerPatterns = {
17
+ console: ['log', 'error', 'warn', 'info'],
18
+ logger: ['log', 'error', 'warn', 'info', 'debug'],
19
+ log: ['error', 'warn', 'info', 'debug'],
20
+ winston: ['log', 'error', 'warn', 'info', 'debug'],
21
+ bunyan: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'],
22
+ pino: ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
23
+ };
24
+
25
+ // Required context elements
26
+ this.requiredContext = {
27
+ errorInfo: ['message', 'stack', 'error', 'err'],
28
+ identifier: ['id', 'requestId', 'userId', 'transactionId', 'correlationId'],
29
+ context: ['service', 'method', 'operation', 'module', 'component']
30
+ };
31
+
32
+ // Sensitive data patterns to flag (more specific to avoid false positives)
33
+ this.sensitivePatterns = [
34
+ 'password', 'passwd', 'pwd', 'pass',
35
+ 'token', 'jwt', 'secret', 'privatekey', 'publickey', 'apikey', 'accesskey',
36
+ 'ssn', 'social', 'creditcard', 'cardnumber', 'cvv', 'pin',
37
+ 'authorization', 'bearer'
38
+ ];
39
+ }
40
+
41
+ async initialize(semanticEngine = null) {
42
+ if (semanticEngine) {
43
+ this.semanticEngine = semanticEngine;
44
+ }
45
+ this.verbose = semanticEngine?.verbose || false;
46
+
47
+ if (process.env.SUNLINT_DEBUG) {
48
+ console.log(`🔧 [C035 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
49
+ }
50
+ }
51
+
52
+ async analyzeFileBasic(filePath, options = {}) {
53
+ // This is the main entry point called by the hybrid analyzer
54
+ return await this.analyzeFileWithSymbols(filePath, options);
55
+ }
56
+
57
+ async analyzeFileWithSymbols(filePath, options = {}) {
58
+ const violations = [];
59
+
60
+ // Enable verbose mode if requested
61
+ const verbose = options.verbose || this.verbose;
62
+
63
+ if (!this.semanticEngine?.project) {
64
+ if (verbose) {
65
+ console.warn('[C035 Symbol-Based] No semantic engine available, skipping analysis');
66
+ }
67
+ return violations;
68
+ }
69
+
70
+ if (verbose) {
71
+ console.log(`🔍 [C035 Symbol-Based] Starting analysis for ${filePath}`);
72
+ }
73
+
74
+ try {
75
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
76
+ if (!sourceFile) {
77
+ return violations;
78
+ }
79
+
80
+ // Find all try-catch statements in the file
81
+ const tryCatchStatements = sourceFile.getDescendantsOfKind(SyntaxKind.TryStatement);
82
+
83
+ if (verbose) {
84
+ console.log(`🔍 [C035 Symbol-Based] Found ${tryCatchStatements.length} try-catch statements`);
85
+ }
86
+
87
+ for (const tryStatement of tryCatchStatements) {
88
+ const catchClause = tryStatement.getCatchClause();
89
+ if (catchClause) {
90
+ const catchViolations = this.analyzeCatchBlock(catchClause, sourceFile, filePath, verbose);
91
+ violations.push(...catchViolations);
92
+ }
93
+ }
94
+
95
+ if (verbose) {
96
+ console.log(`🔍 [C035 Symbol-Based] Total violations found: ${violations.length}`);
97
+ }
98
+
99
+ return violations;
100
+ } catch (error) {
101
+ if (verbose) {
102
+ console.warn(`[C035 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
103
+ }
104
+ return violations;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Analyze catch block for logging context violations
110
+ */
111
+ analyzeCatchBlock(catchClause, sourceFile, filePath, verbose = false) {
112
+ const violations = [];
113
+
114
+ if (verbose) {
115
+ console.log(`🔍 [C035 Symbol-Based] Analyzing catch block in ${filePath}`);
116
+ }
117
+
118
+ // Get catch parameter (e, error, err, etc.)
119
+ const catchParameter = catchClause.getVariableDeclaration();
120
+ const errorVarName = catchParameter?.getName() || 'e';
121
+
122
+ if (verbose) {
123
+ console.log(`🔍 [C035 Symbol-Based] Error variable name: ${errorVarName}`);
124
+ }
125
+
126
+ // Find all log calls within catch block
127
+ const catchBlock = catchClause.getBlock();
128
+ const logCalls = this.findLogCallsInBlock(catchBlock);
129
+
130
+ if (verbose) {
131
+ console.log(`🔍 [C035 Symbol-Based] Found ${logCalls.length} log calls in catch block`);
132
+ }
133
+
134
+ if (logCalls.length === 0) {
135
+ // No logging found - but this is C029's concern, not C035
136
+ // We only analyze existing logs for quality
137
+ return violations;
138
+ }
139
+
140
+ // Analyze each log call for context quality
141
+ for (const logCall of logCalls) {
142
+ if (verbose) {
143
+ console.log(`🔍 [C035 Symbol-Based] Analyzing log call: ${logCall.getText()}`);
144
+ }
145
+ const logViolations = this.analyzeLogCall(logCall, errorVarName, sourceFile, filePath, verbose);
146
+ violations.push(...logViolations);
147
+ }
148
+
149
+ return violations;
150
+ }
151
+
152
+ /**
153
+ * Find all logging method calls within a block
154
+ */
155
+ findLogCallsInBlock(block) {
156
+ const logCalls = [];
157
+ const callExpressions = block.getDescendantsOfKind(SyntaxKind.CallExpression);
158
+
159
+ for (const callExpr of callExpressions) {
160
+ if (this.isLoggerCall(callExpr)) {
161
+ logCalls.push(callExpr);
162
+ }
163
+ }
164
+
165
+ return logCalls;
166
+ }
167
+
168
+ /**
169
+ * Check if a call expression is a logger call
170
+ */
171
+ isLoggerCall(callExpr) {
172
+ const expression = callExpr.getExpression();
173
+
174
+ // Handle property access (logger.error, console.log, etc.)
175
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
176
+ const objectName = expression.getExpression().getText().toLowerCase();
177
+ const methodName = expression.getName().toLowerCase();
178
+
179
+ // Check against known logger patterns
180
+ for (const [loggerName, methods] of Object.entries(this.loggerPatterns)) {
181
+ if (objectName.includes(loggerName) && methods.includes(methodName)) {
182
+ return true;
183
+ }
184
+ }
185
+ }
186
+
187
+ return false;
188
+ }
189
+
190
+ /**
191
+ * Analyze individual log call for context quality
192
+ */
193
+ analyzeLogCall(logCall, errorVarName, sourceFile, filePath, verbose = false) {
194
+ const violations = [];
195
+ const lineNumber = logCall.getStartLineNumber();
196
+ const columnNumber = logCall.getStart() - logCall.getStartLinePos();
197
+
198
+ const args = logCall.getArguments();
199
+ if (args.length === 0) {
200
+ return violations; // No arguments to analyze
201
+ }
202
+
203
+ // Analyze logging structure and content
204
+ const analysis = this.analyzeLogArguments(args, errorVarName, verbose);
205
+
206
+ // Check for violations
207
+ if (!analysis.isStructured) {
208
+ violations.push({
209
+ ruleId: this.ruleId,
210
+ severity: 'warning',
211
+ message: 'Error logging should use structured format (object) instead of string concatenation',
212
+ source: this.ruleId,
213
+ file: filePath,
214
+ line: lineNumber,
215
+ column: columnNumber,
216
+ description: `[SYMBOL-BASED] Non-structured logging detected. Use object format for better parsing and monitoring.`,
217
+ suggestion: 'Use logger.error("message", { error: e.message, context: {...} }) instead of string concatenation',
218
+ category: 'logging'
219
+ });
220
+ }
221
+
222
+ if (!analysis.hasRequiredContext) {
223
+ violations.push({
224
+ ruleId: this.ruleId,
225
+ severity: 'warning',
226
+ message: 'Error logging missing required context information',
227
+ source: this.ruleId,
228
+ file: filePath,
229
+ line: lineNumber,
230
+ column: columnNumber,
231
+ description: `[SYMBOL-BASED] Missing context: ${analysis.missingContext.join(', ')}. Include identifiers and operation context.`,
232
+ suggestion: 'Add requestId, userId, and operation context to log for better traceability',
233
+ category: 'logging'
234
+ });
235
+ }
236
+
237
+ if (analysis.hasSensitiveData) {
238
+ violations.push({
239
+ ruleId: this.ruleId,
240
+ severity: 'error',
241
+ message: 'Error logging contains potentially sensitive data',
242
+ source: this.ruleId,
243
+ file: filePath,
244
+ line: lineNumber,
245
+ column: columnNumber,
246
+ description: `[SYMBOL-BASED] Sensitive patterns detected: ${analysis.sensitivePatterns.join(', ')}. Mask or exclude sensitive data.`,
247
+ suggestion: 'Mask sensitive data: password.substring(0,2) + "***" or exclude entirely',
248
+ category: 'security'
249
+ });
250
+ }
251
+
252
+ return violations;
253
+ }
254
+
255
+ /**
256
+ * Analyze log arguments for structure, context, and sensitive data
257
+ */
258
+ analyzeLogArguments(args, errorVarName, verbose = false) {
259
+ const analysis = {
260
+ isStructured: false,
261
+ hasRequiredContext: false,
262
+ hasSensitiveData: false,
263
+ missingContext: [],
264
+ sensitivePatterns: []
265
+ };
266
+
267
+ // Check if any argument is an object (structured logging)
268
+ for (const arg of args) {
269
+ if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
270
+ analysis.isStructured = true;
271
+
272
+ // Analyze object properties for context and sensitive data
273
+ const properties = arg.getProperties();
274
+ this.analyzeObjectProperties(properties, analysis, verbose);
275
+ break;
276
+ }
277
+ }
278
+
279
+ // If not structured, check for string concatenation patterns
280
+ if (!analysis.isStructured) {
281
+ for (const arg of args) {
282
+ const argText = arg.getText().toLowerCase();
283
+ this.checkForSensitiveDataInText(argText, analysis);
284
+ }
285
+ }
286
+
287
+ // Check required context
288
+ this.validateRequiredContext(analysis);
289
+
290
+ return analysis;
291
+ }
292
+
293
+ /**
294
+ * Analyze object literal properties for context and sensitive data
295
+ */
296
+ analyzeObjectProperties(properties, analysis, verbose = false) {
297
+ const foundContext = {
298
+ errorInfo: false,
299
+ identifier: false,
300
+ context: false
301
+ };
302
+
303
+ if (verbose) {
304
+ console.log(`🔍 [C035 Symbol-Based] Analyzing ${properties.length} object properties`);
305
+ }
306
+
307
+ for (const prop of properties) {
308
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
309
+ const propName = prop.getName()?.toLowerCase() || '';
310
+
311
+ if (verbose) {
312
+ console.log(`🔍 [C035 Symbol-Based] Checking property: '${propName}' (kind: PropertyAssignment)`);
313
+ }
314
+ } else if (prop.getKind() === SyntaxKind.ShorthandPropertyAssignment) {
315
+ const propName = prop.getName()?.toLowerCase() || '';
316
+
317
+ if (verbose) {
318
+ console.log(`🔍 [C035 Symbol-Based] Checking shorthand property: '${propName}' (kind: ShorthandPropertyAssignment)`);
319
+ }
320
+
321
+ // Check for required context - same logic for shorthand properties
322
+ if (this.requiredContext.errorInfo.some(ctx => propName.includes(ctx))) {
323
+ foundContext.errorInfo = true;
324
+ if (verbose) {
325
+ console.log(`🔍 [C035 Symbol-Based] Found error info in shorthand: '${propName}'`);
326
+ }
327
+ }
328
+ if (this.requiredContext.identifier.some(ctx => propName.includes(ctx))) {
329
+ foundContext.identifier = true;
330
+ if (verbose) {
331
+ console.log(`🔍 [C035 Symbol-Based] Found identifier in shorthand: '${propName}'`);
332
+ }
333
+ }
334
+ if (this.requiredContext.context.some(ctx => propName.includes(ctx))) {
335
+ foundContext.context = true;
336
+ if (verbose) {
337
+ console.log(`🔍 [C035 Symbol-Based] Found context in shorthand: '${propName}'`);
338
+ }
339
+ }
340
+
341
+ // Check for sensitive data in shorthand properties too
342
+ const matchingSensitivePattern = this.sensitivePatterns.find(pattern => {
343
+ const regex = new RegExp(`\\b${pattern}\\b`, 'i');
344
+ return regex.test(propName);
345
+ });
346
+
347
+ if (matchingSensitivePattern) {
348
+ analysis.hasSensitiveData = true;
349
+ analysis.sensitivePatterns.push(propName);
350
+ if (verbose) {
351
+ console.log(`🔍 [C035 Symbol-Based] Sensitive pattern detected in shorthand: '${propName}' matches '${matchingSensitivePattern}'`);
352
+ }
353
+ }
354
+ } else {
355
+ if (verbose) {
356
+ console.log(`🔍 [C035 Symbol-Based] Skipping property with kind: ${prop.getKindName()}`);
357
+ }
358
+ }
359
+
360
+ // Original PropertyAssignment logic
361
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
362
+ const propName = prop.getName()?.toLowerCase() || '';
363
+
364
+ if (verbose) {
365
+ console.log(`🔍 [C035 Symbol-Based] Checking property: '${propName}'`);
366
+ }
367
+
368
+ // Check for required context
369
+ if (this.requiredContext.errorInfo.some(ctx => propName.includes(ctx))) {
370
+ foundContext.errorInfo = true;
371
+ if (verbose) {
372
+ console.log(`🔍 [C035 Symbol-Based] Found error info: '${propName}'`);
373
+ }
374
+ }
375
+ if (this.requiredContext.identifier.some(ctx => propName.includes(ctx))) {
376
+ foundContext.identifier = true;
377
+ if (verbose) {
378
+ console.log(`🔍 [C035 Symbol-Based] Found identifier: '${propName}'`);
379
+ }
380
+ }
381
+ if (this.requiredContext.context.some(ctx => propName.includes(ctx))) {
382
+ foundContext.context = true;
383
+ if (verbose) {
384
+ console.log(`🔍 [C035 Symbol-Based] Found context: '${propName}'`);
385
+ }
386
+ }
387
+
388
+ // Check for sensitive data (use word boundaries to avoid false positives)
389
+ const matchingSensitivePattern = this.sensitivePatterns.find(pattern => {
390
+ const regex = new RegExp(`\\b${pattern}\\b`, 'i');
391
+ return regex.test(propName);
392
+ });
393
+
394
+ if (matchingSensitivePattern) {
395
+ analysis.hasSensitiveData = true;
396
+ analysis.sensitivePatterns.push(propName);
397
+ if (verbose) {
398
+ console.log(`🔍 [C035 Symbol-Based] Sensitive pattern detected: '${propName}' matches '${matchingSensitivePattern}'`);
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ // Update analysis based on found context
405
+ // For structured logs, be more lenient - having error info is sufficient
406
+ // Identifier and context are nice-to-have but not required for structured logs
407
+ analysis.hasRequiredContext = foundContext.errorInfo ||
408
+ (foundContext.identifier && foundContext.context);
409
+
410
+ if (verbose) {
411
+ console.log(`🔍 [C035 Symbol-Based] Context found - errorInfo: ${foundContext.errorInfo}, identifier: ${foundContext.identifier}, context: ${foundContext.context}`);
412
+ console.log(`🔍 [C035 Symbol-Based] hasRequiredContext: ${analysis.hasRequiredContext}`);
413
+ }
414
+
415
+ // Only flag missing context if there's no error info at all
416
+ // For structured logs with error object, consider it sufficient
417
+ if (!foundContext.errorInfo && !foundContext.identifier) {
418
+ analysis.missingContext.push('error information or identifier');
419
+ }
420
+ // Remove the overly strict context requirement for structured logs with error info
421
+ // Context is nice-to-have but not required when we have structured error info
422
+ }
423
+
424
+ /**
425
+ * Check text for sensitive data patterns
426
+ */
427
+ checkForSensitiveDataInText(text, analysis) {
428
+ for (const pattern of this.sensitivePatterns) {
429
+ if (text.includes(pattern)) {
430
+ analysis.hasSensitiveData = true;
431
+ analysis.sensitivePatterns.push(pattern);
432
+ }
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Validate required context elements
438
+ */
439
+ validateRequiredContext(analysis) {
440
+ // For structured logs, context validation is already handled in analyzeObjectProperties
441
+ if (analysis.isStructured) {
442
+ // If structured and has error info, consider it sufficient
443
+ if (analysis.hasRequiredContext && analysis.missingContext.length === 0) {
444
+ return; // Already validated in analyzeObjectProperties
445
+ }
446
+ } else {
447
+ // For non-structured logs, we can't reliably detect context
448
+ analysis.missingContext.push('structured format required for context validation');
449
+ analysis.hasRequiredContext = false;
450
+ }
451
+ }
452
+ }
453
+
454
+ module.exports = C035SymbolBasedAnalyzer;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * C040 Main Analyzer - Symbol-based with minimal regex fallback
3
+ * Primary: Symbol-based analysis (95% cases)
4
+ * Fallback: Regex-based only when symbol analysis completely fails
5
+ */
6
+
7
+ const C040SymbolBasedAnalyzer = require('./symbol-based-analyzer');
8
+ const C040RegexBasedAnalyzer = require('./regex-based-analyzer');
9
+
10
+ class C040Analyzer {
11
+ constructor(options = {}) {
12
+ if (process.env.SUNLINT_DEBUG) {
13
+ console.log(`🔧 [C040] Constructor called with options:`, !!options);
14
+ console.log(`🔧 [C040] Options type:`, typeof options, Object.keys(options || {}));
15
+ }
16
+
17
+ this.ruleId = 'C040';
18
+ this.ruleName = 'Centralized Validation Logic';
19
+ this.description = 'Don\'t scatter validation logic across multiple classes - Move validation to dedicated validators';
20
+ this.semanticEngine = options.semanticEngine || null;
21
+ this.verbose = options.verbose || false;
22
+
23
+ // Initialize analyzers
24
+ this.symbolBasedAnalyzer = new C040SymbolBasedAnalyzer(this.semanticEngine);
25
+ this.regexBasedAnalyzer = new C040RegexBasedAnalyzer(this.semanticEngine);
26
+
27
+ // Configuration
28
+ this.config = {
29
+ useSymbolBased: true, // Primary approach
30
+ fallbackToRegex: true, // Only when symbol fails completely
31
+ symbolBasedOnly: false // Can be set to true for pure mode
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Initialize with semantic engine
37
+ */
38
+ async initialize(semanticEngine = null) {
39
+ if (semanticEngine) {
40
+ this.semanticEngine = semanticEngine;
41
+ }
42
+ this.verbose = semanticEngine?.verbose || false;
43
+
44
+ // Initialize both analyzers
45
+ await this.symbolBasedAnalyzer.initialize(semanticEngine);
46
+ await this.regexBasedAnalyzer.initialize(semanticEngine);
47
+
48
+ if (this.verbose) {
49
+ console.log(`[DEBUG] 🔧 C040: Analyzer initialized - Symbol-based: ✅, Regex fallback: ${this.config.fallbackToRegex ? '✅' : '❌'}`);
50
+ }
51
+ }
52
+
53
+ async analyze(files, language, options = {}) {
54
+ const violations = [];
55
+ let symbolCount = 0;
56
+ let regexCount = 0;
57
+
58
+ for (const filePath of files) {
59
+ try {
60
+ const fileViolations = await this.analyzeFile(filePath, options);
61
+ violations.push(...fileViolations);
62
+
63
+ // Count strategy usage
64
+ const strategy = fileViolations[0]?.analysisStrategy;
65
+ if (strategy === 'symbol-based') symbolCount++;
66
+ else if (strategy === 'regex-fallback') regexCount++;
67
+
68
+ } catch (error) {
69
+ if (this.verbose) {
70
+ console.warn(`[C040] Analysis failed for ${filePath}:`, error.message);
71
+ }
72
+ }
73
+ }
74
+
75
+ // Summary of strategy usage
76
+ if (this.verbose && (symbolCount > 0 || regexCount > 0)) {
77
+ console.log(`📊 [C040-SUMMARY] Analysis strategy usage:`);
78
+ console.log(` 🧠 Symbol-based: ${symbolCount} files`);
79
+ console.log(` 🔄 Regex-fallback: ${regexCount} files`);
80
+ console.log(` 📈 Coverage: ${symbolCount}/${symbolCount + regexCount} files used primary strategy`);
81
+ }
82
+
83
+ return violations;
84
+ }
85
+
86
+ async analyzeFile(filePath, options = {}) {
87
+ // 1. Try Symbol-based analysis first (primary)
88
+ if (this.config.useSymbolBased && this.semanticEngine?.project) {
89
+ try {
90
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
91
+ if (sourceFile) {
92
+ const violations = await this.symbolBasedAnalyzer.analyzeFile(filePath, options);
93
+
94
+ if (this.verbose) {
95
+ console.log(`🧠 [C040-SYMBOL] ${filePath}: Found ${violations.length} violations`);
96
+ }
97
+
98
+ return violations.map(v => ({ ...v, analysisStrategy: 'symbol-based' }));
99
+ } else {
100
+ if (this.verbose) {
101
+ console.log(`⚠️ [C040-SYMBOL] ${filePath}: Source file not found in ts-morph project, falling back to regex`);
102
+ }
103
+ }
104
+ } catch (error) {
105
+ if (this.verbose) {
106
+ console.warn(`❌ [C040-SYMBOL] ${filePath}: Symbol analysis failed, falling back to regex:`, error.message);
107
+ }
108
+ }
109
+ } else {
110
+ if (this.verbose) {
111
+ const reason = !this.config.useSymbolBased ? 'Symbol-based disabled' : 'No semantic engine';
112
+ console.log(`⚠️ [C040] ${filePath}: Skipping symbol analysis (${reason}), using regex`);
113
+ }
114
+ }
115
+
116
+ // 2. Fallback to Regex-based analysis (only if symbol fails or unavailable)
117
+ if (this.config.fallbackToRegex && !this.config.symbolBasedOnly) {
118
+ try {
119
+ const violations = await this.regexBasedAnalyzer.analyzeFileBasic(filePath, options);
120
+
121
+ if (this.verbose) {
122
+ console.log(`🔄 [C040-REGEX] ${filePath}: Found ${violations.length} violations`);
123
+ }
124
+
125
+ return violations.map(v => ({ ...v, analysisStrategy: 'regex-fallback' }));
126
+ } catch (error) {
127
+ if (this.verbose) {
128
+ console.warn(`❌ [C040-REGEX] ${filePath}: Regex fallback also failed:`, error.message);
129
+ }
130
+ }
131
+ }
132
+
133
+ return [];
134
+ }
135
+
136
+ // Legacy compatibility methods
137
+ async analyzeWithSemantics(filePath, options = {}) {
138
+ return await this.analyzeFile(filePath, options);
139
+ }
140
+
141
+ async analyzeFileBasic(filePath, options = {}) {
142
+ // Force regex-based for legacy compatibility
143
+ const violations = await this.regexBasedAnalyzer.analyzeFileBasic(filePath, options);
144
+ return violations.map(v => ({ ...v, analysisStrategy: 'regex-legacy' }));
145
+ }
146
+
147
+ // Configuration methods
148
+ enableSymbolBasedOnly() {
149
+ this.config.symbolBasedOnly = true;
150
+ this.config.fallbackToRegex = false;
151
+ if (this.verbose) {
152
+ console.log(`[C040] Switched to symbol-based only mode`);
153
+ }
154
+ }
155
+
156
+ enableHybridMode() {
157
+ this.config.symbolBasedOnly = false;
158
+ this.config.fallbackToRegex = true;
159
+ if (this.verbose) {
160
+ console.log(`[C040] Switched to hybrid mode (symbol-based + regex fallback)`);
161
+ }
162
+ }
163
+ }
164
+
165
+ module.exports = C040Analyzer;
@@ -0,0 +1,46 @@
1
+ {
2
+ "rule": "C040",
3
+ "name": "Centralized Validation Logic",
4
+ "description": "Don't scatter validation logic across multiple classes",
5
+ "category": "code-quality",
6
+ "languages": ["javascript", "typescript"],
7
+ "applies_to": ["all"],
8
+ "strategies": ["symbol-based", "regex"],
9
+ "priority": "hybrid",
10
+ "severity": "major",
11
+ "enabled": true,
12
+ "options": {
13
+ "minCentralizationScore": 70,
14
+ "maxDuplicationCount": 3,
15
+ "frameworkDetection": true,
16
+ "layerDetection": true,
17
+ "validationPatterns": [
18
+ "validate*",
19
+ "*Validator",
20
+ "isValid*",
21
+ "ensure*",
22
+ "check*"
23
+ ],
24
+ "frameworkKeywords": [
25
+ "zod",
26
+ "joi",
27
+ "yup",
28
+ "class-validator",
29
+ "ajv",
30
+ "checkSchema",
31
+ "validateSync"
32
+ ],
33
+ "layerPatterns": {
34
+ "controller": ["**/controllers/**", "**/controller/**", "**/*Controller*", "**/*controller*"],
35
+ "service": ["**/services/**", "**/service/**", "**/*Service*", "**/*service*"],
36
+ "repository": ["**/repositories/**", "**/repository/**", "**/*Repository*", "**/*repository*"],
37
+ "validator": ["**/validators/**", "**/validation/**", "**/*Validator*", "**/*validator*"]
38
+ },
39
+ "errorTypes": [
40
+ "ValidationError",
41
+ "BadRequest",
42
+ "InvalidInput",
43
+ "TypeError"
44
+ ]
45
+ }
46
+ }