@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
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom ESLint rule for: C076 – Each test should assert only one behavior
|
|
3
|
-
* Rule ID: custom/c076
|
|
4
|
-
* Purpose: Ensure test functions focus on testing a single behavior/aspect
|
|
5
|
-
* Note: Similar to C072 but with broader scope and different focus
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
module.exports = {
|
|
9
|
-
meta: {
|
|
10
|
-
type: "suggestion",
|
|
11
|
-
docs: {
|
|
12
|
-
description: "Each test should assert only one behavior to maintain focus and clarity",
|
|
13
|
-
recommended: true,
|
|
14
|
-
category: "Testing"
|
|
15
|
-
},
|
|
16
|
-
schema: [
|
|
17
|
-
{
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
maxAssertions: {
|
|
21
|
-
type: "integer",
|
|
22
|
-
minimum: 1,
|
|
23
|
-
default: 1
|
|
24
|
-
},
|
|
25
|
-
allowSetupAssertions: {
|
|
26
|
-
type: "boolean",
|
|
27
|
-
default: true
|
|
28
|
-
},
|
|
29
|
-
assertionPatterns: {
|
|
30
|
-
type: "array",
|
|
31
|
-
items: {
|
|
32
|
-
type: "string"
|
|
33
|
-
},
|
|
34
|
-
default: ["expect", "assert", "should"]
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
additionalProperties: false
|
|
38
|
-
}
|
|
39
|
-
],
|
|
40
|
-
messages: {
|
|
41
|
-
tooManyAssertions: "Test '{{testName}}' has {{count}} assertions. Each test should focus on one behavior (max {{max}} assertions)",
|
|
42
|
-
multipleBehaviors: "Test '{{testName}}' appears to test multiple behaviors. Consider splitting into separate test cases"
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
create(context) {
|
|
46
|
-
const options = context.options[0] || {};
|
|
47
|
-
const maxAssertions = options.maxAssertions || 1;
|
|
48
|
-
const allowSetupAssertions = options.allowSetupAssertions !== false;
|
|
49
|
-
const assertionPatterns = options.assertionPatterns || ["expect", "assert", "should"];
|
|
50
|
-
|
|
51
|
-
function isTestFunction(node) {
|
|
52
|
-
if (!node || !node.callee) return false;
|
|
53
|
-
|
|
54
|
-
if (node.type === "CallExpression") {
|
|
55
|
-
// Direct test/it/describe calls
|
|
56
|
-
if (node.callee.type === "Identifier") {
|
|
57
|
-
return ["test", "it", "describe", "context"].includes(node.callee.name);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Method calls like jest.test, mocha.it, etc.
|
|
61
|
-
if (node.callee.type === "MemberExpression" &&
|
|
62
|
-
node.callee.property &&
|
|
63
|
-
["test", "it", "describe", "context"].includes(node.callee.property.name)) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isSetupFunction(node) {
|
|
71
|
-
if (!node || !node.callee) return false;
|
|
72
|
-
|
|
73
|
-
const setupFunctions = ["beforeEach", "afterEach", "beforeAll", "afterAll", "before", "after"];
|
|
74
|
-
|
|
75
|
-
if (node.type === "CallExpression") {
|
|
76
|
-
if (node.callee.type === "Identifier") {
|
|
77
|
-
return setupFunctions.includes(node.callee.name);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (node.callee.type === "MemberExpression" &&
|
|
81
|
-
node.callee.property &&
|
|
82
|
-
setupFunctions.includes(node.callee.property.name)) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isAssertionCall(node) {
|
|
90
|
-
if (!node || node.type !== "CallExpression" || !node.callee) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Direct assertion calls
|
|
95
|
-
if (node.callee.type === "Identifier") {
|
|
96
|
-
return assertionPatterns.includes(node.callee.name);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Method calls like chai.expect, jest.expect, etc.
|
|
100
|
-
if (node.callee.type === "MemberExpression" && node.callee.property) {
|
|
101
|
-
return assertionPatterns.includes(node.callee.property.name);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function countAssertions(testBody, testName) {
|
|
108
|
-
let assertionCount = 0;
|
|
109
|
-
let setupAssertionCount = 0;
|
|
110
|
-
let hasMultipleBehaviorIndicators = false;
|
|
111
|
-
|
|
112
|
-
function traverse(node) {
|
|
113
|
-
if (!node || typeof node !== 'object') return;
|
|
114
|
-
|
|
115
|
-
// Count assertions
|
|
116
|
-
if (isAssertionCall(node)) {
|
|
117
|
-
// Check if this assertion is in setup/teardown
|
|
118
|
-
let parent = node;
|
|
119
|
-
let inSetup = false;
|
|
120
|
-
while (parent && parent.parent) {
|
|
121
|
-
parent = parent.parent;
|
|
122
|
-
if (parent.type === "CallExpression" && isSetupFunction(parent)) {
|
|
123
|
-
inSetup = true;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (inSetup) {
|
|
129
|
-
setupAssertionCount++;
|
|
130
|
-
} else {
|
|
131
|
-
assertionCount++;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Look for multiple behavior indicators
|
|
136
|
-
if (node.type === "CallExpression") {
|
|
137
|
-
// Multiple nested test functions indicate multiple behaviors
|
|
138
|
-
if (isTestFunction(node) && node !== testBody.parent) {
|
|
139
|
-
hasMultipleBehaviorIndicators = true;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Look for comment patterns that suggest multiple behaviors
|
|
144
|
-
if (node.type === "ExpressionStatement" && node.leadingComments) {
|
|
145
|
-
const comments = node.leadingComments.map(c => c.value.toLowerCase());
|
|
146
|
-
const behaviorKeywords = ["and", "also", "then", "next", "additionally", "furthermore"];
|
|
147
|
-
if (comments.some(comment =>
|
|
148
|
-
behaviorKeywords.some(keyword => comment.includes(keyword)))) {
|
|
149
|
-
hasMultipleBehaviorIndicators = true;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Recursively check child nodes, but don't go into nested test functions
|
|
154
|
-
for (const key in node) {
|
|
155
|
-
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
156
|
-
|
|
157
|
-
const child = node[key];
|
|
158
|
-
if (Array.isArray(child)) {
|
|
159
|
-
child.forEach(item => {
|
|
160
|
-
if (item && typeof item === 'object' && item.type) {
|
|
161
|
-
if (!(item.type === "CallExpression" && isTestFunction(item))) {
|
|
162
|
-
traverse(item);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
} else if (child && typeof child === 'object' && child.type) {
|
|
167
|
-
if (!(child.type === "CallExpression" && isTestFunction(child))) {
|
|
168
|
-
traverse(child);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
traverse(testBody);
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
assertions: assertionCount,
|
|
178
|
-
setupAssertions: setupAssertionCount,
|
|
179
|
-
hasMultipleBehaviors: hasMultipleBehaviorIndicators
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
CallExpression(node) {
|
|
185
|
-
// Only check test function calls, not describe blocks
|
|
186
|
-
if (!isTestFunction(node) || (node.callee.type === "Identifier" && node.callee.name === "describe")) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Skip describe blocks
|
|
191
|
-
if (node.callee.type === "Identifier" && ["describe", "context"].includes(node.callee.name)) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Must have test name and callback
|
|
196
|
-
if (!node.arguments || node.arguments.length < 2) return;
|
|
197
|
-
|
|
198
|
-
const testName = node.arguments[0];
|
|
199
|
-
const testCallback = node.arguments[1];
|
|
200
|
-
|
|
201
|
-
if (!testCallback ||
|
|
202
|
-
(testCallback.type !== "FunctionExpression" &&
|
|
203
|
-
testCallback.type !== "ArrowFunctionExpression")) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const testNameStr = testName.type === "Literal" ? testName.value :
|
|
208
|
-
testName.type === "TemplateLiteral" ? "template" : "unnamed";
|
|
209
|
-
|
|
210
|
-
// Get function body
|
|
211
|
-
const fnBody = testCallback.body;
|
|
212
|
-
if (!fnBody) return;
|
|
213
|
-
|
|
214
|
-
// Handle both block statements and expression bodies
|
|
215
|
-
let bodyToCheck = fnBody;
|
|
216
|
-
if (testCallback.type === "ArrowFunctionExpression" && fnBody.type !== "BlockStatement") {
|
|
217
|
-
bodyToCheck = { type: "BlockStatement", body: [{ type: "ExpressionStatement", expression: fnBody }] };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Count assertions and analyze behavior
|
|
221
|
-
const analysis = countAssertions(bodyToCheck, testNameStr);
|
|
222
|
-
|
|
223
|
-
// Calculate effective assertions (exclude setup if allowed)
|
|
224
|
-
const effectiveAssertions = allowSetupAssertions ?
|
|
225
|
-
analysis.assertions :
|
|
226
|
-
analysis.assertions + analysis.setupAssertions;
|
|
227
|
-
|
|
228
|
-
// Report if too many assertions
|
|
229
|
-
if (effectiveAssertions > maxAssertions) {
|
|
230
|
-
context.report({
|
|
231
|
-
node,
|
|
232
|
-
messageId: "tooManyAssertions",
|
|
233
|
-
data: {
|
|
234
|
-
testName: testNameStr,
|
|
235
|
-
count: effectiveAssertions,
|
|
236
|
-
max: maxAssertions
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Report if multiple behaviors detected
|
|
242
|
-
if (analysis.hasMultipleBehaviors) {
|
|
243
|
-
context.report({
|
|
244
|
-
node,
|
|
245
|
-
messageId: "multipleBehaviors",
|
|
246
|
-
data: {
|
|
247
|
-
testName: testNameStr
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
};
|
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
cons async analyze(files, language, options = {}) {
|
|
3
|
-
// Use Smart Pipeline as primary choice
|
|
4
|
-
if (this.smartPipeline) {
|
|
5
|
-
console.log('🎯 C029: Using Smart Pipeline (3-stage analysis)...');
|
|
6
|
-
return await this.smartPipeline.analyze(files, language, options);
|
|
7
|
-
} else if (this.astAnalyzer) {
|
|
8
|
-
console.log('🚀 C029: Using AST-enhanced analysis...');
|
|
9
|
-
return await this.astAnalyzer.analyze(files, language, options);
|
|
10
|
-
} else {
|
|
11
|
-
console.log('🔍 C029: Using regex-based analysis...');
|
|
12
|
-
return await this.analyzeWithRegex(files, language, options);
|
|
13
|
-
}
|
|
14
|
-
}ire('path');
|
|
15
|
-
const { PatternMatcher } = require('../../utils/pattern-matchers');
|
|
16
|
-
const { RuleHelper } = require('../../utils/rule-helpers');
|
|
17
|
-
|
|
18
|
-
class C029Analyzer {
|
|
19
|
-
constructor() {
|
|
20
|
-
this.ruleId = 'C029';
|
|
21
|
-
this.ruleName = 'Enhanced Catch Block Error Logging';
|
|
22
|
-
this.description = 'Mọi catch block phải log nguyên nhân lỗi đầy đủ và bảo toàn context (Smart Pipeline 3-stage analysis)';
|
|
23
|
-
|
|
24
|
-
// Load Smart Pipeline as primary analyzer
|
|
25
|
-
this.smartPipeline = null;
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
this.smartPipeline = require('./analyzer-smart-pipeline.js');
|
|
29
|
-
console.log('🎯 C029: Smart Pipeline loaded (3-stage: Regex → AST → Data Flow)');
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.warn('⚠️ C029: Smart Pipeline failed, falling back:', error.message);
|
|
32
|
-
|
|
33
|
-
// Fallback to simpler analyzers
|
|
34
|
-
try {
|
|
35
|
-
this.astAnalyzer = require('./ast-analyzer.js');
|
|
36
|
-
console.log('🚀 C029: AST analyzer loaded (hybrid approach)');
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.warn('⚠️ C029: AST analyzer failed:', error.message);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async analyze(files, language, options = {}) {
|
|
44
|
-
// Use Smart Pipeline as primary choice
|
|
45
|
-
if (this.smartPipeline) {
|
|
46
|
-
console.log('🎯 C029: Using Smart Pipeline (3-stage analysis)...');
|
|
47
|
-
return await this.smartPipeline.analyze(files, language, options);
|
|
48
|
-
} else if (this.astAnalyzer) {
|
|
49
|
-
console.log('🚀 C029: Using AST-enhanced analysis...');
|
|
50
|
-
return await this.astAnalyzer.analyze(files, language, options);
|
|
51
|
-
} else {
|
|
52
|
-
console.log('🔍 C029: Using regex-based analysis...');
|
|
53
|
-
return await this.analyzeWithRegex(files, language, options);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
console.log('🧠 C029: Using Data Flow analysis (IDE-level)...');
|
|
57
|
-
return await this.dataFlowAnalyzer.analyze(files, language, options);
|
|
58
|
-
} else if (this.astAnalyzer) {
|
|
59
|
-
console.log('� C029: Using AST-enhanced analysis...');
|
|
60
|
-
return await this.astAnalyzer.analyze(files, language, options);
|
|
61
|
-
} else {
|
|
62
|
-
console.log('🔍 C029: Using regex-based analysis...');
|
|
63
|
-
return await this.analyzeWithRegex(files, language, options);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async analyzeWithRegex(files, language, options = {}) {
|
|
68
|
-
const violations = [];
|
|
69
|
-
|
|
70
|
-
for (const filePath of files) {
|
|
71
|
-
if (options.verbose) {
|
|
72
|
-
console.log(`🔍 Running pattern analysis on ${path.basename(filePath)}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
77
|
-
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
78
|
-
violations.push(...fileViolations);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return violations;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async analyzeFile(filePath, content, language, config) {
|
|
88
|
-
switch (language) {
|
|
89
|
-
case 'typescript':
|
|
90
|
-
case 'javascript':
|
|
91
|
-
return this.analyzeTypeScript(filePath, content, config);
|
|
92
|
-
default:
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async analyzeTypeScript(filePath, content, config) {
|
|
98
|
-
const violations = [];
|
|
99
|
-
const lines = content.split('\n');
|
|
100
|
-
|
|
101
|
-
// Focus on core functionality - only detect truly silent catch blocks
|
|
102
|
-
violations.push(...this.findSilentCatchBlocks(lines, filePath));
|
|
103
|
-
// Disabled strict checks to reduce false positives:
|
|
104
|
-
// violations.push(...this.findInadequateErrorLogging(lines, filePath));
|
|
105
|
-
// violations.push(...this.findMissingErrorContext(lines, filePath));
|
|
106
|
-
|
|
107
|
-
return violations;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Core functionality from ESLint C029 - detect silent catch blocks
|
|
112
|
-
*/
|
|
113
|
-
findSilentCatchBlocks(lines, filePath) {
|
|
114
|
-
const violations = [];
|
|
115
|
-
|
|
116
|
-
lines.forEach((line, index) => {
|
|
117
|
-
const lineNumber = index + 1;
|
|
118
|
-
const trimmedLine = line.trim();
|
|
119
|
-
|
|
120
|
-
// Detect catch block start
|
|
121
|
-
if (this.isCatchBlockStart(trimmedLine)) {
|
|
122
|
-
const catchBlockInfo = this.extractCatchBlockInfo(lines, index);
|
|
123
|
-
|
|
124
|
-
if (catchBlockInfo.isEmpty) {
|
|
125
|
-
violations.push({
|
|
126
|
-
ruleId: this.ruleId,
|
|
127
|
-
file: filePath,
|
|
128
|
-
line: lineNumber,
|
|
129
|
-
column: line.indexOf('catch') + 1,
|
|
130
|
-
message: 'Empty catch block - error is silently ignored',
|
|
131
|
-
severity: 'error',
|
|
132
|
-
code: trimmedLine,
|
|
133
|
-
type: 'empty_catch_block',
|
|
134
|
-
confidence: 1.0,
|
|
135
|
-
suggestion: 'Add error logging or rethrowing in catch block'
|
|
136
|
-
});
|
|
137
|
-
} else if (!catchBlockInfo.hasLoggingOrRethrow) {
|
|
138
|
-
violations.push({
|
|
139
|
-
ruleId: this.ruleId,
|
|
140
|
-
file: filePath,
|
|
141
|
-
line: lineNumber,
|
|
142
|
-
column: line.indexOf('catch') + 1,
|
|
143
|
-
message: 'Catch block must log error or rethrow - silent error handling hides bugs',
|
|
144
|
-
severity: 'error',
|
|
145
|
-
code: trimmedLine,
|
|
146
|
-
type: 'silent_catch_block',
|
|
147
|
-
confidence: 0.9,
|
|
148
|
-
suggestion: 'Add error logging (console.error, logger.error) or rethrow the error'
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
return violations;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* SunLint enhanced functionality - detect inadequate error logging
|
|
159
|
-
*/
|
|
160
|
-
findInadequateErrorLogging(lines, filePath) {
|
|
161
|
-
const violations = [];
|
|
162
|
-
|
|
163
|
-
lines.forEach((line, index) => {
|
|
164
|
-
const lineNumber = index + 1;
|
|
165
|
-
const trimmedLine = line.trim();
|
|
166
|
-
|
|
167
|
-
if (this.isCatchBlockStart(trimmedLine)) {
|
|
168
|
-
const catchBlockInfo = this.extractCatchBlockInfo(lines, index);
|
|
169
|
-
|
|
170
|
-
if (catchBlockInfo.hasLoggingOrRethrow && !catchBlockInfo.hasAdequateLogging) {
|
|
171
|
-
violations.push({
|
|
172
|
-
ruleId: this.ruleId,
|
|
173
|
-
file: filePath,
|
|
174
|
-
line: lineNumber,
|
|
175
|
-
column: line.indexOf('catch') + 1,
|
|
176
|
-
message: 'Error logging should include error message, stack trace, and context',
|
|
177
|
-
severity: 'warning',
|
|
178
|
-
code: trimmedLine,
|
|
179
|
-
type: 'inadequate_error_logging',
|
|
180
|
-
confidence: 0.8,
|
|
181
|
-
suggestion: 'Include error.message, error.stack, and relevant context in error logging'
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
return violations;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* SunLint enhanced functionality - detect missing error context
|
|
192
|
-
* DISABLED: Too strict, causing false positives
|
|
193
|
-
*/
|
|
194
|
-
findMissingErrorContext(lines, filePath) {
|
|
195
|
-
// Disabled to reduce false positives
|
|
196
|
-
return [];
|
|
197
|
-
|
|
198
|
-
/* Original strict implementation:
|
|
199
|
-
const violations = [];
|
|
200
|
-
|
|
201
|
-
lines.forEach((line, index) => {
|
|
202
|
-
const lineNumber = index + 1;
|
|
203
|
-
const trimmedLine = line.trim();
|
|
204
|
-
|
|
205
|
-
if (this.isCatchBlockStart(trimmedLine)) {
|
|
206
|
-
const catchBlockInfo = this.extractCatchBlockInfo(lines, index);
|
|
207
|
-
|
|
208
|
-
if (catchBlockInfo.hasLoggingOrRethrow && !catchBlockInfo.hasContextualLogging) {
|
|
209
|
-
violations.push({
|
|
210
|
-
ruleId: this.ruleId,
|
|
211
|
-
file: filePath,
|
|
212
|
-
line: lineNumber,
|
|
213
|
-
column: line.indexOf('catch') + 1,
|
|
214
|
-
message: 'Error logging should include operational context (function name, input parameters)',
|
|
215
|
-
severity: 'info',
|
|
216
|
-
code: trimmedLine,
|
|
217
|
-
type: 'missing_error_context',
|
|
218
|
-
confidence: 0.7,
|
|
219
|
-
suggestion: 'Include function name, input parameters, and operational context in error logging'
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return violations;
|
|
226
|
-
*/
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
isCatchBlockStart(line) {
|
|
230
|
-
return line.includes('catch (') || line.includes('catch(');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
extractCatchBlockInfo(lines, startIndex) {
|
|
234
|
-
const catchBlockLines = [];
|
|
235
|
-
let braceDepth = 0;
|
|
236
|
-
let foundCatchBrace = false;
|
|
237
|
-
|
|
238
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
239
|
-
const line = lines[i];
|
|
240
|
-
catchBlockLines.push(line);
|
|
241
|
-
|
|
242
|
-
// Check if this is the catch line with opening brace
|
|
243
|
-
if (this.isCatchBlockStart(line.trim())) {
|
|
244
|
-
// Count braces in the catch line itself
|
|
245
|
-
for (const char of line) {
|
|
246
|
-
if (char === '{') {
|
|
247
|
-
foundCatchBrace = true;
|
|
248
|
-
braceDepth = 1; // Start counting from 1 for the catch block
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
} else if (foundCatchBrace) {
|
|
252
|
-
// Count braces in subsequent lines
|
|
253
|
-
for (const char of line) {
|
|
254
|
-
if (char === '{') {
|
|
255
|
-
braceDepth++;
|
|
256
|
-
} else if (char === '}') {
|
|
257
|
-
braceDepth--;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// If we've closed all braces, we're done
|
|
262
|
-
if (braceDepth === 0) {
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const content = catchBlockLines.join('\n').toLowerCase();
|
|
269
|
-
const originalContent = catchBlockLines.join('\n');
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
isEmpty: this.isCatchBlockEmpty(catchBlockLines),
|
|
273
|
-
hasLoggingOrRethrow: this.hasLoggingOrRethrow(content, originalContent),
|
|
274
|
-
hasAdequateLogging: this.hasAdequateErrorLogging(content, originalContent),
|
|
275
|
-
hasContextualLogging: this.hasContextualErrorLogging(content, originalContent)
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
isCatchBlockEmpty(catchBlockLines) {
|
|
280
|
-
if (catchBlockLines.length === 0) return true;
|
|
281
|
-
|
|
282
|
-
// Join all lines and find the content between braces
|
|
283
|
-
const fullContent = catchBlockLines.join('\n');
|
|
284
|
-
|
|
285
|
-
// Find the opening brace and closing brace
|
|
286
|
-
let openBraceIndex = fullContent.indexOf('{');
|
|
287
|
-
let closeBraceIndex = fullContent.lastIndexOf('}');
|
|
288
|
-
|
|
289
|
-
if (openBraceIndex === -1 || closeBraceIndex === -1) {
|
|
290
|
-
return true; // Malformed catch block
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Extract body content between braces
|
|
294
|
-
const bodyContent = fullContent.substring(openBraceIndex + 1, closeBraceIndex).trim();
|
|
295
|
-
|
|
296
|
-
// Remove comments and whitespace
|
|
297
|
-
const cleanedBody = bodyContent
|
|
298
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
299
|
-
.replace(/\/\/.*$/gm, '') // Remove line comments
|
|
300
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
301
|
-
.trim();
|
|
302
|
-
|
|
303
|
-
return cleanedBody.length === 0;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
hasLoggingOrRethrow(content, originalContent) {
|
|
307
|
-
// Check for throw statements
|
|
308
|
-
if (content.includes('throw ') || content.includes('throw;')) {
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check for test assertions - valid form of error handling
|
|
313
|
-
const testPatterns = [
|
|
314
|
-
'expect(',
|
|
315
|
-
'assert(',
|
|
316
|
-
'tobeinstanceof',
|
|
317
|
-
'toequal(',
|
|
318
|
-
'tohavebeencalled',
|
|
319
|
-
'toBe(',
|
|
320
|
-
'toHaveBeenCalledWith'
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
const hasTestAssertions = testPatterns.some(pattern =>
|
|
324
|
-
content.includes(pattern.toLowerCase()) || originalContent.toLowerCase().includes(pattern.toLowerCase())
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
if (hasTestAssertions) {
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Check for Redux/async thunk error handling
|
|
332
|
-
const reduxErrorHandlers = [
|
|
333
|
-
'handleaxioserror',
|
|
334
|
-
'rejectwithvalue',
|
|
335
|
-
'dispatch(',
|
|
336
|
-
'seterror(',
|
|
337
|
-
'return value',
|
|
338
|
-
'return rejectwithvalue'
|
|
339
|
-
];
|
|
340
|
-
|
|
341
|
-
const hasReduxHandling = reduxErrorHandlers.some(pattern =>
|
|
342
|
-
content.includes(pattern.toLowerCase()) || originalContent.toLowerCase().includes(pattern.toLowerCase())
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
if (hasReduxHandling) {
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Check for logging patterns (expanded from original)
|
|
350
|
-
const loggingPatterns = [
|
|
351
|
-
'console.error',
|
|
352
|
-
'console.log',
|
|
353
|
-
'console.warn',
|
|
354
|
-
'logger.error',
|
|
355
|
-
'log.error',
|
|
356
|
-
'logger.warn',
|
|
357
|
-
'log.warn',
|
|
358
|
-
'winston.error',
|
|
359
|
-
'bunyan.error',
|
|
360
|
-
'pino.error',
|
|
361
|
-
'.error(',
|
|
362
|
-
'.warn(',
|
|
363
|
-
'.log('
|
|
364
|
-
];
|
|
365
|
-
|
|
366
|
-
// Accept basic error logging - don't require context
|
|
367
|
-
const hasBasicLogging = loggingPatterns.some(pattern =>
|
|
368
|
-
content.includes(pattern) || originalContent.includes(pattern)
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
return hasBasicLogging;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
hasAdequateErrorLogging(content, originalContent) {
|
|
375
|
-
// Check for error properties being logged
|
|
376
|
-
const errorProperties = [
|
|
377
|
-
'error.message',
|
|
378
|
-
'error.stack',
|
|
379
|
-
'err.message',
|
|
380
|
-
'err.stack',
|
|
381
|
-
'e.message',
|
|
382
|
-
'e.stack',
|
|
383
|
-
'error.name',
|
|
384
|
-
'error.cause'
|
|
385
|
-
];
|
|
386
|
-
|
|
387
|
-
const hasErrorProperties = errorProperties.some(prop =>
|
|
388
|
-
content.includes(prop) || originalContent.includes(prop)
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
// Check for comprehensive error logging
|
|
392
|
-
const hasLogging = this.hasLoggingOrRethrow(content, originalContent);
|
|
393
|
-
|
|
394
|
-
return hasLogging && hasErrorProperties;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
hasContextualErrorLogging(content, originalContent) {
|
|
398
|
-
// Check for contextual information in logging
|
|
399
|
-
const contextualPatterns = [
|
|
400
|
-
'function',
|
|
401
|
-
'method',
|
|
402
|
-
'operation',
|
|
403
|
-
'input',
|
|
404
|
-
'parameters',
|
|
405
|
-
'context',
|
|
406
|
-
'state',
|
|
407
|
-
'request',
|
|
408
|
-
'response',
|
|
409
|
-
'userId',
|
|
410
|
-
'sessionId',
|
|
411
|
-
'transactionId'
|
|
412
|
-
];
|
|
413
|
-
|
|
414
|
-
// Check if logging includes contextual information
|
|
415
|
-
const hasContextualInfo = contextualPatterns.some(pattern =>
|
|
416
|
-
content.includes(pattern) || originalContent.includes(pattern)
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
// Check for template literals or string concatenation (indicates contextual logging)
|
|
420
|
-
const hasTemplateLogging = originalContent.includes('${') || originalContent.includes('" + ') || originalContent.includes("' + ");
|
|
421
|
-
|
|
422
|
-
return hasContextualInfo || hasTemplateLogging;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
module.exports = new C029Analyzer();
|