@sun-asterisk/sunlint 1.0.5
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 +202 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/cli-legacy.js +355 -0
- package/cli.js +35 -0
- package/config/default.json +22 -0
- package/config/presets/beginner.json +36 -0
- package/config/presets/ci.json +46 -0
- package/config/presets/recommended.json +24 -0
- package/config/presets/strict.json +32 -0
- package/config/rules-registry.json +681 -0
- package/config/sunlint-schema.json +166 -0
- package/config/typescript/custom-rules-new.js +0 -0
- package/config/typescript/custom-rules.js +9 -0
- package/config/typescript/eslint.config.js +110 -0
- package/config/typescript/package-lock.json +1585 -0
- package/config/typescript/package.json +13 -0
- package/config/typescript/security-rules/index.js +90 -0
- package/config/typescript/security-rules/s005-no-origin-auth.js +95 -0
- package/config/typescript/security-rules/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/config/typescript/security-rules/s008-crypto-agility.js +62 -0
- package/config/typescript/security-rules/s009-no-insecure-crypto.js +103 -0
- package/config/typescript/security-rules/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/config/typescript/security-rules/s011-no-insecure-uuid.js +66 -0
- package/config/typescript/security-rules/s012-hardcode-secret.js +71 -0
- package/config/typescript/security-rules/s014-insecure-tls-version.js +50 -0
- package/config/typescript/security-rules/s015-insecure-tls-certificate.js +43 -0
- package/config/typescript/security-rules/s016-sensitive-query-parameter.js +59 -0
- package/config/typescript/security-rules/s017-no-sql-injection.js +193 -0
- package/config/typescript/security-rules/s018-positive-input-validation.js +56 -0
- package/config/typescript/security-rules/s019-no-raw-user-input-in-email.js +113 -0
- package/config/typescript/security-rules/s020-no-eval-dynamic-execution.js +89 -0
- package/config/typescript/security-rules/s022-output-encoding.js +78 -0
- package/config/typescript/security-rules/s023-no-json-injection.js +300 -0
- package/config/typescript/security-rules/s025-server-side-input-validation.js +217 -0
- package/config/typescript/security-rules/s026-json-schema-validation.js +68 -0
- package/config/typescript/security-rules/s027-no-hardcoded-secrets.js +80 -0
- package/config/typescript/security-rules/s029-require-csrf-protection.js +79 -0
- package/config/typescript/security-rules/s030-no-directory-browsing.js +78 -0
- package/config/typescript/security-rules/s033-require-samesite-cookie.js +80 -0
- package/config/typescript/security-rules/s034-require-host-cookie-prefix.js +77 -0
- package/config/typescript/security-rules/s035-cookie-specific-path.js +74 -0
- package/config/typescript/security-rules/s036-no-unsafe-file-include.js +68 -0
- package/config/typescript/security-rules/s037-require-anti-cache-headers.js +70 -0
- package/config/typescript/security-rules/s038-no-version-disclosure.js +74 -0
- package/config/typescript/security-rules/s039-no-session-token-in-url.js +63 -0
- package/config/typescript/security-rules/s041-require-session-invalidate-on-logout.js +211 -0
- package/config/typescript/security-rules/s042-require-periodic-reauthentication.js +294 -0
- package/config/typescript/security-rules/s043-terminate-sessions-on-password-change.js +254 -0
- package/config/typescript/security-rules/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/config/typescript/security-rules/s045-anti-automation-controls.js +46 -0
- package/config/typescript/security-rules/s046-secure-notification-on-auth-change.js +44 -0
- package/config/typescript/security-rules/s048-password-credential-recovery.js +54 -0
- package/config/typescript/security-rules/s050-session-token-weak-hash.js +94 -0
- package/config/typescript/security-rules/s052-secure-random-authentication-code.js +66 -0
- package/config/typescript/security-rules/s054-verification-default-account.js +109 -0
- package/config/typescript/security-rules/s057-utc-logging.js +54 -0
- package/config/typescript/security-rules/s058-no-ssrf.js +73 -0
- package/config/typescript/test-s005-working.ts +22 -0
- package/config/typescript/tsconfig.json +29 -0
- package/core/ai-analyzer.js +169 -0
- package/core/analysis-orchestrator.js +705 -0
- package/core/cli-action-handler.js +230 -0
- package/core/cli-program.js +106 -0
- package/core/config-manager.js +396 -0
- package/core/config-merger.js +136 -0
- package/core/config-override-processor.js +74 -0
- package/core/config-preset-resolver.js +65 -0
- package/core/config-source-loader.js +152 -0
- package/core/config-validator.js +126 -0
- package/core/dependency-manager.js +105 -0
- package/core/eslint-engine-service.js +312 -0
- package/core/eslint-instance-manager.js +104 -0
- package/core/eslint-integration-service.js +363 -0
- package/core/git-utils.js +170 -0
- package/core/multi-rule-runner.js +239 -0
- package/core/output-service.js +250 -0
- package/core/report-generator.js +320 -0
- package/core/rule-mapping-service.js +309 -0
- package/core/rule-selection-service.js +121 -0
- package/core/sunlint-engine-service.js +23 -0
- package/core/typescript-analyzer.js +262 -0
- package/core/typescript-engine.js +313 -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/DEBUG.md +86 -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/HEURISTIC_VS_AI.md +113 -0
- package/docs/README.md +32 -0
- package/docs/RELEASE_GUIDE.md +230 -0
- package/docs/RULE-RESPONSIBILITY-MATRIX.md +204 -0
- package/eslint-integration/.eslintrc.js +98 -0
- package/eslint-integration/cli.js +35 -0
- package/eslint-integration/eslint-plugin-custom/c002-no-duplicate-code.js +204 -0
- package/eslint-integration/eslint-plugin-custom/c003-no-vague-abbreviations.js +246 -0
- package/eslint-integration/eslint-plugin-custom/c006-function-name-verb-noun.js +207 -0
- package/eslint-integration/eslint-plugin-custom/c010-limit-block-nesting.js +90 -0
- package/eslint-integration/eslint-plugin-custom/c013-no-dead-code.js +43 -0
- package/eslint-integration/eslint-plugin-custom/c014-abstract-dependency-preferred.js +38 -0
- package/eslint-integration/eslint-plugin-custom/c017-limit-constructor-logic.js +39 -0
- package/eslint-integration/eslint-plugin-custom/c018-no-generic-throw.js +335 -0
- package/eslint-integration/eslint-plugin-custom/c023-no-duplicate-variable-name-in-scope.js +142 -0
- package/eslint-integration/eslint-plugin-custom/c027-limit-function-nesting.js +50 -0
- package/eslint-integration/eslint-plugin-custom/c029-catch-block-logging.js +80 -0
- package/eslint-integration/eslint-plugin-custom/c030-use-custom-error-classes.js +294 -0
- package/eslint-integration/eslint-plugin-custom/c034-no-implicit-return.js +34 -0
- package/eslint-integration/eslint-plugin-custom/c035-no-empty-catch.js +32 -0
- package/eslint-integration/eslint-plugin-custom/c041-no-config-inline.js +64 -0
- package/eslint-integration/eslint-plugin-custom/c042-boolean-name-prefix.js +406 -0
- package/eslint-integration/eslint-plugin-custom/c043-no-console-or-print.js +300 -0
- package/eslint-integration/eslint-plugin-custom/c047-no-duplicate-retry-logic.js +239 -0
- package/eslint-integration/eslint-plugin-custom/c048-no-var-declaration.js +31 -0
- package/eslint-integration/eslint-plugin-custom/c076-one-assert-per-test.js +184 -0
- package/eslint-integration/eslint-plugin-custom/index.js +155 -0
- package/eslint-integration/eslint-plugin-custom/package.json +13 -0
- package/eslint-integration/eslint-plugin-custom/package.json.bak +9 -0
- package/eslint-integration/eslint-plugin-custom/s003-no-unvalidated-redirect.js +86 -0
- package/eslint-integration/eslint-plugin-custom/s005-no-origin-auth.js +95 -0
- package/eslint-integration/eslint-plugin-custom/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/eslint-integration/eslint-plugin-custom/s008-crypto-agility.js +62 -0
- package/eslint-integration/eslint-plugin-custom/s009-no-insecure-crypto.js +103 -0
- package/eslint-integration/eslint-plugin-custom/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/eslint-integration/eslint-plugin-custom/s011-no-insecure-uuid.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s012-hardcode-secret.js +71 -0
- package/eslint-integration/eslint-plugin-custom/s014-insecure-tls-version.js +50 -0
- package/eslint-integration/eslint-plugin-custom/s015-insecure-tls-certificate.js +43 -0
- package/eslint-integration/eslint-plugin-custom/s016-sensitive-query-parameter.js +59 -0
- package/eslint-integration/eslint-plugin-custom/s017-no-sql-injection.js +193 -0
- package/eslint-integration/eslint-plugin-custom/s018-positive-input-validation.js +56 -0
- package/eslint-integration/eslint-plugin-custom/s019-no-raw-user-input-in-email.js +113 -0
- package/eslint-integration/eslint-plugin-custom/s020-no-eval-dynamic-execution.js +89 -0
- package/eslint-integration/eslint-plugin-custom/s022-output-encoding.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s023-no-json-injection.js +300 -0
- package/eslint-integration/eslint-plugin-custom/s025-server-side-input-validation.js +217 -0
- package/eslint-integration/eslint-plugin-custom/s026-json-schema-validation.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s027-no-hardcoded-secrets.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s029-require-csrf-protection.js +79 -0
- package/eslint-integration/eslint-plugin-custom/s030-no-directory-browsing.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s033-require-samesite-cookie.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s034-require-host-cookie-prefix.js +77 -0
- package/eslint-integration/eslint-plugin-custom/s035-cookie-specific-path.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s036-no-unsafe-file-include.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s037-require-anti-cache-headers.js +70 -0
- package/eslint-integration/eslint-plugin-custom/s038-no-version-disclosure.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s039-no-session-token-in-url.js +63 -0
- package/eslint-integration/eslint-plugin-custom/s041-require-session-invalidate-on-logout.js +211 -0
- package/eslint-integration/eslint-plugin-custom/s042-require-periodic-reauthentication.js +294 -0
- package/eslint-integration/eslint-plugin-custom/s043-terminate-sessions-on-password-change.js +254 -0
- package/eslint-integration/eslint-plugin-custom/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/eslint-integration/eslint-plugin-custom/s045-anti-automation-controls.js +46 -0
- package/eslint-integration/eslint-plugin-custom/s046-secure-notification-on-auth-change.js +44 -0
- package/eslint-integration/eslint-plugin-custom/s047-secure-random-passwords.js +108 -0
- package/eslint-integration/eslint-plugin-custom/s048-password-credential-recovery.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s050-session-token-weak-hash.js +94 -0
- package/eslint-integration/eslint-plugin-custom/s052-secure-random-authentication-code.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s054-verification-default-account.js +109 -0
- package/eslint-integration/eslint-plugin-custom/s055-verification-rest-check-the-incoming-content-type.js +143 -0
- package/eslint-integration/eslint-plugin-custom/s057-utc-logging.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s058-no-ssrf.js +73 -0
- package/eslint-integration/eslint-plugin-custom/t002-interface-prefix-i.js +42 -0
- package/eslint-integration/eslint-plugin-custom/t003-ts-ignore-reason.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t004-interface-public-only.js +160 -0
- package/eslint-integration/eslint-plugin-custom/t007-no-fn-in-constructor.js +52 -0
- package/eslint-integration/eslint-plugin-custom/t011-no-real-time-dependency.js +175 -0
- package/eslint-integration/eslint-plugin-custom/t019-no-empty-type.js +95 -0
- package/eslint-integration/eslint-plugin-custom/t025-no-nested-union-tuple.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t026-limit-nested-generics.js +377 -0
- package/eslint-integration/eslint.config.js +125 -0
- package/eslint-integration/eslint.config.simple.js +24 -0
- package/eslint-integration/node_modules/eslint-plugin-custom/package.json +0 -0
- package/eslint-integration/package.json +23 -0
- package/eslint-integration/sample.ts +53 -0
- package/eslint-integration/test-s003.js +5 -0
- package/eslint-integration/tsconfig.json +27 -0
- package/examples/.github/workflows/code-quality.yml +111 -0
- package/examples/.sunlint.json +42 -0
- package/examples/README.md +47 -0
- package/examples/package.json +33 -0
- package/package.json +100 -0
- package/rules/C006_function_naming/analyzer.js +338 -0
- package/rules/C006_function_naming/config.json +86 -0
- package/rules/C019_log_level_usage/analyzer.js +359 -0
- package/rules/C019_log_level_usage/config.json +121 -0
- package/rules/C029_catch_block_logging/analyzer.js +339 -0
- package/rules/C029_catch_block_logging/config.json +59 -0
- package/rules/C031_validation_separation/README.md +72 -0
- package/rules/C031_validation_separation/analyzer.js +186 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T002 – Interface names should start with 'I'
|
|
3
|
+
* Rule ID: custom/t002
|
|
4
|
+
* Purpose: Enforce consistent interface naming convention by requiring the 'I' prefix
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Interface names should start with 'I'",
|
|
14
|
+
recommended: false
|
|
15
|
+
},
|
|
16
|
+
fixable: "code",
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
interfacePrefix: "Interface name '{{name}}' should start with 'I'"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
TSInterfaceDeclaration(node) {
|
|
26
|
+
const interfaceName = node.id.name;
|
|
27
|
+
if (!interfaceName.startsWith("I")) {
|
|
28
|
+
context.report({
|
|
29
|
+
node: node.id,
|
|
30
|
+
messageId: "interfacePrefix",
|
|
31
|
+
data: {
|
|
32
|
+
name: interfaceName,
|
|
33
|
+
},
|
|
34
|
+
fix(fixer) {
|
|
35
|
+
return fixer.replaceText(node.id, `I${interfaceName}`);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T003 – Avoid using @ts-ignore without a justification
|
|
3
|
+
* Rule ID: custom/t003
|
|
4
|
+
* Purpose: Ensure @ts-ignore comments include a reason to maintain code quality and documentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Avoid using @ts-ignore without a justification",
|
|
14
|
+
recommended: false
|
|
15
|
+
},
|
|
16
|
+
fixable: null,
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
tsIgnoreReason: "Please provide a reason for using @ts-ignore"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
Program() {
|
|
26
|
+
const sourceCode = context.getSourceCode();
|
|
27
|
+
const comments = sourceCode.getAllComments();
|
|
28
|
+
|
|
29
|
+
comments.forEach((comment) => {
|
|
30
|
+
if (comment.type === "Line" && comment.value.includes("@ts-ignore")) {
|
|
31
|
+
// Check if there's a justification after @ts-ignore
|
|
32
|
+
const hasJustification = comment.value
|
|
33
|
+
.replace("@ts-ignore", "")
|
|
34
|
+
.trim()
|
|
35
|
+
.length > 0;
|
|
36
|
+
|
|
37
|
+
if (!hasJustification) {
|
|
38
|
+
context.report({
|
|
39
|
+
node: comment,
|
|
40
|
+
messageId: "tsIgnoreReason",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T004 – Interface chỉ nên chứa public method
|
|
3
|
+
* Rule ID: custom/t004
|
|
4
|
+
* Purpose: Interface là hợp đồng, không được có logic private/internal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Interface chỉ nên chứa public method",
|
|
14
|
+
category: "TypeScript",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
fixable: null,
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
interfacePrivateMethod: "Interface không được chứa private method '{{methodName}}'. Interface là hợp đồng public, hãy chuyển logic private sang class implementation.",
|
|
21
|
+
interfaceProtectedMethod: "Interface không được chứa protected method '{{methodName}}'. Interface là hợp đồng public, hãy chuyển logic protected sang class implementation.",
|
|
22
|
+
interfacePrivateProperty: "Interface không được chứa private property '{{propertyName}}'. Interface là hợp đồng public, hãy chuyển property private sang class implementation.",
|
|
23
|
+
interfaceProtectedProperty: "Interface không được chứa protected property '{{propertyName}}'. Interface là hợp đồng public, hãy chuyển property protected sang class implementation.",
|
|
24
|
+
interfaceImplementation: "Interface không được chứa implementation logic trong method '{{methodName}}'. Interface chỉ định nghĩa hợp đồng, không có implementation.",
|
|
25
|
+
interfacePrivateNamingMethod: "Interface method '{{methodName}}' có tên gợi ý private (bắt đầu với _). Interface chỉ nên chứa public contract, hãy remove hoặc chuyển sang class implementation.",
|
|
26
|
+
interfacePrivateNamingProperty: "Interface property '{{propertyName}}' có tên gợi ý private (bắt đầu với _). Interface chỉ nên chứa public contract, hãy remove hoặc chuyển sang class implementation.",
|
|
27
|
+
interfaceImplementationDetails: "Interface method '{{methodName}}' leak implementation details. Interface nên abstract, không expose chi tiết implementation như SQL, cache, connection handling.",
|
|
28
|
+
interfaceInternalState: "Interface property '{{propertyName}}' expose internal state management. Interface nên chỉ define public contract, không expose internal resources.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
// Helper function to check if a name suggests private/internal usage
|
|
34
|
+
function isPrivateNaming(name) {
|
|
35
|
+
return name && (name.startsWith('_') || name.startsWith('__'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper function to check if method name suggests implementation details
|
|
39
|
+
function isImplementationDetail(name) {
|
|
40
|
+
const implementationKeywords = [
|
|
41
|
+
'buildSql', 'executeRaw', 'handleConnection', 'logQuery', 'log',
|
|
42
|
+
'buildQuery', 'executeQuery', 'handleRetry', 'handleError',
|
|
43
|
+
'internal', 'cache', 'pool', 'connection', 'retry', 'performance'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
return name && implementationKeywords.some(keyword =>
|
|
47
|
+
name.toLowerCase().includes(keyword.toLowerCase())
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper function to check if property suggests internal state
|
|
52
|
+
function isInternalState(name) {
|
|
53
|
+
const internalStateKeywords = [
|
|
54
|
+
'pool', 'cache', 'connection', 'internal', 'secret', 'config',
|
|
55
|
+
'state', 'buffer', 'queue', 'stack', 'heap'
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return name && internalStateKeywords.some(keyword =>
|
|
59
|
+
name.toLowerCase().includes(keyword.toLowerCase())
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
TSInterfaceDeclaration(node) {
|
|
65
|
+
// Kiểm tra từng member trong interface
|
|
66
|
+
node.body.body.forEach(member => {
|
|
67
|
+
// Kiểm tra method signature
|
|
68
|
+
if (member.type === 'TSMethodSignature') {
|
|
69
|
+
const methodName = member.key.name || member.key.value;
|
|
70
|
+
|
|
71
|
+
// Kiểm tra accessibility modifiers (for completeness, though TS doesn't allow this)
|
|
72
|
+
if (member.accessibility === 'private') {
|
|
73
|
+
context.report({
|
|
74
|
+
node: member,
|
|
75
|
+
messageId: "interfacePrivateMethod",
|
|
76
|
+
data: { methodName }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (member.accessibility === 'protected') {
|
|
81
|
+
context.report({
|
|
82
|
+
node: member,
|
|
83
|
+
messageId: "interfaceProtectedMethod",
|
|
84
|
+
data: { methodName }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Kiểm tra tên method có gợi ý private không
|
|
89
|
+
if (isPrivateNaming(methodName)) {
|
|
90
|
+
context.report({
|
|
91
|
+
node: member,
|
|
92
|
+
messageId: "interfacePrivateNamingMethod",
|
|
93
|
+
data: { methodName }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Kiểm tra method có leak implementation details không
|
|
98
|
+
if (isImplementationDetail(methodName)) {
|
|
99
|
+
context.report({
|
|
100
|
+
node: member,
|
|
101
|
+
messageId: "interfaceImplementationDetails",
|
|
102
|
+
data: { methodName }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Kiểm tra property signature
|
|
108
|
+
if (member.type === 'TSPropertySignature') {
|
|
109
|
+
const propertyName = member.key.name || member.key.value;
|
|
110
|
+
|
|
111
|
+
// Kiểm tra accessibility modifiers
|
|
112
|
+
if (member.accessibility === 'private') {
|
|
113
|
+
context.report({
|
|
114
|
+
node: member,
|
|
115
|
+
messageId: "interfacePrivateProperty",
|
|
116
|
+
data: { propertyName }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (member.accessibility === 'protected') {
|
|
121
|
+
context.report({
|
|
122
|
+
node: member,
|
|
123
|
+
messageId: "interfaceProtectedProperty",
|
|
124
|
+
data: { propertyName }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Kiểm tra tên property có gợi ý private không
|
|
129
|
+
if (isPrivateNaming(propertyName)) {
|
|
130
|
+
context.report({
|
|
131
|
+
node: member,
|
|
132
|
+
messageId: "interfacePrivateNamingProperty",
|
|
133
|
+
data: { propertyName }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Kiểm tra property có expose internal state không
|
|
138
|
+
if (isInternalState(propertyName)) {
|
|
139
|
+
context.report({
|
|
140
|
+
node: member,
|
|
141
|
+
messageId: "interfaceInternalState",
|
|
142
|
+
data: { propertyName }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Kiểm tra nếu có implementation trong interface (thường không xảy ra trong TS, nhưng để đảm bảo)
|
|
148
|
+
if (member.type === 'MethodDefinition' && member.value && member.value.body) {
|
|
149
|
+
const methodName = member.key.name || member.key.value;
|
|
150
|
+
context.report({
|
|
151
|
+
node: member,
|
|
152
|
+
messageId: "interfaceImplementation",
|
|
153
|
+
data: { methodName }
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T007 – Avoid declaring functions inside constructors or class bodies
|
|
3
|
+
* Rule ID: custom/t007
|
|
4
|
+
* Purpose: Prevent function declarations inside constructors and class bodies to maintain clean code structure
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Avoid declaring functions inside constructors or class bodies",
|
|
12
|
+
recommended: false
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
noFunctionInConstructor: "Avoid declaring functions inside class constructors.",
|
|
17
|
+
noFunctionInClassBody: "Avoid declaring nested functions inside class body."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
MethodDefinition(node) {
|
|
23
|
+
if (node.kind === "constructor" && node.value && node.value.body && node.value.body.body) {
|
|
24
|
+
const constructorBody = node.value.body.body;
|
|
25
|
+
constructorBody.forEach(element => {
|
|
26
|
+
if (element.type === "FunctionDeclaration" || element.type === "FunctionExpression") {
|
|
27
|
+
context.report({
|
|
28
|
+
node: element,
|
|
29
|
+
messageId: "noFunctionInConstructor"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
ClassBody(node) {
|
|
36
|
+
node.body.forEach(element => {
|
|
37
|
+
if (element.type === "MethodDefinition" && element.value && element.value.body && element.value.body.body) {
|
|
38
|
+
const methodBody = element.value.body.body;
|
|
39
|
+
methodBody.forEach(subNode => {
|
|
40
|
+
if (subNode.type === "FunctionDeclaration") {
|
|
41
|
+
context.report({
|
|
42
|
+
node: subNode,
|
|
43
|
+
messageId: "noFunctionInClassBody"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T011 – Test không nên phụ thuộc vào thời gian thực
|
|
3
|
+
* Rule ID: custom/t011
|
|
4
|
+
* Purpose: Detect real-time dependencies in tests like setTimeout, delay, sleep, etc.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Test không nên phụ thuộc vào thời gian thực (No real-time dependencies in tests)",
|
|
12
|
+
isRecommended: true
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
noRealTimeDelay: "Test should not depend on real-time delays. Use mocks, stubs, or deterministic approaches instead of '{{method}}'.",
|
|
17
|
+
noDateNow: "Test should not depend on current timestamp. Use mock time or inject clock instead of 'Date.now()' or 'new Date()'.",
|
|
18
|
+
noSetTimeout: "Test should not use setTimeout/setInterval for delays. Use mock timers or deterministic test patterns.",
|
|
19
|
+
noThreadSleep: "Test should not use Thread.sleep() or similar blocking delays. Use mock objects or callbacks."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
|
|
24
|
+
const TEST_FUNCTIONS = ["test", "it", "beforeEach", "beforeAll", "afterEach", "afterAll"];
|
|
25
|
+
const DESCRIBE_FUNCTIONS = ["describe", "context", "suite"];
|
|
26
|
+
const PROBLEMATIC_TIMERS = ["setTimeout", "setInterval", "setImmediate", "delay", "sleep", "wait", "pause"];
|
|
27
|
+
|
|
28
|
+
// Check if current context is inside a test function
|
|
29
|
+
function isInTestContext(node) {
|
|
30
|
+
let parent = node.parent;
|
|
31
|
+
while (parent) {
|
|
32
|
+
if (parent.type === "CallExpression" && isTestOrDescribeFunction(parent)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
parent = parent.parent;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTestOrDescribeFunction(node) {
|
|
41
|
+
if (!node || !node.callee) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (node.type === "CallExpression") {
|
|
46
|
+
const allTestFunctions = [...TEST_FUNCTIONS, ...DESCRIBE_FUNCTIONS];
|
|
47
|
+
|
|
48
|
+
// Handle direct calls
|
|
49
|
+
if (node.callee.type === "Identifier") {
|
|
50
|
+
return allTestFunctions.includes(node.callee.name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle member expression calls like test.skip, describe.only
|
|
54
|
+
if (node.callee.type === "MemberExpression" &&
|
|
55
|
+
node.callee.object.type === "Identifier") {
|
|
56
|
+
const baseName = node.callee.object.name;
|
|
57
|
+
return allTestFunctions.includes(baseName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkProblematicMethod(node, methodName, messageId = "noSetTimeout") {
|
|
65
|
+
if (PROBLEMATIC_TIMERS.includes(methodName)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
messageId,
|
|
69
|
+
data: { method: methodName }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
// Detect setTimeout, setInterval calls
|
|
76
|
+
CallExpression(node) {
|
|
77
|
+
if (!isInTestContext(node)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const callee = node.callee;
|
|
82
|
+
|
|
83
|
+
// Direct function calls
|
|
84
|
+
if (callee.type === "Identifier") {
|
|
85
|
+
checkProblematicMethod(node, callee.name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Member expression calls
|
|
89
|
+
if (callee.type === "MemberExpression" && callee.property && callee.property.type === "Identifier") {
|
|
90
|
+
checkProblematicMethod(node, callee.property.name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Promise-based delays
|
|
94
|
+
if (callee.type === "Identifier" && callee.name === "Promise" &&
|
|
95
|
+
node.arguments.length > 0 && node.arguments[0].type === "ArrowFunctionExpression") {
|
|
96
|
+
const promiseBody = node.arguments[0].body;
|
|
97
|
+
if (promiseBody && promiseBody.type === "CallExpression" &&
|
|
98
|
+
promiseBody.callee && promiseBody.callee.name === "setTimeout") {
|
|
99
|
+
context.report({
|
|
100
|
+
node,
|
|
101
|
+
messageId: "noRealTimeDelay",
|
|
102
|
+
data: { method: "Promise + setTimeout" }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Detect Date.now(), new Date() without parameters
|
|
109
|
+
NewExpression(node) {
|
|
110
|
+
if (!isInTestContext(node)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (node.callee.type === "Identifier" && node.callee.name === "Date" && node.arguments.length === 0) {
|
|
115
|
+
context.report({
|
|
116
|
+
node,
|
|
117
|
+
messageId: "noDateNow"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Detect Date.now() calls
|
|
123
|
+
MemberExpression(node) {
|
|
124
|
+
if (!isInTestContext(node)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (node.object.type === "Identifier" &&
|
|
129
|
+
node.object.name === "Date" &&
|
|
130
|
+
node.property.type === "Identifier" &&
|
|
131
|
+
node.property.name === "now") {
|
|
132
|
+
context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: "noDateNow"
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Detect await expressions with timing-based patterns
|
|
140
|
+
AwaitExpression(node) {
|
|
141
|
+
if (!isInTestContext(node)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const argument = node.argument;
|
|
146
|
+
|
|
147
|
+
// await new Promise(resolve => setTimeout(...))
|
|
148
|
+
if (argument.type === "NewExpression" &&
|
|
149
|
+
argument.callee.type === "Identifier" &&
|
|
150
|
+
argument.callee.name === "Promise") {
|
|
151
|
+
context.report({
|
|
152
|
+
node,
|
|
153
|
+
messageId: "noRealTimeDelay",
|
|
154
|
+
data: { method: "await Promise + delay" }
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// await sleep(), await delay(), etc.
|
|
159
|
+
if (argument.type === "CallExpression" &&
|
|
160
|
+
argument.callee.type === "Identifier") {
|
|
161
|
+
const methodName = argument.callee.name;
|
|
162
|
+
const problematicAsyncMethods = ["sleep", "delay", "wait", "pause"];
|
|
163
|
+
|
|
164
|
+
if (problematicAsyncMethods.includes(methodName)) {
|
|
165
|
+
context.report({
|
|
166
|
+
node,
|
|
167
|
+
messageId: "noRealTimeDelay",
|
|
168
|
+
data: { method: `await ${methodName}` }
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T019 – Disallow empty type definitions
|
|
3
|
+
* Rule ID: custom/t019
|
|
4
|
+
* Purpose: Prevent empty type definitions and suggest more specific types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Disallow empty type definitions",
|
|
14
|
+
category: "TypeScript",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
fixable: "code",
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
emptyType: "Empty type definition is not allowed. Add at least one property or use a more specific type.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
// Helper function to get a more specific type suggestion based on the name
|
|
26
|
+
function getSuggestedType(name) {
|
|
27
|
+
const lowerName = name.toLowerCase();
|
|
28
|
+
|
|
29
|
+
// Suggest more specific types based on common naming patterns
|
|
30
|
+
if (lowerName.includes("config") || lowerName.includes("options")) {
|
|
31
|
+
return "Record<string, unknown>";
|
|
32
|
+
}
|
|
33
|
+
if (lowerName.includes("props")) {
|
|
34
|
+
return "Record<string, React.ReactNode>";
|
|
35
|
+
}
|
|
36
|
+
if (lowerName.includes("state")) {
|
|
37
|
+
return "Record<string, unknown>";
|
|
38
|
+
}
|
|
39
|
+
if (lowerName.includes("params")) {
|
|
40
|
+
return "Record<string, string | number | boolean>";
|
|
41
|
+
}
|
|
42
|
+
if (lowerName.includes("result") || lowerName.includes("response")) {
|
|
43
|
+
return "Record<string, unknown>";
|
|
44
|
+
}
|
|
45
|
+
if (lowerName.includes("event") || lowerName.includes("handler")) {
|
|
46
|
+
return "(...args: unknown[]) => void";
|
|
47
|
+
}
|
|
48
|
+
if (lowerName.includes("callback") || lowerName.includes("fn")) {
|
|
49
|
+
return "(...args: unknown[]) => unknown";
|
|
50
|
+
}
|
|
51
|
+
if (lowerName.includes("data") || lowerName.includes("item")) {
|
|
52
|
+
return "Record<string, unknown>";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default suggestion
|
|
56
|
+
return "Record<string, unknown>";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
TSInterfaceDeclaration(node) {
|
|
61
|
+
if (node.body.body.length === 0) {
|
|
62
|
+
const suggestedType = getSuggestedType(node.id.name);
|
|
63
|
+
context.report({
|
|
64
|
+
node: node.body,
|
|
65
|
+
messageId: "emptyType",
|
|
66
|
+
fix(fixer) {
|
|
67
|
+
return fixer.replaceText(
|
|
68
|
+
node,
|
|
69
|
+
`type ${node.id.name} = ${suggestedType};`
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
TSTypeAliasDeclaration(node) {
|
|
76
|
+
if (
|
|
77
|
+
node.typeAnnotation.type === "TSTypeLiteral" &&
|
|
78
|
+
node.typeAnnotation.members.length === 0
|
|
79
|
+
) {
|
|
80
|
+
const suggestedType = getSuggestedType(node.id.name);
|
|
81
|
+
context.report({
|
|
82
|
+
node: node.typeAnnotation,
|
|
83
|
+
messageId: "emptyType",
|
|
84
|
+
fix(fixer) {
|
|
85
|
+
return fixer.replaceText(
|
|
86
|
+
node.typeAnnotation,
|
|
87
|
+
suggestedType
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: T025 – Avoid deeply nested union or tuple types
|
|
3
|
+
* Rule ID: custom/t025
|
|
4
|
+
* Purpose: Prevent complex nested union or tuple types to maintain type readability and simplicity
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Avoid deeply nested union or tuple types",
|
|
12
|
+
recommended: false
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
nestedUnion: "Avoid nested union types.",
|
|
17
|
+
nestedTuple: "Avoid nested tuple types."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
function checkNestedTypes(node) {
|
|
22
|
+
if (node.type === 'TSUnionType') {
|
|
23
|
+
const nested = node.types.some(t => t.type === 'TSUnionType' || t.type === 'TSTupleType');
|
|
24
|
+
if (nested) {
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: "nestedUnion"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node.type === 'TSTupleType') {
|
|
33
|
+
const nested = node.elementTypes.some(t => t.type === 'TSTupleType' || t.type === 'TSUnionType');
|
|
34
|
+
if (nested) {
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
messageId: "nestedTuple"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
TSUnionType: checkNestedTypes,
|
|
45
|
+
TSTupleType: checkNestedTypes
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|