@sun-asterisk/sunlint 1.3.0 → 1.3.2
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 +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- 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 +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- 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 +232 -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/{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 +6 -1
- 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/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -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/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- 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/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C024 Symbol-based Analyzer - Advanced Do not scatter hardcoded constants throughout the logic
|
|
3
|
+
* Purpose: The rule prevents scattering hardcoded constants throughout the logic. Instead, constants should be defined in a single place to improve maintainability and readability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require('ts-morph');
|
|
7
|
+
|
|
8
|
+
class C024SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = 'C024';
|
|
11
|
+
this.ruleName = 'Error Scatter hardcoded constants throughout the logic (Symbol-Based)';
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
this.safeStrings = ["UNKNOWN", "N/A"]; // allowlist of special fallback values
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize(semanticEngine = null) {
|
|
18
|
+
if (semanticEngine) {
|
|
19
|
+
this.semanticEngine = semanticEngine;
|
|
20
|
+
}
|
|
21
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
22
|
+
|
|
23
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
24
|
+
console.log(`🔧 [C024 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
29
|
+
// This is the main entry point called by the hybrid analyzer
|
|
30
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
34
|
+
const violations = [];
|
|
35
|
+
|
|
36
|
+
// Enable verbose mode if requested
|
|
37
|
+
const verbose = options.verbose || this.verbose;
|
|
38
|
+
|
|
39
|
+
if (!this.semanticEngine?.project) {
|
|
40
|
+
if (verbose) {
|
|
41
|
+
console.warn('[C024 Symbol-Based] No semantic engine available, skipping analysis');
|
|
42
|
+
}
|
|
43
|
+
return violations;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (verbose) {
|
|
47
|
+
console.log(`🔍 [C024 Symbol-Based] Starting analysis for ${filePath}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
52
|
+
if (!sourceFile) {
|
|
53
|
+
return violations;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// skip constants files
|
|
57
|
+
if (this.isConstantsFile(filePath)) return violations;
|
|
58
|
+
// Detect hardcoded constants
|
|
59
|
+
sourceFile.forEachDescendant((node) => {
|
|
60
|
+
this.checkLiterals(node, sourceFile, violations);
|
|
61
|
+
this.checkConstDeclaration(node, sourceFile, violations);
|
|
62
|
+
this.checkStaticReadonly(node, sourceFile, violations);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if (verbose) {
|
|
67
|
+
console.log(`🔍 [C024 Symbol-Based] Total violations found: ${violations.length}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return violations;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (verbose) {
|
|
73
|
+
console.warn(`[C024 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return violations;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- push violation object ---
|
|
81
|
+
pushViolation(violations, node, filePath, text, message) {
|
|
82
|
+
violations.push({
|
|
83
|
+
ruleId: this.ruleId,
|
|
84
|
+
severity: "warning",
|
|
85
|
+
message: message || `Hardcoded constant found: "${text}"`,
|
|
86
|
+
source: this.ruleId,
|
|
87
|
+
file: filePath,
|
|
88
|
+
line: node.getStartLineNumber(),
|
|
89
|
+
column: node.getStart() - node.getStartLinePos(),
|
|
90
|
+
description:
|
|
91
|
+
"[SYMBOL-BASED] Hardcoded constants should be defined in a single place to improve maintainability.",
|
|
92
|
+
suggestion: "Define constants in a dedicated file or section",
|
|
93
|
+
category: "constants",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- check literals like "ADMIN", 123, true ---
|
|
98
|
+
checkLiterals(node, sourceFile, violations) {
|
|
99
|
+
const kind = node.getKind();
|
|
100
|
+
if (
|
|
101
|
+
kind === SyntaxKind.StringLiteral ||
|
|
102
|
+
kind === SyntaxKind.NumericLiteral ||
|
|
103
|
+
kind === SyntaxKind.TrueKeyword ||
|
|
104
|
+
kind === SyntaxKind.FalseKeyword
|
|
105
|
+
) {
|
|
106
|
+
const text = node.getText().replace(/['"`]/g, ""); // strip quotes
|
|
107
|
+
if (this.isAllowedLiteral(node, text)) return;
|
|
108
|
+
|
|
109
|
+
this.pushViolation(
|
|
110
|
+
violations,
|
|
111
|
+
node,
|
|
112
|
+
sourceFile.getFilePath(),
|
|
113
|
+
node.getText()
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- check const declarations outside constants.ts ---
|
|
119
|
+
checkConstDeclaration(node, sourceFile, violations) {
|
|
120
|
+
const kind = node.getKind();
|
|
121
|
+
if (kind === SyntaxKind.VariableDeclaration) {
|
|
122
|
+
const parentKind = node.getParent()?.getKind();
|
|
123
|
+
if (
|
|
124
|
+
parentKind === SyntaxKind.VariableDeclarationList &&
|
|
125
|
+
node.getParent().getDeclarationKind() === "const"
|
|
126
|
+
) {
|
|
127
|
+
this.pushViolation(
|
|
128
|
+
violations,
|
|
129
|
+
node,
|
|
130
|
+
sourceFile.getFilePath(),
|
|
131
|
+
node.getName(),
|
|
132
|
+
`Const declaration "${node.getName()}" should be moved into constants file`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- check static readonly properties inside classes ---
|
|
139
|
+
checkStaticReadonly(node, sourceFile, violations) {
|
|
140
|
+
const kind = node.getKind();
|
|
141
|
+
if (kind === SyntaxKind.PropertyDeclaration) {
|
|
142
|
+
const modifiers = node.getModifiers().map((m) => m.getText());
|
|
143
|
+
if (modifiers.includes("static") && modifiers.includes("readonly")) {
|
|
144
|
+
this.pushViolation(
|
|
145
|
+
violations,
|
|
146
|
+
node,
|
|
147
|
+
sourceFile.getFilePath(),
|
|
148
|
+
node.getName(),
|
|
149
|
+
`Static readonly property "${node.getName()}" should be moved into constants file`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- helper: allow safe literals ---
|
|
156
|
+
isAllowedLiteral(node, text) {
|
|
157
|
+
// skip imports
|
|
158
|
+
if (node.getParent()?.getKind() === SyntaxKind.ImportDeclaration) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// allow short strings
|
|
163
|
+
if (typeof text === "string" && text.length <= 1) return true;
|
|
164
|
+
|
|
165
|
+
// allow sentinel numbers
|
|
166
|
+
if (text === "0" || text === "1" || text === "-1") return true;
|
|
167
|
+
|
|
168
|
+
// allow known safe strings (like "UNKNOWN")
|
|
169
|
+
if (this.safeStrings.includes(text)) return true;
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// helper to check if file is a constants file
|
|
175
|
+
isConstantsFile(filePath) {
|
|
176
|
+
const lower = filePath.toLowerCase();
|
|
177
|
+
return lower.endsWith("constants.ts") || lower.includes("/constants/");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = C024SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rule C030 - Use Custom Error Classes
|
|
6
|
+
* Enforce using application-specific error classes instead of generic system errors
|
|
7
|
+
* Examples to flag: throw new Error(), throw new TypeError(), Promise.reject(new Error(...))
|
|
8
|
+
*/
|
|
9
|
+
class C030UseCustomErrorClassesAnalyzer {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.ruleId = 'C030';
|
|
12
|
+
this.ruleName = 'Use Custom Error Classes';
|
|
13
|
+
this.description = 'Use custom error classes instead of generic system errors';
|
|
14
|
+
this.severity = 'warning';
|
|
15
|
+
this.verbose = options.verbose || false;
|
|
16
|
+
|
|
17
|
+
this.builtinErrorNames = [
|
|
18
|
+
'Error',
|
|
19
|
+
'TypeError',
|
|
20
|
+
'RangeError',
|
|
21
|
+
'ReferenceError',
|
|
22
|
+
'SyntaxError',
|
|
23
|
+
'URIError',
|
|
24
|
+
'EvalError'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Precompile regexes for speed
|
|
28
|
+
const namesGroup = this.builtinErrorNames.join('|');
|
|
29
|
+
this.patterns = [
|
|
30
|
+
// throw new Error(...)
|
|
31
|
+
new RegExp(`\\bthrow\\s+new\\s+(${namesGroup})\\s*\\(`),
|
|
32
|
+
// throw Error(...)
|
|
33
|
+
new RegExp(`\\bthrow\\s+(${namesGroup})\\s*\\(`),
|
|
34
|
+
// Promise.reject(new Error(...))
|
|
35
|
+
new RegExp(`Promise\\.reject\\s*\\(\\s*new\\s+(${namesGroup})\\s*\\(`),
|
|
36
|
+
// reject(new Error(...))
|
|
37
|
+
new RegExp(`\\breject\\s*\\(\\s*new\\s+(${namesGroup})\\s*\\(`),
|
|
38
|
+
// Throwing string literals (single, double quotes)
|
|
39
|
+
/\bthrow\s+['"][^'"]*['"]/,
|
|
40
|
+
// Throwing template literals
|
|
41
|
+
/\bthrow\s+`[^`]*`/,
|
|
42
|
+
// Throwing numbers
|
|
43
|
+
/\bthrow\s+\d+/,
|
|
44
|
+
// Throwing variables (simple identifiers) - remove $ anchor to allow comments
|
|
45
|
+
/\bthrow\s+[a-zA-Z_$][a-zA-Z0-9_$]*(?:\s*;|\s*\/\/|\s*$)/
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async analyze(files, language, config = {}) {
|
|
50
|
+
const violations = [];
|
|
51
|
+
|
|
52
|
+
for (const filePath of files) {
|
|
53
|
+
try {
|
|
54
|
+
// Handle both file paths and direct content
|
|
55
|
+
let content;
|
|
56
|
+
if (typeof filePath === 'string' && fs.existsSync(filePath)) {
|
|
57
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
58
|
+
} else if (typeof filePath === 'object' && filePath.content) {
|
|
59
|
+
// Handle test cases with direct content
|
|
60
|
+
content = filePath.content;
|
|
61
|
+
filePath = filePath.path || 'test.js';
|
|
62
|
+
} else {
|
|
63
|
+
if (this.verbose) {
|
|
64
|
+
console.warn(`C030: Skipping invalid file path: ${filePath}`);
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, config);
|
|
70
|
+
violations.push(...fileViolations);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (this.verbose) {
|
|
73
|
+
console.warn(`C030 analysis error for ${path.basename(filePath)}: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return violations;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async analyzeFile(filePath, content, language, config = {}) {
|
|
82
|
+
const violations = [];
|
|
83
|
+
|
|
84
|
+
// Only target JS/TS for now
|
|
85
|
+
if (!this.isJsLike(filePath)) {
|
|
86
|
+
return violations;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const lines = content.split('\n');
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const line = lines[i];
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
|
|
95
|
+
// Skip comments-only lines quickly
|
|
96
|
+
if (this.isCommentOnly(trimmed)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const pattern of this.patterns) {
|
|
101
|
+
const match = trimmed.match(pattern);
|
|
102
|
+
if (match) {
|
|
103
|
+
const column = line.indexOf(match[0]) + 1;
|
|
104
|
+
const builtInName = this.extractBuiltinName(match);
|
|
105
|
+
const violationType = this.getViolationType(pattern, trimmed);
|
|
106
|
+
|
|
107
|
+
const suggestion = this.getSuggestion(builtInName, violationType);
|
|
108
|
+
|
|
109
|
+
violations.push({
|
|
110
|
+
ruleId: this.ruleId,
|
|
111
|
+
file: filePath,
|
|
112
|
+
line: i + 1,
|
|
113
|
+
column: Math.max(column, 1),
|
|
114
|
+
message: this.getMessage(violationType),
|
|
115
|
+
severity: this.severity,
|
|
116
|
+
code: trimmed,
|
|
117
|
+
type: violationType,
|
|
118
|
+
suggestion
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Avoid double-reporting same line on multiple patterns
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return violations;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isJsLike(filePath) {
|
|
131
|
+
return /\.(js|jsx|ts|tsx|mjs|cjs)$/.test(filePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isCommentOnly(trimmedLine) {
|
|
135
|
+
return trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') || trimmedLine === '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
extractBuiltinName(regexMatch) {
|
|
139
|
+
if (!regexMatch || regexMatch.length < 2) return null;
|
|
140
|
+
const candidate = regexMatch[1];
|
|
141
|
+
return this.builtinErrorNames.includes(candidate) ? candidate : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getViolationType(pattern, line) {
|
|
145
|
+
if (pattern.source.includes('new\\s+(')) {
|
|
146
|
+
return 'generic_system_error_constructor';
|
|
147
|
+
} else if (pattern.source.includes('\\s+(') && pattern.source.includes('\\(')) {
|
|
148
|
+
return 'generic_system_error_call';
|
|
149
|
+
} else if (pattern.source.includes('Promise\\.reject')) {
|
|
150
|
+
return 'promise_reject_generic_error';
|
|
151
|
+
} else if (pattern.source.includes('reject\\s*\\(')) {
|
|
152
|
+
return 'reject_generic_error';
|
|
153
|
+
} else if (pattern.source.includes('`[^`]*`')) {
|
|
154
|
+
return 'throw_template_literal';
|
|
155
|
+
} else if (pattern.source.includes("['\"")) {
|
|
156
|
+
return 'throw_string_literal';
|
|
157
|
+
} else if (pattern.source.includes('\\d+')) {
|
|
158
|
+
return 'throw_number';
|
|
159
|
+
} else if (pattern.source.includes('[a-zA-Z_$]')) {
|
|
160
|
+
return 'throw_variable';
|
|
161
|
+
}
|
|
162
|
+
return 'generic_system_error_usage';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getMessage(violationType) {
|
|
166
|
+
const messages = {
|
|
167
|
+
'generic_system_error_constructor': 'Use custom error classes instead of generic system error constructors',
|
|
168
|
+
'generic_system_error_call': 'Use custom error classes instead of generic system error calls',
|
|
169
|
+
'promise_reject_generic_error': 'Use custom error classes instead of rejecting with generic errors',
|
|
170
|
+
'reject_generic_error': 'Use custom error classes instead of rejecting with generic errors',
|
|
171
|
+
'throw_string_literal': 'Use custom error classes instead of throwing string literals',
|
|
172
|
+
'throw_template_literal': 'Use custom error classes instead of throwing template literals',
|
|
173
|
+
'throw_number': 'Use custom error classes instead of throwing numbers',
|
|
174
|
+
'throw_variable': 'Use custom error classes instead of throwing variables',
|
|
175
|
+
'generic_system_error_usage': 'Use custom error classes instead of generic system errors'
|
|
176
|
+
};
|
|
177
|
+
return messages[violationType] || messages['generic_system_error_usage'];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getSuggestion(builtInName, violationType) {
|
|
181
|
+
if (builtInName) {
|
|
182
|
+
return `Define and throw a custom error class (e.g., DomainError extends Error) instead of ${builtInName}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const suggestions = {
|
|
186
|
+
'throw_string_literal': 'Define and throw a custom error class (e.g., DomainError extends Error) instead of throwing string literals',
|
|
187
|
+
'throw_template_literal': 'Define and throw a custom error class (e.g., DomainError extends Error) instead of throwing template literals',
|
|
188
|
+
'throw_number': 'Define and throw a custom error class (e.g., DomainError extends Error) instead of throwing numbers',
|
|
189
|
+
'throw_variable': 'Define and throw a custom error class (e.g., DomainError extends Error) instead of throwing variables',
|
|
190
|
+
'promise_reject_generic_error': 'Define and throw a custom error class (e.g., DomainError extends Error) instead of rejecting with generic errors',
|
|
191
|
+
'reject_generic_error': 'Define and throw a custom error class (e.g., DomainError extends Error) instead of rejecting with generic errors'
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return suggestions[violationType] || 'Define and throw a custom error class (e.g., DomainError extends Error)';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = new C030UseCustomErrorClassesAnalyzer();
|
|
199
|
+
|
|
200
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# C033: Separate Service and Repository Logic
|
|
2
|
+
|
|
3
|
+
## Rule Description
|
|
4
|
+
|
|
5
|
+
Enforces proper separation between Service and Repository layers in your application architecture:
|
|
6
|
+
|
|
7
|
+
- **Services** should contain business logic and use repositories for data access
|
|
8
|
+
- **Repositories** should contain only data access logic, no business rules
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
This rule uses a **hybrid analysis approach**:
|
|
13
|
+
|
|
14
|
+
1. **Primary: Symbol-based Analysis** (using ts-morph)
|
|
15
|
+
- AST parsing and symbol resolution
|
|
16
|
+
- Accurate detection of database operations
|
|
17
|
+
- Excludes queue/job operations automatically
|
|
18
|
+
|
|
19
|
+
2. **Fallback: Regex-based Analysis**
|
|
20
|
+
- Pattern matching for when symbol analysis fails
|
|
21
|
+
- Handles edge cases and complex code structures
|
|
22
|
+
|
|
23
|
+
## Files Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
C033_separate_service_repository/
|
|
27
|
+
├── analyzer.js # Main hybrid orchestrator
|
|
28
|
+
├── symbol-based-analyzer.js # Primary AST-based analysis
|
|
29
|
+
├── regex-based-analyzer.js # Fallback pattern matching
|
|
30
|
+
├── config.json # Rule configuration
|
|
31
|
+
└── README.md # This documentation
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What this rule detects
|
|
35
|
+
|
|
36
|
+
### Violations in Service files:
|
|
37
|
+
- Direct database calls (`repository.createQueryBuilder()`, `dataSource.createQueryBuilder()`)
|
|
38
|
+
- Direct ORM operations (`entity.save()`, `entity.find()`)
|
|
39
|
+
- SQL queries embedded in service methods
|
|
40
|
+
- **Excludes**: Queue/job operations (`job.remove()`, `job.isFailed()`, etc.)
|
|
41
|
+
|
|
42
|
+
### Violations in Repository files:
|
|
43
|
+
- Complex business logic (filtering, calculations, validations)
|
|
44
|
+
- Business rules and workflows
|
|
45
|
+
- Complex conditional logic for data processing
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
See test cases in the standard test fixtures location:
|
|
50
|
+
|
|
51
|
+
- **Violations**: `examples/rule-test-fixtures/rules/C033_separate_service_repository/violations/test-cases.js`
|
|
52
|
+
- **Clean code**: `examples/rule-test-fixtures/rules/C033_separate_service_repository/clean/good-examples.js`
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Test violations
|
|
58
|
+
node cli.js --rule=C033 --input=examples/rule-test-fixtures/rules/C033_separate_service_repository/violations --engine=heuristic
|
|
59
|
+
|
|
60
|
+
# Test clean code
|
|
61
|
+
node cli.js --rule=C033 --input=examples/rule-test-fixtures/rules/C033_separate_service_repository/clean --engine=heuristic
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Technical Implementation
|
|
65
|
+
|
|
66
|
+
- **Primary Analysis**: Semantic analysis using ts-morph for AST traversal
|
|
67
|
+
- **Fallback**: Regex pattern matching for environments without ts-morph
|
|
68
|
+
- **Engine**: Heuristic engine (registered in enhanced-rules-registry.js)
|
|
69
|
+
- **File Detection**: Classifies files as Service/Repository based on naming patterns
|
|
70
|
+
|
|
71
|
+
## Philosophy
|
|
72
|
+
|
|
73
|
+
This rule enforces the Repository Pattern and Domain-Driven Design principles:
|
|
74
|
+
|
|
75
|
+
1. **Separation of Concerns**: Business logic in Services, data access in Repositories
|
|
76
|
+
2. **Testability**: Each layer can be tested independently
|
|
77
|
+
3. **Maintainability**: Changes to business rules don't affect data access code
|
|
78
|
+
4. **Flexibility**: Data storage can be changed without affecting business logic
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C033 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 C033SymbolBasedAnalyzer = require('./symbol-based-analyzer');
|
|
8
|
+
const C033RegexBasedAnalyzer = require('./regex-based-analyzer');
|
|
9
|
+
|
|
10
|
+
class C033Analyzer {
|
|
11
|
+
constructor(semanticEngine = null) {
|
|
12
|
+
this.ruleId = 'C033';
|
|
13
|
+
this.ruleName = 'Separate Service and Repository Logic';
|
|
14
|
+
this.description = 'Tách logic xử lý và truy vấn dữ liệu trong service layer - Repository chỉ chứa CRUD, Service chứa business logic';
|
|
15
|
+
this.semanticEngine = semanticEngine;
|
|
16
|
+
this.verbose = false;
|
|
17
|
+
|
|
18
|
+
// Initialize analyzers
|
|
19
|
+
this.symbolBasedAnalyzer = new C033SymbolBasedAnalyzer(semanticEngine);
|
|
20
|
+
this.regexBasedAnalyzer = new C033RegexBasedAnalyzer(semanticEngine);
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
this.config = {
|
|
24
|
+
useSymbolBased: true, // Primary approach
|
|
25
|
+
fallbackToRegex: true, // Only when symbol fails completely
|
|
26
|
+
symbolBasedOnly: false // Can be set to true for pure mode
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize with semantic engine
|
|
32
|
+
*/
|
|
33
|
+
async initialize(semanticEngine = null) {
|
|
34
|
+
if (semanticEngine) {
|
|
35
|
+
this.semanticEngine = semanticEngine;
|
|
36
|
+
}
|
|
37
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
38
|
+
|
|
39
|
+
// Initialize both analyzers
|
|
40
|
+
await this.symbolBasedAnalyzer.initialize(semanticEngine);
|
|
41
|
+
await this.regexBasedAnalyzer.initialize(semanticEngine);
|
|
42
|
+
|
|
43
|
+
if (this.verbose) {
|
|
44
|
+
console.log(`[DEBUG] 🔧 C033: Analyzer initialized - Symbol-based: ✅, Regex fallback: ${this.config.fallbackToRegex ? '✅' : '❌'}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async analyze(files, language, options = {}) {
|
|
49
|
+
const violations = [];
|
|
50
|
+
let symbolCount = 0;
|
|
51
|
+
let regexCount = 0;
|
|
52
|
+
|
|
53
|
+
for (const filePath of files) {
|
|
54
|
+
try {
|
|
55
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
56
|
+
violations.push(...fileViolations);
|
|
57
|
+
|
|
58
|
+
// Count strategy usage
|
|
59
|
+
const strategy = fileViolations[0]?.analysisStrategy;
|
|
60
|
+
if (strategy === 'symbol-based') symbolCount++;
|
|
61
|
+
else if (strategy === 'regex-fallback') regexCount++;
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (this.verbose) {
|
|
65
|
+
console.warn(`[C033] Analysis failed for ${filePath}:`, error.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Summary of strategy usage
|
|
71
|
+
if (this.verbose && (symbolCount > 0 || regexCount > 0)) {
|
|
72
|
+
console.log(`📊 [C033-SUMMARY] Analysis strategy usage:`);
|
|
73
|
+
console.log(` 🧠 Symbol-based: ${symbolCount} files`);
|
|
74
|
+
console.log(` 🔄 Regex-fallback: ${regexCount} files`);
|
|
75
|
+
console.log(` 📈 Coverage: ${symbolCount}/${symbolCount + regexCount} files used primary strategy`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return violations;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async analyzeFile(filePath, options = {}) {
|
|
82
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
83
|
+
if (this.config.useSymbolBased && this.semanticEngine?.project) {
|
|
84
|
+
try {
|
|
85
|
+
const sourceFile = this.semanticEngine.project.getSourceFileByFilePath(filePath);
|
|
86
|
+
if (sourceFile) {
|
|
87
|
+
const violations = await this.symbolBasedAnalyzer.analyzeFileWithSymbols(filePath, options);
|
|
88
|
+
|
|
89
|
+
if (this.verbose) {
|
|
90
|
+
console.log(`🧠 [C033-SYMBOL] ${filePath}: Found ${violations.length} violations`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return violations.map(v => ({ ...v, analysisStrategy: 'symbol-based' }));
|
|
94
|
+
} else {
|
|
95
|
+
if (this.verbose) {
|
|
96
|
+
console.log(`⚠️ [C033-SYMBOL] ${filePath}: Source file not found in ts-morph project, falling back to regex`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (this.verbose) {
|
|
101
|
+
console.warn(`❌ [C033-SYMBOL] ${filePath}: Symbol analysis failed, falling back to regex:`, error.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if (this.verbose) {
|
|
106
|
+
const reason = !this.config.useSymbolBased ? 'Symbol-based disabled' : 'No semantic engine';
|
|
107
|
+
console.log(`⚠️ [C033] ${filePath}: Skipping symbol analysis (${reason}), using regex`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Fallback to Regex-based analysis (only if symbol fails or unavailable)
|
|
112
|
+
if (this.config.fallbackToRegex && !this.config.symbolBasedOnly) {
|
|
113
|
+
try {
|
|
114
|
+
const violations = await this.regexBasedAnalyzer.analyzeFileBasic(filePath, options);
|
|
115
|
+
|
|
116
|
+
if (this.verbose) {
|
|
117
|
+
console.log(`🔄 [C033-REGEX] ${filePath}: Found ${violations.length} violations`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return violations.map(v => ({ ...v, analysisStrategy: 'regex-fallback' }));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (this.verbose) {
|
|
123
|
+
console.warn(`❌ [C033-REGEX] ${filePath}: Regex fallback also failed:`, error.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Legacy compatibility methods
|
|
132
|
+
async analyzeWithSemantics(filePath, options = {}) {
|
|
133
|
+
return await this.analyzeFile(filePath, options);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
137
|
+
// Force regex-based for legacy compatibility
|
|
138
|
+
const violations = await this.regexBasedAnalyzer.analyzeFileBasic(filePath, options);
|
|
139
|
+
return violations.map(v => ({ ...v, analysisStrategy: 'regex-legacy' }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Configuration methods
|
|
143
|
+
enableSymbolBasedOnly() {
|
|
144
|
+
this.config.symbolBasedOnly = true;
|
|
145
|
+
this.config.fallbackToRegex = false;
|
|
146
|
+
if (this.verbose) {
|
|
147
|
+
console.log(`[C033] Switched to symbol-based only mode`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
enableHybridMode() {
|
|
152
|
+
this.config.symbolBasedOnly = false;
|
|
153
|
+
this.config.fallbackToRegex = true;
|
|
154
|
+
if (this.verbose) {
|
|
155
|
+
console.log(`[C033] Switched to hybrid mode (symbol-based + regex fallback)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = C033Analyzer;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "C033_separate_service_repository",
|
|
3
|
+
"name": "C033_separate_service_repository",
|
|
4
|
+
"category": "architecture",
|
|
5
|
+
"description": "C033 - Tách logic xử lý và truy vấn dữ liệu trong service layer",
|
|
6
|
+
"severity": "warning",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"priority": "high",
|
|
11
|
+
"fallback": "heuristic"
|
|
12
|
+
},
|
|
13
|
+
"patterns": {
|
|
14
|
+
"include": [
|
|
15
|
+
"**/*.js",
|
|
16
|
+
"**/*.ts",
|
|
17
|
+
"**/*.jsx",
|
|
18
|
+
"**/*.tsx"
|
|
19
|
+
],
|
|
20
|
+
"exclude": [
|
|
21
|
+
"**/*.test.*",
|
|
22
|
+
"**/*.spec.*",
|
|
23
|
+
"**/*.mock.*",
|
|
24
|
+
"**/test/**",
|
|
25
|
+
"**/tests/**",
|
|
26
|
+
"**/spec/**"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"options": {
|
|
30
|
+
"strictMode": false,
|
|
31
|
+
"allowedDbMethods": [],
|
|
32
|
+
"repositoryPatterns": [
|
|
33
|
+
"*Repository*",
|
|
34
|
+
"*Repo*",
|
|
35
|
+
"*DAO*",
|
|
36
|
+
"*Store*"
|
|
37
|
+
],
|
|
38
|
+
"servicePatterns": [
|
|
39
|
+
"*Service*",
|
|
40
|
+
"*UseCase*",
|
|
41
|
+
"*Handler*",
|
|
42
|
+
"*Manager*"
|
|
43
|
+
],
|
|
44
|
+
"complexityThreshold": {
|
|
45
|
+
"methodLength": 200,
|
|
46
|
+
"cyclomaticComplexity": 5,
|
|
47
|
+
"nestedDepth": 3
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|