@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/CONTRIBUTING.md +249 -605
  3. package/README.md +3 -4
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/large-project.json +143 -0
  7. package/config/presets/all.json +0 -1
  8. package/config/release.json +70 -0
  9. package/config/rule-analysis-strategies.js +38 -3
  10. package/config/rules/enhanced-rules-registry.json +474 -1179
  11. package/config/rules/rules-registry-generated.json +3 -3
  12. package/core/cli-action-handler.js +24 -30
  13. package/core/cli-program.js +11 -3
  14. package/core/config-merger.js +29 -2
  15. package/core/enhanced-rules-registry.js +3 -2
  16. package/core/semantic-engine.js +129 -19
  17. package/core/semantic-rule-base.js +4 -2
  18. package/core/unified-rule-registry.js +1 -1
  19. package/docs/COMMAND-EXAMPLES.md +134 -0
  20. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  21. package/engines/heuristic-engine.js +135 -16
  22. package/integrations/eslint/plugin/index.js +0 -2
  23. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  24. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  25. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  26. package/origin-rules/common-en.md +19 -15
  27. package/package.json +1 -1
  28. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  29. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  30. package/rules/common/C006_function_naming/analyzer.js +29 -3
  31. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  32. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  33. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  34. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  35. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  36. package/rules/common/C013_no_dead_code/config.json +61 -0
  37. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  38. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  39. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  40. package/rules/common/C014_dependency_injection/config.json +26 -0
  41. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  42. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  43. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  44. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  45. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  46. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  47. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  48. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  49. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  50. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  51. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  52. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  53. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  56. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  57. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  58. package/rules/common/C033_separate_service_repository/README.md +78 -0
  59. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  60. package/rules/common/C033_separate_service_repository/config.json +50 -0
  61. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  62. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  63. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  64. package/rules/common/C035_error_logging_context/analyzer.js +232 -0
  65. package/rules/common/C035_error_logging_context/config.json +54 -0
  66. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  67. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  68. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  69. package/rules/common/C040_centralized_validation/config.json +46 -0
  70. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  71. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  72. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  73. package/rules/common/C076_explicit_function_types/README.md +30 -0
  74. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  75. package/rules/common/C076_explicit_function_types/config.json +15 -0
  76. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  77. package/rules/index.js +6 -1
  78. package/rules/parser/rule-parser.js +13 -2
  79. package/rules/security/S005_no_origin_auth/README.md +226 -0
  80. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  81. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  82. package/rules/security/S005_no_origin_auth/config.json +85 -0
  83. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  84. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  85. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  86. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  87. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  88. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  89. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  90. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  91. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  92. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  93. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  94. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  95. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  96. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  97. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  98. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  99. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  100. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  101. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  102. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  103. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  104. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  105. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  106. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  107. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  108. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  109. package/rules/security/S055_content_type_validation/README.md +176 -0
  110. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  111. package/rules/security/S055_content_type_validation/config.json +48 -0
  112. package/rules/utils/rule-helpers.js +140 -1
  113. package/scripts/consolidate-config.js +116 -0
  114. package/scripts/prepare-release.sh +1 -1
  115. package/config/rules/rules-registry.json +0 -765
  116. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  117. package/docs/FUTURE_PACKAGES.md +0 -83
  118. package/docs/HEURISTIC_VS_AI.md +0 -113
  119. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  120. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  121. package/docs/RELEASE_GUIDE.md +0 -230
  122. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  123. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  124. 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
+ }