@sun-asterisk/sunlint 1.0.7 → 1.1.4
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/.sunlint.json +35 -0
- package/CHANGELOG.md +30 -3
- package/CONTRIBUTING.md +235 -0
- package/PROJECT_STRUCTURE.md +60 -0
- package/README.md +146 -58
- package/cli.js +1 -0
- package/config/README.md +88 -0
- package/config/defaults/ai-rules-context.json +231 -0
- package/config/engines/engines.json +49 -0
- package/config/engines/eslint-rule-mapping.json +74 -0
- package/config/eslint-rule-mapping.json +126 -0
- package/config/integrations/eslint/base.config.js +125 -0
- package/config/integrations/eslint/simple.config.js +24 -0
- package/config/presets/strict.json +0 -1
- package/config/rule-analysis-strategies.js +74 -0
- package/config/{rules-registry.json → rules/rules-registry.json} +30 -7
- package/core/analysis-orchestrator.js +383 -591
- package/core/ast-modules/README.md +103 -0
- package/core/ast-modules/base-parser.js +90 -0
- package/core/ast-modules/index.js +97 -0
- package/core/ast-modules/package.json +37 -0
- package/core/ast-modules/parsers/eslint-js-parser.js +153 -0
- package/core/ast-modules/parsers/eslint-ts-parser.js +98 -0
- package/core/ast-modules/parsers/javascript-parser.js +187 -0
- package/core/ast-modules/parsers/typescript-parser.js +187 -0
- package/core/cli-action-handler.js +271 -255
- package/core/cli-program.js +18 -4
- package/core/config-manager.js +9 -3
- package/core/config-merger.js +40 -1
- package/core/config-validator.js +2 -2
- package/core/dependency-checker.js +125 -0
- package/core/enhanced-rules-registry.js +331 -0
- package/core/file-targeting-service.js +92 -23
- package/core/interfaces/analysis-engine.interface.js +100 -0
- package/core/multi-rule-runner.js +0 -221
- package/core/output-service.js +1 -1
- package/core/rule-mapping-service.js +1 -1
- package/core/rule-selection-service.js +10 -2
- package/core/smart-installer.js +164 -0
- package/docs/AI.md +163 -0
- package/docs/ARCHITECTURE.md +78 -0
- package/docs/CI-CD-GUIDE.md +315 -0
- package/docs/COMMAND-EXAMPLES.md +256 -0
- package/docs/CONFIGURATION.md +414 -0
- package/docs/DEBUG.md +86 -0
- package/docs/DEPENDENCIES.md +90 -0
- package/docs/DEPLOYMENT-STRATEGIES.md +270 -0
- package/docs/DISTRIBUTION.md +153 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
- package/docs/ESLINT_INTEGRATION.md +238 -0
- package/docs/FOLDER_STRUCTURE.md +59 -0
- package/docs/FUTURE_PACKAGES.md +83 -0
- package/docs/HEURISTIC_VS_AI.md +113 -0
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +112 -0
- package/docs/PRODUCTION_SIZE_IMPACT.md +183 -0
- package/docs/README.md +32 -0
- package/docs/RELEASE_GUIDE.md +230 -0
- package/engines/eslint-engine.js +610 -0
- package/engines/heuristic-engine.js +864 -0
- package/engines/openai-engine.js +374 -0
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/README.md +99 -0
- package/integrations/eslint/configs/.eslintrc.js +98 -0
- package/integrations/eslint/configs/eslint.config.js +133 -0
- package/integrations/eslint/configs/eslint.config.simple.js +24 -0
- package/integrations/eslint/package.json +23 -0
- package/integrations/eslint/plugin/index.js +164 -0
- package/integrations/eslint/plugin/package.json +13 -0
- package/integrations/eslint/plugin/rules/common/c002-no-duplicate-code.js +204 -0
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +246 -0
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +216 -0
- package/integrations/eslint/plugin/rules/common/c010-limit-block-nesting.js +90 -0
- package/integrations/eslint/plugin/rules/common/c013-no-dead-code.js +78 -0
- package/integrations/eslint/plugin/rules/common/c014-abstract-dependency-preferred.js +38 -0
- package/integrations/eslint/plugin/rules/common/c017-limit-constructor-logic.js +146 -0
- package/integrations/eslint/plugin/rules/common/c018-no-generic-throw.js +335 -0
- package/integrations/eslint/plugin/rules/common/c023-no-duplicate-variable-name-in-scope.js +142 -0
- package/integrations/eslint/plugin/rules/common/c029-catch-block-logging.js +115 -0
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +294 -0
- package/integrations/eslint/plugin/rules/common/c035-no-empty-catch.js +162 -0
- package/integrations/eslint/plugin/rules/common/c041-no-config-inline.js +122 -0
- package/integrations/eslint/plugin/rules/common/c042-boolean-name-prefix.js +406 -0
- package/integrations/eslint/plugin/rules/common/c043-no-console-or-print.js +300 -0
- package/integrations/eslint/plugin/rules/common/c047-no-duplicate-retry-logic.js +239 -0
- package/integrations/eslint/plugin/rules/common/c072-one-assert-per-test.js +184 -0
- package/integrations/eslint/plugin/rules/common/c075-explicit-function-return-types.js +168 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +254 -0
- package/integrations/eslint/plugin/rules/security/s001-fail-securely.js +381 -0
- package/integrations/eslint/plugin/rules/security/s002-idor-check.js +945 -0
- package/integrations/eslint/plugin/rules/security/s003-no-unvalidated-redirect.js +86 -0
- package/integrations/eslint/plugin/rules/security/s007-no-plaintext-otp.js +74 -0
- package/integrations/eslint/plugin/rules/security/s013-verify-tls-connection.js +47 -0
- package/integrations/eslint/plugin/rules/security/s047-secure-random-passwords.js +108 -0
- package/integrations/eslint/plugin/rules/security/s055-verification-rest-check-the-incoming-content-type.js +143 -0
- package/integrations/eslint/plugin/rules/typescript/t002-interface-prefix-i.js +42 -0
- package/integrations/eslint/plugin/rules/typescript/t003-ts-ignore-reason.js +48 -0
- package/integrations/eslint/plugin/rules/typescript/t004-no-empty-type.js +95 -0
- package/integrations/eslint/plugin/rules/typescript/t007-no-fn-in-constructor.js +52 -0
- package/integrations/eslint/plugin/rules/typescript/t010-no-nested-union-tuple.js +48 -0
- package/integrations/eslint/plugin/rules/typescript/t019-no-this-assign.js +81 -0
- package/integrations/eslint/plugin/rules/typescript/t020-no-default-multi-export.js +127 -0
- package/integrations/eslint/plugin/rules/typescript/t021-limit-nested-generics.js +150 -0
- package/integrations/eslint/tsconfig.json +27 -0
- package/package.json +61 -21
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/{C006_function_naming → common/C006_function_naming}/analyzer.js +13 -2
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/{C019_log_level_usage → common/C019_log_level_usage}/analyzer.js +5 -2
- package/rules/{C029_catch_block_logging → common/C029_catch_block_logging}/analyzer.js +49 -15
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +304 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +351 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/index.js +149 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +263 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +264 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/build-release.sh +117 -0
- package/scripts/ci-report.js +179 -0
- package/scripts/install.sh +196 -0
- package/scripts/manual-release.sh +338 -0
- package/scripts/merge-reports.js +424 -0
- package/scripts/pre-release-test.sh +175 -0
- package/scripts/prepare-release.sh +202 -0
- package/scripts/setup-github-registry.sh +42 -0
- package/scripts/test-scripts/README.md +22 -0
- package/scripts/test-scripts/test-c041-comparison.js +114 -0
- package/scripts/test-scripts/test-c041-eslint.js +67 -0
- package/scripts/test-scripts/test-eslint-rules.js +146 -0
- package/scripts/test-scripts/test-real-world.js +44 -0
- package/scripts/test-scripts/test-rules-on-real-projects.js +86 -0
- package/scripts/trigger-release.sh +285 -0
- package/scripts/validate-rule-structure.js +148 -0
- package/scripts/verify-install.sh +82 -0
- package/config/sunlint-schema.json +0 -159
- package/config/typescript/custom-rules.js +0 -9
- package/config/typescript/package-lock.json +0 -1585
- package/config/typescript/package.json +0 -13
- package/config/typescript/security-rules/index.js +0 -90
- package/config/typescript/tsconfig.json +0 -29
- package/core/ai-analyzer.js +0 -169
- package/core/eslint-engine-service.js +0 -312
- package/core/eslint-instance-manager.js +0 -104
- package/core/eslint-integration-service.js +0 -363
- package/core/sunlint-engine-service.js +0 -23
- package/core/typescript-analyzer.js +0 -262
- package/core/typescript-engine.js +0 -313
- /package/config/{default.json → defaults/default.json} +0 -0
- /package/config/{typescript/eslint.config.js → integrations/eslint/typescript.config.js} +0 -0
- /package/config/{typescript/custom-rules-new.js → schemas/sunlint-schema.json} +0 -0
- /package/config/{typescript → testing}/test-s005-working.ts +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s005-no-origin-auth.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s006-activation-recovery-secret-not-plaintext.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s008-crypto-agility.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s009-no-insecure-crypto.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s010-no-insecure-random-in-sensitive-context.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s011-no-insecure-uuid.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s012-hardcode-secret.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s014-insecure-tls-version.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s015-insecure-tls-certificate.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s016-sensitive-query-parameter.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s017-no-sql-injection.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s018-positive-input-validation.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s019-no-raw-user-input-in-email.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s020-no-eval-dynamic-execution.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s022-output-encoding.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s023-no-json-injection.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s025-server-side-input-validation.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s026-json-schema-validation.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s027-no-hardcoded-secrets.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s029-require-csrf-protection.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s030-no-directory-browsing.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s033-require-samesite-cookie.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s034-require-host-cookie-prefix.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s035-cookie-specific-path.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s036-no-unsafe-file-include.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s037-require-anti-cache-headers.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s038-no-version-disclosure.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s039-no-session-token-in-url.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s041-require-session-invalidate-on-logout.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s042-require-periodic-reauthentication.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s043-terminate-sessions-on-password-change.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s044-require-full-session-for-sensitive-operations.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s045-anti-automation-controls.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s046-secure-notification-on-auth-change.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s048-password-credential-recovery.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s050-session-token-weak-hash.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s052-secure-random-authentication-code.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s054-verification-default-account.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s057-utc-logging.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s058-no-ssrf.js +0 -0
- /package/rules/{C006_function_naming → common/C006_function_naming}/config.json +0 -0
- /package/rules/{C019_log_level_usage → common/C019_log_level_usage}/config.json +0 -0
- /package/rules/{C029_catch_block_logging → common/C029_catch_block_logging}/config.json +0 -0
- /package/rules/{C031_validation_separation → common/C031_validation_separation}/analyzer.js +0 -0
- /package/rules/{C031_validation_separation/README.md → docs/C031_validation_separation.md} +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: C047 – Logic retry không được viết lặp lại nhiều nơi
|
|
3
|
+
* Rule ID: custom/c047
|
|
4
|
+
* Purpose: Detect duplicate retry logic patterns and enforce centralized retry utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Logic retry không được viết lặp lại nhiều nơi - use centralized retry utility instead",
|
|
12
|
+
recommended: false
|
|
13
|
+
},
|
|
14
|
+
schema: [
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
maxRetryPatterns: {
|
|
19
|
+
type: "number",
|
|
20
|
+
minimum: 1,
|
|
21
|
+
default: 2,
|
|
22
|
+
description: "Maximum number of retry patterns allowed before suggesting centralization"
|
|
23
|
+
},
|
|
24
|
+
allowedRetryUtils: {
|
|
25
|
+
type: "array",
|
|
26
|
+
items: { type: "string" },
|
|
27
|
+
default: ["RetryUtil", "retryWithBackoff", "withRetry"],
|
|
28
|
+
description: "Names of allowed centralized retry utilities"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
messages: {
|
|
35
|
+
duplicateRetryLogic: "Duplicate retry logic detected ({{count}} occurrences). Consider using a centralized retry utility like RetryUtil.withRetry()",
|
|
36
|
+
inlineRetryLogic: "Inline retry logic found. Consider using a centralized retry utility for consistency and maintainability.",
|
|
37
|
+
suggestRetryUtil: "Use centralized retry utility instead of custom retry logic."
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {};
|
|
42
|
+
const maxRetryPatterns = options.maxRetryPatterns || 2;
|
|
43
|
+
const allowedRetryUtils = options.allowedRetryUtils || ["RetryUtil", "retryWithBackoff", "withRetry"];
|
|
44
|
+
|
|
45
|
+
const retryPatterns = [];
|
|
46
|
+
const sourceCode = context.getSourceCode();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a node represents a retry pattern
|
|
50
|
+
*/
|
|
51
|
+
function isRetryPattern(node) {
|
|
52
|
+
// Pattern 1: for/while loop with try-catch for retry
|
|
53
|
+
if ((node.type === "ForStatement" || node.type === "WhileStatement") &&
|
|
54
|
+
node.body && node.body.type === "BlockStatement") {
|
|
55
|
+
const hasRetryLogic = node.body.body.some(stmt =>
|
|
56
|
+
stmt.type === "TryStatement" ||
|
|
57
|
+
(stmt.type === "IfStatement" && hasRetryCondition(stmt))
|
|
58
|
+
);
|
|
59
|
+
return hasRetryLogic;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pattern 2: do-while with try-catch
|
|
63
|
+
if (node.type === "DoWhileStatement" &&
|
|
64
|
+
node.body && node.body.type === "BlockStatement") {
|
|
65
|
+
return node.body.body.some(stmt => stmt.type === "TryStatement");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pattern 3: recursive function with retry logic
|
|
69
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") {
|
|
70
|
+
return hasRecursiveRetryPattern(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if statement has retry-related conditions
|
|
78
|
+
*/
|
|
79
|
+
function hasRetryCondition(ifStmt) {
|
|
80
|
+
if (!ifStmt.test) return false;
|
|
81
|
+
|
|
82
|
+
const testText = sourceCode.getText(ifStmt.test).toLowerCase();
|
|
83
|
+
return testText.includes('retry') ||
|
|
84
|
+
testText.includes('attempt') ||
|
|
85
|
+
testText.includes('tries') ||
|
|
86
|
+
testText.includes('maxretries') ||
|
|
87
|
+
testText.includes('maxattempts');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if function has recursive retry pattern
|
|
92
|
+
*/
|
|
93
|
+
function hasRecursiveRetryPattern(funcNode) {
|
|
94
|
+
if (!funcNode.body || !funcNode.body.body) return false;
|
|
95
|
+
|
|
96
|
+
const funcName = funcNode.id ? funcNode.id.name : null;
|
|
97
|
+
if (!funcName) return false;
|
|
98
|
+
|
|
99
|
+
// Look for recursive calls with retry logic
|
|
100
|
+
const hasRecursiveCall = funcNode.body.body.some(stmt => {
|
|
101
|
+
if (stmt.type === "TryStatement" && stmt.handler) {
|
|
102
|
+
// Check if catch block has recursive call
|
|
103
|
+
return containsRecursiveCall(stmt.handler.body, funcName);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return hasRecursiveCall;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if block contains recursive call to the function
|
|
113
|
+
*/
|
|
114
|
+
function containsRecursiveCall(block, funcName) {
|
|
115
|
+
if (!block || !block.body) return false;
|
|
116
|
+
|
|
117
|
+
return block.body.some(stmt => {
|
|
118
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
119
|
+
return containsCallExpression(stmt.argument, funcName);
|
|
120
|
+
}
|
|
121
|
+
if (stmt.type === "ExpressionStatement") {
|
|
122
|
+
return containsCallExpression(stmt.expression, funcName);
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if expression contains call to specific function
|
|
130
|
+
*/
|
|
131
|
+
function containsCallExpression(expr, funcName) {
|
|
132
|
+
if (expr.type === "CallExpression" &&
|
|
133
|
+
expr.callee && expr.callee.name === funcName) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (expr.type === "AwaitExpression" && expr.argument) {
|
|
138
|
+
return containsCallExpression(expr.argument, funcName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if node uses allowed retry utilities
|
|
146
|
+
*/
|
|
147
|
+
function usesAllowedRetryUtil(node) {
|
|
148
|
+
const nodeText = sourceCode.getText(node);
|
|
149
|
+
return allowedRetryUtils.some(utilName => nodeText.includes(utilName));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get hash for retry pattern to detect duplicates
|
|
154
|
+
*/
|
|
155
|
+
function getRetryPatternHash(node) {
|
|
156
|
+
let text = sourceCode.getText(node);
|
|
157
|
+
// Normalize text for comparison (remove variable names, whitespace)
|
|
158
|
+
text = text
|
|
159
|
+
.replace(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g, 'VAR') // Replace identifiers
|
|
160
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
161
|
+
.replace(/\/\*.*?\*\//g, '') // Remove block comments
|
|
162
|
+
.replace(/\/\/.*$/gm, ''); // Remove line comments
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
// Check for retry patterns in various constructs
|
|
168
|
+
"ForStatement, WhileStatement, DoWhileStatement"(node) {
|
|
169
|
+
if (isRetryPattern(node) && !usesAllowedRetryUtil(node)) {
|
|
170
|
+
const hash = getRetryPatternHash(node);
|
|
171
|
+
const existing = retryPatterns.find(p => p.hash === hash);
|
|
172
|
+
|
|
173
|
+
if (existing) {
|
|
174
|
+
existing.count++;
|
|
175
|
+
existing.nodes.push(node);
|
|
176
|
+
} else {
|
|
177
|
+
retryPatterns.push({
|
|
178
|
+
hash,
|
|
179
|
+
count: 1,
|
|
180
|
+
nodes: [node],
|
|
181
|
+
type: node.type
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
"FunctionDeclaration, FunctionExpression"(node) {
|
|
188
|
+
if (isRetryPattern(node) && !usesAllowedRetryUtil(node)) {
|
|
189
|
+
context.report({
|
|
190
|
+
node,
|
|
191
|
+
messageId: "inlineRetryLogic"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Check for inline setTimeout/setInterval retry patterns
|
|
197
|
+
"CallExpression"(node) {
|
|
198
|
+
if (node.callee &&
|
|
199
|
+
(node.callee.name === "setTimeout" || node.callee.name === "setInterval")) {
|
|
200
|
+
const parent = node.parent;
|
|
201
|
+
// Check if this setTimeout is part of retry logic
|
|
202
|
+
if (parent && parent.type === "ExpressionStatement") {
|
|
203
|
+
let current = parent.parent;
|
|
204
|
+
while (current) {
|
|
205
|
+
if (current.type === "TryStatement" ||
|
|
206
|
+
(current.type === "IfStatement" && hasRetryCondition(current))) {
|
|
207
|
+
if (!usesAllowedRetryUtil(current)) {
|
|
208
|
+
context.report({
|
|
209
|
+
node: current,
|
|
210
|
+
messageId: "inlineRetryLogic"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
current = current.parent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// Report duplicates at end of program
|
|
222
|
+
"Program:exit"() {
|
|
223
|
+
retryPatterns.forEach(pattern => {
|
|
224
|
+
if (pattern.count >= maxRetryPatterns) {
|
|
225
|
+
pattern.nodes.forEach(node => {
|
|
226
|
+
context.report({
|
|
227
|
+
node,
|
|
228
|
+
messageId: "duplicateRetryLogic",
|
|
229
|
+
data: {
|
|
230
|
+
count: pattern.count
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: C072 – Each test should assert only one behavior (Single Assert Rule)
|
|
3
|
+
* Rule ID: custom/c072
|
|
4
|
+
* Purpose: A test case should only have one main behavior to test (one expect statement)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Each test should assert only one behavior (Single Assert Rule)",
|
|
12
|
+
recommended: false
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
tooMany: "Test contains too many expect statements ({{count}}). Each test should have only one main behavior to verify."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
function isTestFunction(node) {
|
|
21
|
+
// Safety check for node structure
|
|
22
|
+
if (!node || !node.callee) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for test/it calls
|
|
27
|
+
if (node.type === "CallExpression") {
|
|
28
|
+
// Handle direct test/it calls
|
|
29
|
+
if (node.callee.type === "Identifier") {
|
|
30
|
+
return ["test", "it"].includes(node.callee.name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle imported test/it calls
|
|
34
|
+
if (node.callee.type === "MemberExpression" &&
|
|
35
|
+
node.callee.object.type === "Identifier" &&
|
|
36
|
+
["test", "it"].includes(node.callee.property.name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isDescribeBlock(node) {
|
|
45
|
+
if (!node || !node.callee) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (node.type === "CallExpression") {
|
|
50
|
+
// Handle direct describe calls
|
|
51
|
+
if (node.callee.type === "Identifier") {
|
|
52
|
+
return node.callee.name === "describe";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle imported describe calls
|
|
56
|
+
if (node.callee.type === "MemberExpression" &&
|
|
57
|
+
node.callee.object.type === "Identifier" &&
|
|
58
|
+
node.callee.property.name === "describe") {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isSetupOrTeardown(node) {
|
|
67
|
+
if (!node || !node.callee) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (node.type === "CallExpression") {
|
|
72
|
+
const name = node.callee.type === "Identifier"
|
|
73
|
+
? node.callee.name
|
|
74
|
+
: node.callee.type === "MemberExpression"
|
|
75
|
+
? node.callee.property.name
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
return ["beforeEach", "afterEach", "beforeAll", "afterAll"].includes(name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function countExpectCalls(body) {
|
|
85
|
+
let count = 0;
|
|
86
|
+
|
|
87
|
+
function traverse(node) {
|
|
88
|
+
// Safety check to ensure node exists and has type property
|
|
89
|
+
if (!node || typeof node !== 'object' || !node.type) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if this node is an expect call
|
|
94
|
+
if (
|
|
95
|
+
node.type === "CallExpression" &&
|
|
96
|
+
node.callee &&
|
|
97
|
+
node.callee.type === "Identifier" &&
|
|
98
|
+
node.callee.name === "expect"
|
|
99
|
+
) {
|
|
100
|
+
count++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Safely traverse child nodes, but don't go into nested test functions
|
|
104
|
+
for (const key in node) {
|
|
105
|
+
if (key === 'parent' || key === 'range' || key === 'loc') {
|
|
106
|
+
continue; // Skip circular references and metadata
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const child = node[key];
|
|
110
|
+
if (Array.isArray(child)) {
|
|
111
|
+
child.forEach(item => {
|
|
112
|
+
if (item && typeof item === 'object' && item.type) {
|
|
113
|
+
// Don't traverse into nested test functions
|
|
114
|
+
if (!(item.type === "CallExpression" && isTestFunction(item))) {
|
|
115
|
+
traverse(item);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
120
|
+
// Don't traverse into nested test functions
|
|
121
|
+
if (!(child.type === "CallExpression" && isTestFunction(child))) {
|
|
122
|
+
traverse(child);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
traverse(body);
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
CallExpression(node) {
|
|
134
|
+
// Only check test/it function calls
|
|
135
|
+
if (!isTestFunction(node)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Ensure we have the required arguments (name and callback)
|
|
140
|
+
if (!node.arguments || node.arguments.length < 2) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const testCallback = node.arguments[1];
|
|
145
|
+
|
|
146
|
+
// Check if the second argument is a function (test body)
|
|
147
|
+
if (
|
|
148
|
+
!testCallback ||
|
|
149
|
+
(testCallback.type !== "FunctionExpression" &&
|
|
150
|
+
testCallback.type !== "ArrowFunctionExpression")
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get the function body
|
|
156
|
+
const fnBody = testCallback.body;
|
|
157
|
+
if (!fnBody) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle both block statements and expression bodies
|
|
162
|
+
let bodyToCheck = fnBody;
|
|
163
|
+
if (testCallback.type === "ArrowFunctionExpression" && fnBody.type !== "BlockStatement") {
|
|
164
|
+
// For arrow functions with expression bodies, wrap in a virtual block
|
|
165
|
+
bodyToCheck = { type: "BlockStatement", body: [{ type: "ExpressionStatement", expression: fnBody }] };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Count expect calls in the test body
|
|
169
|
+
const expectCount = countExpectCalls(bodyToCheck);
|
|
170
|
+
|
|
171
|
+
// Report if more than one expect statement
|
|
172
|
+
if (expectCount > 1) {
|
|
173
|
+
context.report({
|
|
174
|
+
node,
|
|
175
|
+
messageId: "tooMany",
|
|
176
|
+
data: {
|
|
177
|
+
count: expectCount
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: C075 – Functions must have explicit return type declarations
|
|
3
|
+
* Rule ID: sunlint/c075
|
|
4
|
+
* Purpose: Enforce explicit return type annotations for all functions to improve type safety
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Functions must have explicit return type declarations",
|
|
12
|
+
recommended: true,
|
|
13
|
+
category: "TypeScript"
|
|
14
|
+
},
|
|
15
|
+
schema: [
|
|
16
|
+
{
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
allowExpressions: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
default: false
|
|
22
|
+
},
|
|
23
|
+
allowTypedFunctionExpressions: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
default: true
|
|
26
|
+
},
|
|
27
|
+
allowHigherOrderFunctions: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
default: true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
messages: {
|
|
36
|
+
missingReturnType: "Function '{{name}}' is missing explicit return type annotation. Consider adding ': ReturnType'",
|
|
37
|
+
missingReturnTypeArrow: "Arrow function is missing explicit return type annotation. Consider adding ': ReturnType'",
|
|
38
|
+
missingReturnTypeMethod: "Method '{{name}}' is missing explicit return type annotation. Consider adding ': ReturnType'"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
create(context) {
|
|
42
|
+
const options = context.options[0] || {};
|
|
43
|
+
const allowExpressions = options.allowExpressions || false;
|
|
44
|
+
const allowTypedFunctionExpressions = options.allowTypedFunctionExpressions || true;
|
|
45
|
+
const allowHigherOrderFunctions = options.allowHigherOrderFunctions || true;
|
|
46
|
+
|
|
47
|
+
function isTypedFunctionExpression(node) {
|
|
48
|
+
const parent = node.parent;
|
|
49
|
+
if (!parent) return false;
|
|
50
|
+
|
|
51
|
+
// Variable declaration with type annotation
|
|
52
|
+
if (parent.type === "VariableDeclarator" && parent.id && parent.id.typeAnnotation) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Property with type annotation
|
|
57
|
+
if (parent.type === "Property" && parent.typeAnnotation) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Assignment to typed variable
|
|
62
|
+
if (parent.type === "AssignmentExpression" && parent.left && parent.left.typeAnnotation) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isHigherOrderFunction(node) {
|
|
70
|
+
// Check if function returns another function
|
|
71
|
+
if (node.body && node.body.type === "BlockStatement") {
|
|
72
|
+
// Simple heuristic: look for return statements that return functions
|
|
73
|
+
return node.body.body.some(stmt => {
|
|
74
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
75
|
+
return stmt.argument.type === "FunctionExpression" ||
|
|
76
|
+
stmt.argument.type === "ArrowFunctionExpression";
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Arrow function directly returning function
|
|
83
|
+
if (node.body &&
|
|
84
|
+
(node.body.type === "FunctionExpression" || node.body.type === "ArrowFunctionExpression")) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasReturnTypeAnnotation(node) {
|
|
92
|
+
return node.returnType !== null && node.returnType !== undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function checkFunction(node) {
|
|
96
|
+
// Skip if return type is already present
|
|
97
|
+
if (hasReturnTypeAnnotation(node)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Skip if this is a typed function expression and allowed
|
|
102
|
+
if (allowTypedFunctionExpressions && isTypedFunctionExpression(node)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Skip if this is a higher-order function and allowed
|
|
107
|
+
if (allowHigherOrderFunctions && isHigherOrderFunction(node)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Skip constructors
|
|
112
|
+
if (node.parent && node.parent.type === "MethodDefinition" && node.parent.kind === "constructor") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Skip getters/setters (they have implicit return types)
|
|
117
|
+
if (node.parent && node.parent.type === "MethodDefinition" &&
|
|
118
|
+
(node.parent.kind === "get" || node.parent.kind === "set")) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Get function name for better error messages
|
|
123
|
+
let functionName = "anonymous";
|
|
124
|
+
if (node.id && node.id.name) {
|
|
125
|
+
functionName = node.id.name;
|
|
126
|
+
} else if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id) {
|
|
127
|
+
functionName = node.parent.id.name;
|
|
128
|
+
} else if (node.parent && node.parent.type === "Property" && node.parent.key) {
|
|
129
|
+
functionName = node.parent.key.name || node.parent.key.value;
|
|
130
|
+
} else if (node.parent && node.parent.type === "MethodDefinition" && node.parent.key) {
|
|
131
|
+
functionName = node.parent.key.name;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Report the violation
|
|
135
|
+
const messageId = node.type === "ArrowFunctionExpression" ? "missingReturnTypeArrow" :
|
|
136
|
+
node.parent && node.parent.type === "MethodDefinition" ? "missingReturnTypeMethod" :
|
|
137
|
+
"missingReturnType";
|
|
138
|
+
|
|
139
|
+
context.report({
|
|
140
|
+
node,
|
|
141
|
+
messageId,
|
|
142
|
+
data: { name: functionName }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
FunctionDeclaration(node) {
|
|
148
|
+
checkFunction(node);
|
|
149
|
+
},
|
|
150
|
+
FunctionExpression(node) {
|
|
151
|
+
// Skip if expressions are allowed and this is a simple expression
|
|
152
|
+
if (allowExpressions && node.parent &&
|
|
153
|
+
(node.parent.type === "CallExpression" || node.parent.type === "ArrayExpression")) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
checkFunction(node);
|
|
157
|
+
},
|
|
158
|
+
ArrowFunctionExpression(node) {
|
|
159
|
+
// Skip if expressions are allowed and this is a simple expression
|
|
160
|
+
if (allowExpressions && node.parent &&
|
|
161
|
+
(node.parent.type === "CallExpression" || node.parent.type === "ArrayExpression")) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
checkFunction(node);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|