@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.
- package/CHANGELOG.md +107 -1
- package/CONTRIBUTING.md +1654 -66
- package/README.md +19 -6
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +23 -4
- package/config/rules/S027-categories.json +122 -0
- package/config/rules/enhanced-rules-registry.json +2564 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/config/rules/rules-registry.json +13 -1
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +53 -32
- package/core/cli-program.js +11 -3
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +88 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/enhanced-rules-registry.js +3 -3
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +658 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +171 -19
- package/engines/heuristic-engine.js +569 -78
- package/integrations/eslint/plugin/index.js +26 -28
- package/origin-rules/common-en.md +8 -8
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +230 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +8 -0
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/prepare-release.sh +1 -1
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex-based analyzer for: C040 - Centralized Validation Logic
|
|
3
|
+
* Purpose: Use regex patterns to detect scattered validation logic (fallback approach)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class C040RegexBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine = null) {
|
|
8
|
+
this.ruleId = 'C040';
|
|
9
|
+
this.ruleName = 'Centralized Validation Logic';
|
|
10
|
+
this.description = 'Don\'t scatter validation logic across multiple classes - Move validation to dedicated validators';
|
|
11
|
+
this.semanticEngine = semanticEngine;
|
|
12
|
+
this.verbose = false;
|
|
13
|
+
|
|
14
|
+
// Patterns for detecting validation logic
|
|
15
|
+
this.validationPatterns = [
|
|
16
|
+
// Function names (more specific - must be standalone words)
|
|
17
|
+
/\b(?:validate|check|ensure|verify|sanitize|normalize)\w*/gi,
|
|
18
|
+
/\b(?:is[A-Z][a-zA-Z]*|has[A-Z][a-zA-Z]*)\s*(?:\(|\s*:)/gi,
|
|
19
|
+
|
|
20
|
+
// Validation frameworks
|
|
21
|
+
/(?:zod|joi|yup|ajv)\.[\w.]+/gi,
|
|
22
|
+
/(?:class-validator|validateSync|checkSchema)/gi,
|
|
23
|
+
|
|
24
|
+
// Common validation patterns
|
|
25
|
+
/\.test\s*\(\s*[^)]*(?:email|phone|url|uuid|password)/gi,
|
|
26
|
+
/(?:email|phone|url|uuid).*\.match\s*\(/gi,
|
|
27
|
+
/typeof\s+\w+\s*[!=]==?\s*['"](?:string|number|boolean)/gi,
|
|
28
|
+
/\.length\s*[<>]=?\s*\d+/gi,
|
|
29
|
+
|
|
30
|
+
// Error throwing patterns for validation
|
|
31
|
+
/throw\s+new\s+(?:ValidationError|BadRequest|InvalidInput|TypeError)/gi,
|
|
32
|
+
/if\s*\([^)]*(?:invalid|empty|null|undefined)\)\s*(?:throw|return.*error)/gi,
|
|
33
|
+
|
|
34
|
+
// Schema validation patterns
|
|
35
|
+
/\.(?:required|optional|string|number|boolean|array|object)\(\)/gi,
|
|
36
|
+
/\.(?:min|max|email|url|uuid|regex)\(\)/gi
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Layer detection patterns
|
|
40
|
+
this.layerPatterns = {
|
|
41
|
+
controller: /\/controllers?\/|controller\.|Controller\./i,
|
|
42
|
+
service: /\/services?\/|service\.|Service\./i,
|
|
43
|
+
repository: /\/repositories?\/|repository\.|Repository\./i,
|
|
44
|
+
validator: /\/validators?\/|validator\.|Validator\.|\/validation\//i,
|
|
45
|
+
middleware: /\/middleware\//i
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async initialize(semanticEngine = null) {
|
|
50
|
+
if (semanticEngine) {
|
|
51
|
+
this.semanticEngine = semanticEngine;
|
|
52
|
+
}
|
|
53
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
54
|
+
|
|
55
|
+
if (this.verbose) {
|
|
56
|
+
console.log(`[DEBUG] 🔄 C040 Regex-Based: Analyzer initialized`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async analyze(files, language, options = {}) {
|
|
61
|
+
const violations = [];
|
|
62
|
+
|
|
63
|
+
for (const filePath of files) {
|
|
64
|
+
try {
|
|
65
|
+
const fileViolations = await this.analyzeFileBasic(filePath, options);
|
|
66
|
+
violations.push(...fileViolations);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (this.verbose) {
|
|
69
|
+
console.warn(`[C040 Regex] Analysis failed for ${filePath}:`, error.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return violations;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
78
|
+
try {
|
|
79
|
+
const fs = require('fs');
|
|
80
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
81
|
+
const violations = [];
|
|
82
|
+
|
|
83
|
+
const layer = this.detectLayer(filePath);
|
|
84
|
+
const validationMatches = this.findValidationPatterns(content, filePath);
|
|
85
|
+
|
|
86
|
+
if (validationMatches.length > 0) {
|
|
87
|
+
// Check if validation logic is in wrong layer
|
|
88
|
+
if (layer === 'controller' || layer === 'service') {
|
|
89
|
+
const complexValidations = validationMatches.filter(match =>
|
|
90
|
+
this.isComplexValidation(match.pattern)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (complexValidations.length > 0) {
|
|
94
|
+
violations.push({
|
|
95
|
+
ruleId: this.ruleId,
|
|
96
|
+
severity: 'warning',
|
|
97
|
+
message: `Found ${complexValidations.length} validation pattern(s) in ${layer} layer. Consider moving to validators.`,
|
|
98
|
+
file: filePath,
|
|
99
|
+
line: complexValidations[0].line,
|
|
100
|
+
column: complexValidations[0].column,
|
|
101
|
+
details: {
|
|
102
|
+
layer,
|
|
103
|
+
validationCount: complexValidations.length,
|
|
104
|
+
patterns: complexValidations.map(v => v.pattern),
|
|
105
|
+
suggestion: 'Move validation logic to dedicated validator classes',
|
|
106
|
+
ruleName: this.ruleName
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for potential duplicates (simple heuristic)
|
|
113
|
+
const duplicatePatterns = this.findPotentialDuplicates(validationMatches);
|
|
114
|
+
if (duplicatePatterns.length > 0) {
|
|
115
|
+
violations.push({
|
|
116
|
+
ruleId: this.ruleId,
|
|
117
|
+
severity: 'info',
|
|
118
|
+
message: `Found potentially duplicate validation patterns: ${duplicatePatterns.join(', ')}`,
|
|
119
|
+
file: filePath,
|
|
120
|
+
line: validationMatches[0].line,
|
|
121
|
+
column: validationMatches[0].column,
|
|
122
|
+
details: {
|
|
123
|
+
duplicatePatterns,
|
|
124
|
+
suggestion: 'Consider consolidating similar validation logic',
|
|
125
|
+
ruleName: this.ruleName
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (this.verbose) {
|
|
135
|
+
console.warn(`[C040 Regex] Failed to analyze ${filePath}:`, error.message);
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
detectLayer(filePath) {
|
|
142
|
+
const path = filePath.toLowerCase();
|
|
143
|
+
|
|
144
|
+
for (const [layer, pattern] of Object.entries(this.layerPatterns)) {
|
|
145
|
+
if (pattern.test(path)) {
|
|
146
|
+
return layer;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return 'unknown';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
findValidationPatterns(content, filePath) {
|
|
154
|
+
const matches = [];
|
|
155
|
+
const lines = content.split('\n');
|
|
156
|
+
|
|
157
|
+
this.validationPatterns.forEach(pattern => {
|
|
158
|
+
lines.forEach((line, lineIndex) => {
|
|
159
|
+
const match = line.match(pattern);
|
|
160
|
+
if (match) {
|
|
161
|
+
matches.push({
|
|
162
|
+
pattern: match[0],
|
|
163
|
+
line: lineIndex + 1,
|
|
164
|
+
column: line.indexOf(match[0]) + 1,
|
|
165
|
+
type: 'regex',
|
|
166
|
+
fullLine: line.trim()
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return matches;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isComplexValidation(pattern) {
|
|
176
|
+
// Filter out false positives and keep only real validation patterns
|
|
177
|
+
const excludePatterns = [
|
|
178
|
+
/Promise/i, // Promise types
|
|
179
|
+
/Response/i, // HTTP Response
|
|
180
|
+
/Request/i, // HTTP Request
|
|
181
|
+
/Service/i, // Service classes
|
|
182
|
+
/Controller/i, // Controller classes
|
|
183
|
+
/Repository/i, // Repository classes
|
|
184
|
+
/Interface/i, // TypeScript interfaces
|
|
185
|
+
/Type/i, // Type definitions
|
|
186
|
+
/Event/i, // Event objects
|
|
187
|
+
/Error/i, // Error objects (unless ValidationError)
|
|
188
|
+
/Component/i, // React/Vue components
|
|
189
|
+
/Module/i, // Module definitions
|
|
190
|
+
/Config/i, // Configuration objects
|
|
191
|
+
/Context/i, // Context objects
|
|
192
|
+
/Handler/i, // Event handlers
|
|
193
|
+
/Listener/i, // Event listeners
|
|
194
|
+
/Provider/i, // Providers
|
|
195
|
+
/Factory/i, // Factory patterns
|
|
196
|
+
/Builder/i, // Builder patterns
|
|
197
|
+
/Manager/i, // Manager classes
|
|
198
|
+
/Util/i, // Utility functions
|
|
199
|
+
/Helper/i // Helper functions
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
// Exclude common false positives
|
|
203
|
+
if (excludePatterns.some(excludePattern => excludePattern.test(pattern))) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Include validation-specific patterns
|
|
208
|
+
const validationIndicators = [
|
|
209
|
+
/validate/i,
|
|
210
|
+
/check.*valid/i,
|
|
211
|
+
/ensure.*valid/i,
|
|
212
|
+
/verify/i,
|
|
213
|
+
/sanitize/i,
|
|
214
|
+
/normalize/i,
|
|
215
|
+
/ValidationError/i,
|
|
216
|
+
/BadRequest/i,
|
|
217
|
+
/InvalidInput/i,
|
|
218
|
+
/zod\./i,
|
|
219
|
+
/joi\./i,
|
|
220
|
+
/yup\./i,
|
|
221
|
+
/\.required\(/i,
|
|
222
|
+
/\.string\(/i,
|
|
223
|
+
/\.email\(/i,
|
|
224
|
+
/\.min\(/i,
|
|
225
|
+
/\.max\(/i
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
return validationIndicators.some(indicator => indicator.test(pattern));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
findPotentialDuplicates(matches) {
|
|
232
|
+
const patternCounts = {};
|
|
233
|
+
|
|
234
|
+
matches.forEach(match => {
|
|
235
|
+
const normalized = match.pattern.toLowerCase();
|
|
236
|
+
patternCounts[normalized] = (patternCounts[normalized] || 0) + 1;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return Object.keys(patternCounts).filter(pattern => patternCounts[pattern] > 1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = C040RegexBasedAnalyzer;
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol-based analyzer for C040 - Centralized Validation Logic Analysis
|
|
3
|
+
* Purpose: Use AST + Data Flow to detect scattered validation logic across layers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require('ts-morph');
|
|
7
|
+
|
|
8
|
+
class C040SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = 'C040';
|
|
11
|
+
this.ruleName = 'Centralized Validation Logic (Symbol-Based)';
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
|
|
15
|
+
// Validation patterns to detect
|
|
16
|
+
this.validationPatterns = {
|
|
17
|
+
functionNames: [
|
|
18
|
+
'validate', 'check', 'ensure', 'verify', 'isValid', 'hasValid',
|
|
19
|
+
'validateInput', 'checkInput', 'validateData', 'sanitize'
|
|
20
|
+
],
|
|
21
|
+
frameworks: ['zod', 'joi', 'yup', 'class-validator', 'ajv'],
|
|
22
|
+
errorTypes: ['ValidationError', 'BadRequest', 'InvalidInput', 'TypeError']
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Layer detection patterns
|
|
26
|
+
this.layerPatterns = {
|
|
27
|
+
controller: /controller|route|handler/i,
|
|
28
|
+
service: /service|business|logic/i,
|
|
29
|
+
repository: /repository|dao|data/i,
|
|
30
|
+
validator: /validator|validation|schema/i
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async initialize(semanticEngine = null) {
|
|
35
|
+
if (semanticEngine) {
|
|
36
|
+
this.semanticEngine = semanticEngine;
|
|
37
|
+
}
|
|
38
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
39
|
+
|
|
40
|
+
if (this.verbose) {
|
|
41
|
+
console.log(`[DEBUG] 🔧 C040 Symbol-Based: Analyzer initialized`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async analyze(files, language, options = {}) {
|
|
46
|
+
const violations = [];
|
|
47
|
+
|
|
48
|
+
if (!this.semanticEngine?.project) {
|
|
49
|
+
if (this.verbose) {
|
|
50
|
+
console.warn('[C040 Symbol-Based] No semantic engine available, skipping analysis');
|
|
51
|
+
}
|
|
52
|
+
return violations;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const filePath of files) {
|
|
56
|
+
try {
|
|
57
|
+
const fileViolations = await this.analyzeFile(filePath);
|
|
58
|
+
violations.push(...fileViolations);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (this.verbose) {
|
|
61
|
+
console.warn(`[C040] Analysis failed for ${filePath}:`, error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async analyzeFile(filePath) {
|
|
70
|
+
const violations = [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
74
|
+
if (!sourceFile) {
|
|
75
|
+
if (this.verbose) {
|
|
76
|
+
console.log(`[C040] Source file not found in project: ${filePath}`);
|
|
77
|
+
}
|
|
78
|
+
return violations;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.verbose) {
|
|
82
|
+
console.log(`[C040] Analyzing file: ${filePath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Detect layer from file path
|
|
86
|
+
const layer = this.detectLayer(filePath);
|
|
87
|
+
|
|
88
|
+
// Find validation patterns
|
|
89
|
+
const validationScents = this.findValidationPatterns(sourceFile);
|
|
90
|
+
|
|
91
|
+
if (validationScents.length > 0) {
|
|
92
|
+
// Check if validation is in wrong layer
|
|
93
|
+
if (layer === 'controller' || layer === 'service') {
|
|
94
|
+
const complexValidations = validationScents.filter(v => v.isComplex);
|
|
95
|
+
const businessValidations = complexValidations.filter(v =>
|
|
96
|
+
v.context === 'business-validation'
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (businessValidations.length > 0) {
|
|
100
|
+
violations.push({
|
|
101
|
+
ruleId: this.ruleId,
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
message: `Found ${businessValidations.length} complex business validation pattern(s) in ${layer} layer. Consider moving to validators.`,
|
|
104
|
+
file: filePath,
|
|
105
|
+
line: businessValidations[0].line,
|
|
106
|
+
column: businessValidations[0].column,
|
|
107
|
+
details: {
|
|
108
|
+
layer,
|
|
109
|
+
validationCount: businessValidations.length,
|
|
110
|
+
patterns: businessValidations.map(v => v.pattern),
|
|
111
|
+
suggestion: 'Move validation logic to dedicated validator classes',
|
|
112
|
+
ruleName: this.ruleName
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (this.verbose) {
|
|
121
|
+
console.warn(`[C040] Error analyzing ${filePath}:`, error.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return violations;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
detectLayer(filePath) {
|
|
129
|
+
const path = filePath.toLowerCase();
|
|
130
|
+
|
|
131
|
+
for (const [layer, pattern] of Object.entries(this.layerPatterns)) {
|
|
132
|
+
if (pattern.test(path)) {
|
|
133
|
+
return layer;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return 'unknown';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
findValidationPatterns(sourceFile) {
|
|
141
|
+
const patterns = [];
|
|
142
|
+
|
|
143
|
+
// 1. Find business validation functions (semantic analysis)
|
|
144
|
+
sourceFile.getFunctions().forEach(func => {
|
|
145
|
+
const validationInfo = this.analyzeValidationFunction(func);
|
|
146
|
+
if (validationInfo) {
|
|
147
|
+
patterns.push(validationInfo);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 2. Find class methods with validation logic
|
|
152
|
+
sourceFile.getClasses().forEach(cls => {
|
|
153
|
+
cls.getMethods().forEach(method => {
|
|
154
|
+
const validationInfo = this.analyzeValidationMethod(method, cls);
|
|
155
|
+
if (validationInfo) {
|
|
156
|
+
patterns.push(validationInfo);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 3. Find validation decorators and frameworks usage
|
|
162
|
+
sourceFile.getClasses().forEach(cls => {
|
|
163
|
+
cls.getMethods().forEach(method => {
|
|
164
|
+
const decoratorInfo = this.analyzeValidationDecorators(method);
|
|
165
|
+
if (decoratorInfo) {
|
|
166
|
+
patterns.push(decoratorInfo);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return patterns;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Analyze if a function contains actual business validation logic
|
|
176
|
+
*/
|
|
177
|
+
analyzeValidationFunction(func) {
|
|
178
|
+
const name = func.getName();
|
|
179
|
+
const body = func.getBodyText();
|
|
180
|
+
|
|
181
|
+
// Skip if it's infrastructure/utility function
|
|
182
|
+
if (this.isInfrastructureFunction(func, name, body)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for actual validation patterns in body
|
|
187
|
+
const validationSignals = this.countValidationSignals(body);
|
|
188
|
+
|
|
189
|
+
if (validationSignals.score > 2) { // Threshold for real validation
|
|
190
|
+
return {
|
|
191
|
+
type: 'function',
|
|
192
|
+
pattern: name,
|
|
193
|
+
line: func.getStartLineNumber(),
|
|
194
|
+
column: func.getStart(),
|
|
195
|
+
isComplex: validationSignals.score > 5,
|
|
196
|
+
signals: validationSignals.patterns,
|
|
197
|
+
context: 'business-validation'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Analyze if a method contains business validation logic
|
|
206
|
+
*/
|
|
207
|
+
analyzeValidationMethod(method, parentClass) {
|
|
208
|
+
const methodName = method.getName();
|
|
209
|
+
const className = parentClass.getName();
|
|
210
|
+
const body = method.getBodyText();
|
|
211
|
+
|
|
212
|
+
// Skip infrastructure methods
|
|
213
|
+
if (this.isInfrastructureMethod(method, methodName, body, className)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for validation patterns
|
|
218
|
+
const validationSignals = this.countValidationSignals(body);
|
|
219
|
+
|
|
220
|
+
// Balanced threshold to catch meaningful validation
|
|
221
|
+
if (validationSignals.score > 3) {
|
|
222
|
+
return {
|
|
223
|
+
type: 'method',
|
|
224
|
+
pattern: `${className}.${methodName}`,
|
|
225
|
+
line: method.getStartLineNumber(),
|
|
226
|
+
column: method.getStart(),
|
|
227
|
+
isComplex: validationSignals.score > 6,
|
|
228
|
+
signals: validationSignals.patterns,
|
|
229
|
+
context: 'business-validation'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check for validation framework decorators
|
|
238
|
+
*/
|
|
239
|
+
analyzeValidationDecorators(method) {
|
|
240
|
+
const decorators = method.getDecorators();
|
|
241
|
+
const validationDecorators = decorators.filter(dec => {
|
|
242
|
+
const name = dec.getName();
|
|
243
|
+
return ['IsEmail', 'IsString', 'IsNumber', 'Min', 'Max', 'Length', 'Matches'].includes(name);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (validationDecorators.length > 0) {
|
|
247
|
+
return {
|
|
248
|
+
type: 'decorator',
|
|
249
|
+
pattern: validationDecorators.map(d => d.getName()).join(', '),
|
|
250
|
+
line: method.getStartLineNumber(),
|
|
251
|
+
column: method.getStart(),
|
|
252
|
+
isComplex: true,
|
|
253
|
+
context: 'framework-validation'
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if function/method is infrastructure/utility rather than business validation
|
|
262
|
+
*/
|
|
263
|
+
isInfrastructureFunction(func, name, body) {
|
|
264
|
+
// Health check patterns
|
|
265
|
+
if (name === 'check' && body.includes('health')) return true;
|
|
266
|
+
if (name.includes('health') || name.includes('Health')) return true;
|
|
267
|
+
|
|
268
|
+
// Crypto/hash utilities
|
|
269
|
+
if (name === 'check' && (body.includes('bcrypt') || body.includes('hash') || body.includes('crypto'))) return true;
|
|
270
|
+
if (name.includes('hash') && body.includes('bcrypt')) return true;
|
|
271
|
+
|
|
272
|
+
// Authentication utilities (not business validation)
|
|
273
|
+
if (name.includes('verify') && body.includes('jwt')) return true;
|
|
274
|
+
if (name.includes('verify') && body.includes('token')) return true;
|
|
275
|
+
|
|
276
|
+
// Simple getters/setters
|
|
277
|
+
if (name.startsWith('get') || name.startsWith('set')) return true;
|
|
278
|
+
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if method is infrastructure/utility
|
|
284
|
+
*/
|
|
285
|
+
isInfrastructureMethod(method, methodName, body, className) {
|
|
286
|
+
// Infrastructure keywords in method/class names
|
|
287
|
+
const infraKeywords = [
|
|
288
|
+
'health', 'hash', 'auth', 'login', 'register', 'encrypt', 'decrypt',
|
|
289
|
+
'token', 'session', 'cookie', 'cache', 'log', 'monitor', 'metric',
|
|
290
|
+
's3', 'aws', 'storage', 'upload', 'download', 'file', 'config',
|
|
291
|
+
'connection', 'database', 'db', 'migration', 'seed', 'error', 'exception'
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
const lowerMethodName = methodName.toLowerCase();
|
|
295
|
+
const lowerClassName = className.toLowerCase();
|
|
296
|
+
|
|
297
|
+
// Check method/class names
|
|
298
|
+
if (infraKeywords.some(keyword =>
|
|
299
|
+
lowerMethodName.includes(keyword) || lowerClassName.includes(keyword)
|
|
300
|
+
)) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Controller methods that just delegate (not business validation)
|
|
305
|
+
if (className.toLowerCase().includes('controller')) {
|
|
306
|
+
// Simple delegation patterns
|
|
307
|
+
const delegationPatterns = [
|
|
308
|
+
/return\s+await\s+this\.\w+\.\w+\(/,
|
|
309
|
+
/return\s+this\.\w+\.\w+\(/,
|
|
310
|
+
/^\s*return\s+await/m
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
if (delegationPatterns.some(pattern => pattern.test(body))) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check method body for infrastructure patterns
|
|
319
|
+
const infraPatterns = [
|
|
320
|
+
/bcrypt|crypto|jwt/i,
|
|
321
|
+
/\.hash\(|\.compare\(/i,
|
|
322
|
+
/redis|cache/i,
|
|
323
|
+
/aws|s3|bucket/i,
|
|
324
|
+
/winston|logger/i,
|
|
325
|
+
/fileExist|checkExist/i,
|
|
326
|
+
/uploadFile|downloadFile/i,
|
|
327
|
+
/HttpStatus\./i // Pure HTTP status handling
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
if (infraPatterns.some(pattern => pattern.test(body))) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Call parent function check
|
|
335
|
+
return this.isInfrastructureFunction(method, methodName, body);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Count actual validation signals in code body using semantic analysis
|
|
340
|
+
*/
|
|
341
|
+
countValidationSignals(body) {
|
|
342
|
+
let score = 0;
|
|
343
|
+
const patterns = [];
|
|
344
|
+
|
|
345
|
+
// Business validation patterns (weighted scoring)
|
|
346
|
+
const validationIndicators = [
|
|
347
|
+
// High weight - clear business validation
|
|
348
|
+
{ pattern: /throw new.*ValidationError/gi, weight: 5, name: 'ValidationError' },
|
|
349
|
+
{ pattern: /throw new.*BadRequest/gi, weight: 4, name: 'BadRequestError' },
|
|
350
|
+
{ pattern: /throw new.*InvalidInput/gi, weight: 4, name: 'InvalidInput' },
|
|
351
|
+
|
|
352
|
+
// Medium weight - input validation
|
|
353
|
+
{ pattern: /if\s*\(\s*!.*\)\s*{[^}]*throw/gi, weight: 3, name: 'conditional-validation' },
|
|
354
|
+
{ pattern: /\.length\s*[<>]=?\s*\d+.*throw/gi, weight: 3, name: 'length-validation' },
|
|
355
|
+
{ pattern: /typeof.*!==.*throw/gi, weight: 3, name: 'type-validation' },
|
|
356
|
+
|
|
357
|
+
// Framework validation
|
|
358
|
+
{ pattern: /zod\.|joi\.|yup\./gi, weight: 4, name: 'validation-framework' },
|
|
359
|
+
{ pattern: /class-validator/gi, weight: 4, name: 'class-validator' },
|
|
360
|
+
|
|
361
|
+
// Business rule validation
|
|
362
|
+
{ pattern: /validate[A-Z][a-zA-Z]*\(/gi, weight: 3, name: 'validate-method' },
|
|
363
|
+
{ pattern: /check[A-Z][a-zA-Z]*\(/gi, weight: 2, name: 'check-method' },
|
|
364
|
+
{ pattern: /ensure[A-Z][a-zA-Z]*\(/gi, weight: 3, name: 'ensure-method' },
|
|
365
|
+
|
|
366
|
+
// Low weight - might be utility
|
|
367
|
+
{ pattern: /\.test\(/gi, weight: 1, name: 'regex-test' },
|
|
368
|
+
{ pattern: /\.match\(/gi, weight: 1, name: 'string-match' }
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
validationIndicators.forEach(indicator => {
|
|
372
|
+
const matches = body.match(indicator.pattern);
|
|
373
|
+
if (matches) {
|
|
374
|
+
score += matches.length * indicator.weight;
|
|
375
|
+
patterns.push(`${indicator.name} (${matches.length})`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Penalty for infrastructure patterns (increased penalties)
|
|
380
|
+
const infrastructurePatterns = [
|
|
381
|
+
{ pattern: /bcrypt|crypto|jwt/gi, penalty: -4 },
|
|
382
|
+
{ pattern: /health|Health/gi, penalty: -6 },
|
|
383
|
+
{ pattern: /\.hash\(|\.compare\(/gi, penalty: -3 },
|
|
384
|
+
{ pattern: /HttpStatus\.|\.status\(/gi, penalty: -2 },
|
|
385
|
+
{ pattern: /aws|s3|bucket/gi, penalty: -4 },
|
|
386
|
+
{ pattern: /fileExist|checkExist/gi, penalty: -3 },
|
|
387
|
+
{ pattern: /return\s+await\s+this\./gi, penalty: -2 }, // Delegation pattern
|
|
388
|
+
{ pattern: /BadRequest.*S3/gi, penalty: -5 }, // S3 error handling
|
|
389
|
+
{ pattern: /error.*message/gi, penalty: -1 } // Generic error handling
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
infrastructurePatterns.forEach(infra => {
|
|
393
|
+
const matches = body.match(infra.pattern);
|
|
394
|
+
if (matches) {
|
|
395
|
+
score += matches.length * infra.penalty;
|
|
396
|
+
patterns.push(`infrastructure-penalty (${matches.length})`);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return { score: Math.max(0, score), patterns };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
isValidationFunction(name) {
|
|
404
|
+
return this.validationPatterns.functionNames.some(pattern =>
|
|
405
|
+
name.toLowerCase().includes(pattern.toLowerCase())
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
isValidationError(text) {
|
|
410
|
+
return this.validationPatterns.errorTypes.some(errorType =>
|
|
411
|
+
text.includes(errorType)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = C040SymbolBasedAnalyzer;
|