clean-code-tools 1.0.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/README.md +66 -0
- package/configs/eslint.clean-code.recommended.mjs +211 -0
- package/configs/python.clean-code.pyproject.toml +143 -0
- package/data/clean-code-patterns.jsonl +264 -0
- package/data/vector-record.schema.json +77 -0
- package/docs/README.md +29 -0
- package/docs/eslint-custom-rules.md +74 -0
- package/docs/eslint-recommended-config.md +87 -0
- package/docs/fastmcp-local-server.md +104 -0
- package/docs/publishing.md +125 -0
- package/docs/python-lint-recommended-config.md +57 -0
- package/docs/python-pylint-custom-rules.md +77 -0
- package/docs/semantic-weaviate.md +80 -0
- package/docs/static-trigger-semantic-review.md +97 -0
- package/evals/clean-code-retrieval.jsonl +13 -0
- package/ops/dev/weaviate/README.md +34 -0
- package/ops/dev/weaviate/compose.yaml +34 -0
- package/ops/dev/weaviate/smoke.sh +28 -0
- package/package.json +96 -0
- package/pyproject.toml +303 -0
- package/sample-apps/README.md +40 -0
- package/sample-apps/python-app/pyproject.toml +113 -0
- package/sample-apps/python-app/src/clean_pricing.py +10 -0
- package/sample-apps/python-app/src/smelly_pricing.py +8 -0
- package/sample-apps/ts-backend/eslint.config.mjs +3 -0
- package/sample-apps/ts-backend/package.json +18 -0
- package/sample-apps/ts-backend/src/clean-handler.ts +19 -0
- package/sample-apps/ts-backend/src/smelly-handler.ts +29 -0
- package/sample-apps/ts-backend/tsconfig.json +9 -0
- package/sample-apps/ts-frontend/eslint.config.mjs +3 -0
- package/sample-apps/ts-frontend/package.json +18 -0
- package/sample-apps/ts-frontend/src/CleanWidget.tsx +18 -0
- package/sample-apps/ts-frontend/src/SmellyWidget.tsx +27 -0
- package/sample-apps/ts-frontend/tsconfig.json +10 -0
- package/scripts/_mcp_app.py +21 -0
- package/scripts/check_clean_code_review_candidates.py +302 -0
- package/scripts/check_fastmcp_server.py +106 -0
- package/scripts/check_packages.py +137 -0
- package/scripts/check_python_config.py +130 -0
- package/scripts/check_repo_python_lint.py +46 -0
- package/scripts/check_retrieval_evals.py +132 -0
- package/scripts/check_sample_apps.py +169 -0
- package/scripts/check_semantic_search_tooling.py +102 -0
- package/scripts/clean_code_eslint_triggers.py +272 -0
- package/scripts/clean_code_mcp_server.py +7 -0
- package/scripts/clean_code_python_triggers.py +318 -0
- package/scripts/clean_code_review_candidates.py +291 -0
- package/scripts/clean_code_review_io.py +36 -0
- package/scripts/clean_code_review_models.py +43 -0
- package/scripts/clean_code_semantic.py +27 -0
- package/scripts/set_package_versions.py +82 -0
- package/scripts/weaviate_ingest_clean_code.py +44 -0
- package/scripts/weaviate_search_clean_code.py +51 -0
- package/skills/clean-code-mcp-reviewer/SKILL.md +209 -0
- package/skills/clean-code-mcp-reviewer/evals/evals.json +30 -0
- package/src/js/eslint-plugin-clean-code.mjs +758 -0
- package/src/python/clean_code_tools_pylint/__init__.py +14 -0
- package/src/python/clean_code_tools_pylint/ast_checker.py +122 -0
- package/src/python/clean_code_tools_pylint/comments.py +83 -0
- package/src/python/clean_code_tools_pylint/helpers.py +196 -0
- package/src/python/mcp_server/__init__.py +1 -0
- package/src/python/mcp_server/corpus.py +160 -0
- package/src/python/mcp_server/markdown.py +126 -0
- package/src/python/mcp_server/models.py +73 -0
- package/src/python/mcp_server/ranking.py +125 -0
- package/src/python/mcp_server/ranking_scoring.py +232 -0
- package/src/python/mcp_server/semantic.py +192 -0
- package/src/python/mcp_server/server.py +235 -0
- package/src/python/mcp_server/server_payloads.py +83 -0
- package/src/python/mcp_server/text.py +104 -0
- package/src/python/mcp_server/utils/__init__.py +1 -0
- package/src/python/mcp_server/utils/httpx_loader.py +14 -0
- package/src/python/mcp_server/utils/increment.py +7 -0
- package/src/python/mcp_server/utils/sha256_text.py +8 -0
- package/src/python/mcp_server/utils/unique_strings.py +15 -0
- package/src/python/mcp_server/weaviate.py +182 -0
- package/uv.lock +2012 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Sample Apps
|
|
2
|
+
|
|
3
|
+
These apps exercise the clean-code lint presets against realistic small examples.
|
|
4
|
+
|
|
5
|
+
## Python
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install "ruff>=0.15.0" "pylint>=4.0.0"
|
|
9
|
+
cd sample-apps/python-app
|
|
10
|
+
ruff check src/clean_pricing.py
|
|
11
|
+
pylint --rcfile=pyproject.toml src/clean_pricing.py
|
|
12
|
+
ruff check src/smelly_pricing.py
|
|
13
|
+
pylint --rcfile=pyproject.toml src/smelly_pricing.py
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The clean file should pass. The smelly file should report TODO tracking, commented-out code, unused arguments, and too many arguments.
|
|
17
|
+
|
|
18
|
+
## TypeScript Backend
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd sample-apps/ts-backend
|
|
22
|
+
bun install
|
|
23
|
+
bun run lint:clean
|
|
24
|
+
bun run lint:smelly
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The clean handler should pass. The smelly handler should report untracked TODOs, commented-out code, boolean flag arguments, policy literals, train-wreck navigation, and null usage.
|
|
28
|
+
|
|
29
|
+
## TypeScript Frontend
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd sample-apps/ts-frontend
|
|
33
|
+
bun install
|
|
34
|
+
bun run lint:clean
|
|
35
|
+
bun run lint:smelly
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The clean widget should pass. The smelly widget should report untracked TODOs, commented-out code, train-wreck navigation, policy literals, output argument mutation, null usage, and boolean flag arguments.
|
|
39
|
+
|
|
40
|
+
The TypeScript samples import the preset through `clean-code-tools/configs/eslint.clean-code.recommended.mjs`, so they exercise the package export path rather than a repository-relative config path.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[tool.ruff]
|
|
2
|
+
target-version = "py311"
|
|
3
|
+
line-length = 100
|
|
4
|
+
extend-exclude = [
|
|
5
|
+
".venv",
|
|
6
|
+
"__pycache__",
|
|
7
|
+
"build",
|
|
8
|
+
"dist",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[tool.ruff.lint]
|
|
12
|
+
select = [
|
|
13
|
+
"E",
|
|
14
|
+
"F",
|
|
15
|
+
"I",
|
|
16
|
+
"UP",
|
|
17
|
+
"B",
|
|
18
|
+
"C4",
|
|
19
|
+
"SIM",
|
|
20
|
+
"RET",
|
|
21
|
+
"ARG",
|
|
22
|
+
"ERA",
|
|
23
|
+
"TD",
|
|
24
|
+
"PLR2004",
|
|
25
|
+
"RUF",
|
|
26
|
+
]
|
|
27
|
+
ignore = [
|
|
28
|
+
"E501",
|
|
29
|
+
"TD001",
|
|
30
|
+
]
|
|
31
|
+
fixable = [
|
|
32
|
+
"E",
|
|
33
|
+
"F",
|
|
34
|
+
"I",
|
|
35
|
+
"UP",
|
|
36
|
+
"B",
|
|
37
|
+
"C4",
|
|
38
|
+
"SIM",
|
|
39
|
+
"RET",
|
|
40
|
+
"ARG",
|
|
41
|
+
"RUF",
|
|
42
|
+
]
|
|
43
|
+
unfixable = [
|
|
44
|
+
"ERA",
|
|
45
|
+
"TD",
|
|
46
|
+
"PLR2004",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint.per-file-ignores]
|
|
50
|
+
"tests/**/*.py" = [
|
|
51
|
+
"ARG001",
|
|
52
|
+
"ARG002",
|
|
53
|
+
"PLR2004",
|
|
54
|
+
"S101",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.pylint.main]
|
|
58
|
+
py-version = "3.11"
|
|
59
|
+
jobs = 0
|
|
60
|
+
recursive = true
|
|
61
|
+
load-plugins = [
|
|
62
|
+
"clean_code_tools_pylint",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pylint."messages control"]
|
|
66
|
+
disable = [
|
|
67
|
+
"all",
|
|
68
|
+
]
|
|
69
|
+
enable = [
|
|
70
|
+
"cyclic-import",
|
|
71
|
+
"clean-code-boolean-flag-argument",
|
|
72
|
+
"clean-code-business-policy-literal",
|
|
73
|
+
"clean-code-commented-out-code",
|
|
74
|
+
"clean-code-noisy-comment",
|
|
75
|
+
"clean-code-output-argument-mutation",
|
|
76
|
+
"clean-code-redundant-comment",
|
|
77
|
+
"clean-code-todo-format",
|
|
78
|
+
"clean-code-train-wreck",
|
|
79
|
+
"duplicate-code",
|
|
80
|
+
"import-error",
|
|
81
|
+
"too-many-ancestors",
|
|
82
|
+
"too-many-arguments",
|
|
83
|
+
"too-many-branches",
|
|
84
|
+
"too-many-instance-attributes",
|
|
85
|
+
"too-many-lines",
|
|
86
|
+
"too-many-locals",
|
|
87
|
+
"too-many-nested-blocks",
|
|
88
|
+
"too-many-public-methods",
|
|
89
|
+
"too-many-return-statements",
|
|
90
|
+
"too-many-statements",
|
|
91
|
+
"too-many-boolean-expressions",
|
|
92
|
+
"too-few-public-methods",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.pylint.design]
|
|
96
|
+
max-args = 5
|
|
97
|
+
max-attributes = 7
|
|
98
|
+
max-bool-expr = 5
|
|
99
|
+
max-branches = 12
|
|
100
|
+
max-locals = 15
|
|
101
|
+
max-nested-blocks = 4
|
|
102
|
+
max-public-methods = 12
|
|
103
|
+
max-returns = 6
|
|
104
|
+
max-statements = 40
|
|
105
|
+
max-parents = 7
|
|
106
|
+
min-public-methods = 1
|
|
107
|
+
|
|
108
|
+
[tool.pylint.format]
|
|
109
|
+
max-line-length = 100
|
|
110
|
+
max-module-lines = 300
|
|
111
|
+
|
|
112
|
+
[tool.pylint.reports]
|
|
113
|
+
score = false
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MAX_RETRY_ATTEMPTS = 5
|
|
2
|
+
PENDING_STATUS = "pending"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def can_retry_payment(status: str, failed_attempts: int) -> bool:
|
|
6
|
+
return status == PENDING_STATUS and failed_attempts < MAX_RETRY_ATTEMPTS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calculate_total(subtotal_cents: int, tax_cents: int) -> int:
|
|
10
|
+
return subtotal_cents + tax_cents
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sample-ts-backend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint:clean": "eslint src/clean-handler.ts --max-warnings 0",
|
|
7
|
+
"lint:smelly": "eslint src/smelly-handler.ts"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@eslint/js": "^10.0.0",
|
|
11
|
+
"clean-code-tools": "file:../..",
|
|
12
|
+
"eslint": "^10.4.0",
|
|
13
|
+
"eslint-plugin-sonarjs": "^4.0.0",
|
|
14
|
+
"eslint-plugin-unicorn": "^69.0.0",
|
|
15
|
+
"typescript": "^5.0.0",
|
|
16
|
+
"typescript-eslint": "^8.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const MAX_LOGIN_ATTEMPTS = 5;
|
|
2
|
+
const PAYMENT_FAILED_STATUS = "payment_failed";
|
|
3
|
+
|
|
4
|
+
interface Payment {
|
|
5
|
+
readonly failedAttempts: number;
|
|
6
|
+
readonly status: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function canRetryPayment(payment: Payment): boolean {
|
|
10
|
+
return payment.status === PAYMENT_FAILED_STATUS && payment.failedAttempts < MAX_LOGIN_ATTEMPTS;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function planPaymentRetry(payment: Payment): string {
|
|
14
|
+
if (canRetryPayment(payment)) {
|
|
15
|
+
return "retry";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return "skip";
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// TODO clean this up
|
|
2
|
+
// await publishReceipt(receipt);
|
|
3
|
+
|
|
4
|
+
type Payment = {
|
|
5
|
+
status: string;
|
|
6
|
+
customer: {
|
|
7
|
+
account: {
|
|
8
|
+
billing: {
|
|
9
|
+
email: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function sendPaymentEmail(payment: Payment, dryRun: boolean): null {
|
|
16
|
+
if (payment.status === "payment_failed") {
|
|
17
|
+
sendEmail(payment.customer.account.billing.email, true);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sendEmail(email: string, urgent: boolean): void {
|
|
24
|
+
if (urgent) {
|
|
25
|
+
console.log(email);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sendPaymentEmail({ status: "payment_failed", customer: { account: { billing: { email: "a@b.test" } } } }, false);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sample-ts-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint:clean": "eslint src/CleanWidget.tsx --max-warnings 0",
|
|
7
|
+
"lint:smelly": "eslint src/SmellyWidget.tsx"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@eslint/js": "^10.0.0",
|
|
11
|
+
"clean-code-tools": "file:../..",
|
|
12
|
+
"eslint": "^10.4.0",
|
|
13
|
+
"eslint-plugin-sonarjs": "^4.0.0",
|
|
14
|
+
"eslint-plugin-unicorn": "^69.0.0",
|
|
15
|
+
"typescript": "^5.0.0",
|
|
16
|
+
"typescript-eslint": "^8.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const MAX_VISIBLE_ITEMS = 5;
|
|
2
|
+
|
|
3
|
+
interface DashboardWidgetProps {
|
|
4
|
+
readonly items: readonly string[];
|
|
5
|
+
readonly shouldShowEmptyState: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function visibleItems(items: readonly string[]): readonly string[] {
|
|
9
|
+
return items.slice(0, MAX_VISIBLE_ITEMS);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DashboardWidget({ items, shouldShowEmptyState }: DashboardWidgetProps): string {
|
|
13
|
+
if (items.length === 0 && shouldShowEmptyState) {
|
|
14
|
+
return "No items";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return visibleItems(items).join(", ");
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// TODO fix widget
|
|
2
|
+
// return <OldWidget />;
|
|
3
|
+
|
|
4
|
+
type DashboardWidgetProps = {
|
|
5
|
+
items: string[];
|
|
6
|
+
user: {
|
|
7
|
+
account: {
|
|
8
|
+
plan: {
|
|
9
|
+
status: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function DashboardWidget(props: DashboardWidgetProps, compact: boolean): null {
|
|
16
|
+
if (props.user.account.plan.status === "active") {
|
|
17
|
+
props.items.push("bonus");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (compact) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
DashboardWidget({ items: [], user: { account: { plan: { status: "active" } } } }, true);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import importlib
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
PYTHON_SRC = ROOT / "src" / "python"
|
|
11
|
+
|
|
12
|
+
if str(PYTHON_SRC) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(PYTHON_SRC))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_semantic_module() -> ModuleType:
|
|
17
|
+
return importlib.import_module("mcp_server.semantic")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_server_module() -> ModuleType:
|
|
21
|
+
return importlib.import_module("mcp_server.server")
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from clean_code_eslint_triggers import ESLINT_TRIGGERS
|
|
9
|
+
from clean_code_python_triggers import PYLINT_TRIGGERS, RUFF_TRIGGERS
|
|
10
|
+
from clean_code_review_candidates import (
|
|
11
|
+
candidate_payload,
|
|
12
|
+
eslint_candidates,
|
|
13
|
+
markdown_payload,
|
|
14
|
+
merge_candidates,
|
|
15
|
+
pylint_candidates,
|
|
16
|
+
ruff_candidates,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
20
|
+
ESLINT_CONFIG = ROOT / "configs/eslint.clean-code.recommended.mjs"
|
|
21
|
+
PYTHON_CONFIG = ROOT / "configs/python.clean-code.pyproject.toml"
|
|
22
|
+
REPO_PYTHON_CONFIG = ROOT / "pyproject.toml"
|
|
23
|
+
|
|
24
|
+
EXPECTED_ESLINT_TRIGGERS = {
|
|
25
|
+
"@typescript-eslint/consistent-type-imports",
|
|
26
|
+
"@typescript-eslint/naming-convention",
|
|
27
|
+
"@typescript-eslint/no-confusing-void-expression",
|
|
28
|
+
"@typescript-eslint/no-magic-numbers",
|
|
29
|
+
"@typescript-eslint/no-unnecessary-condition",
|
|
30
|
+
"@typescript-eslint/no-unnecessary-type-assertion",
|
|
31
|
+
"@typescript-eslint/no-unused-vars",
|
|
32
|
+
"@typescript-eslint/prefer-nullish-coalescing",
|
|
33
|
+
"@typescript-eslint/strict-boolean-expressions",
|
|
34
|
+
"@typescript-eslint/switch-exhaustiveness-check",
|
|
35
|
+
"clean-code/no-boolean-flag-arguments",
|
|
36
|
+
"clean-code/no-business-policy-literals",
|
|
37
|
+
"clean-code/no-commented-out-code",
|
|
38
|
+
"clean-code/no-noisy-comments",
|
|
39
|
+
"clean-code/no-output-argument-mutation",
|
|
40
|
+
"clean-code/no-redundant-comment",
|
|
41
|
+
"clean-code/no-train-wrecks",
|
|
42
|
+
"clean-code/todo-format",
|
|
43
|
+
"complexity",
|
|
44
|
+
"max-depth",
|
|
45
|
+
"max-lines",
|
|
46
|
+
"max-lines-per-function",
|
|
47
|
+
"max-params",
|
|
48
|
+
"no-empty",
|
|
49
|
+
"no-negated-condition",
|
|
50
|
+
"no-nested-ternary",
|
|
51
|
+
"no-restricted-syntax",
|
|
52
|
+
"no-useless-return",
|
|
53
|
+
"sonarjs/cognitive-complexity",
|
|
54
|
+
"sonarjs/no-dead-store",
|
|
55
|
+
"sonarjs/no-duplicate-string",
|
|
56
|
+
"sonarjs/no-duplicated-branches",
|
|
57
|
+
"sonarjs/no-identical-conditions",
|
|
58
|
+
"sonarjs/no-identical-functions",
|
|
59
|
+
"sonarjs/no-inverted-boolean-check",
|
|
60
|
+
"unicorn/explicit-length-check",
|
|
61
|
+
"unicorn/no-negated-condition",
|
|
62
|
+
"unicorn/no-null",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ESLINT_SEMANTIC_EXCLUSIONS = {
|
|
66
|
+
"@typescript-eslint/no-floating-promises",
|
|
67
|
+
"@typescript-eslint/no-misused-promises",
|
|
68
|
+
"eqeqeq",
|
|
69
|
+
"spaced-comment",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
EXPECTED_PYLINT_TRIGGERS = {
|
|
73
|
+
"clean-code-boolean-flag-argument",
|
|
74
|
+
"clean-code-business-policy-literal",
|
|
75
|
+
"clean-code-commented-out-code",
|
|
76
|
+
"clean-code-noisy-comment",
|
|
77
|
+
"clean-code-output-argument-mutation",
|
|
78
|
+
"clean-code-redundant-comment",
|
|
79
|
+
"clean-code-todo-format",
|
|
80
|
+
"clean-code-train-wreck",
|
|
81
|
+
"cyclic-import",
|
|
82
|
+
"duplicate-code",
|
|
83
|
+
"too-few-public-methods",
|
|
84
|
+
"too-many-ancestors",
|
|
85
|
+
"too-many-arguments",
|
|
86
|
+
"too-many-boolean-expressions",
|
|
87
|
+
"too-many-branches",
|
|
88
|
+
"too-many-instance-attributes",
|
|
89
|
+
"too-many-lines",
|
|
90
|
+
"too-many-locals",
|
|
91
|
+
"too-many-nested-blocks",
|
|
92
|
+
"too-many-public-methods",
|
|
93
|
+
"too-many-return-statements",
|
|
94
|
+
"too-many-statements",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
PYLINT_SEMANTIC_EXCLUSIONS = {
|
|
98
|
+
"import-error",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
EXPECTED_RUFF_TRIGGERS = {
|
|
102
|
+
"ARG001",
|
|
103
|
+
"ARG002",
|
|
104
|
+
"ERA001",
|
|
105
|
+
"F401",
|
|
106
|
+
"F841",
|
|
107
|
+
"PLR0911",
|
|
108
|
+
"PLR0912",
|
|
109
|
+
"PLR0913",
|
|
110
|
+
"PLR0914",
|
|
111
|
+
"PLR0915",
|
|
112
|
+
"PLR0916",
|
|
113
|
+
"PLR1702",
|
|
114
|
+
"PLR2004",
|
|
115
|
+
"RET505",
|
|
116
|
+
"RET506",
|
|
117
|
+
"RET507",
|
|
118
|
+
"RET508",
|
|
119
|
+
"SIM102",
|
|
120
|
+
"SIM103",
|
|
121
|
+
"SIM108",
|
|
122
|
+
"TD002",
|
|
123
|
+
"TD003",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
RUFF_CURATED_SELECT_PREFIXES = {
|
|
127
|
+
"ARG",
|
|
128
|
+
"ERA",
|
|
129
|
+
"F",
|
|
130
|
+
"PLR2004",
|
|
131
|
+
"RET",
|
|
132
|
+
"SIM",
|
|
133
|
+
"TD",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def enabled_eslint_rules() -> set[str]:
|
|
138
|
+
source = ESLINT_CONFIG.read_text()
|
|
139
|
+
rule_pattern = re.compile(
|
|
140
|
+
r'^\s*(?:"(?P<quoted>[^"]+)"|(?P<bare>[A-Za-z][\w/-]*)):\s*'
|
|
141
|
+
r'(?:(?P<string>"(?:warn|error)")|\[\s*"(?P<array>warn|error)")',
|
|
142
|
+
re.MULTILINE,
|
|
143
|
+
)
|
|
144
|
+
return {
|
|
145
|
+
match.group("quoted") or match.group("bare")
|
|
146
|
+
for match in rule_pattern.finditer(source)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def python_lint_config(path: Path = PYTHON_CONFIG) -> dict:
|
|
151
|
+
return tomllib.loads(path.read_text())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def enabled_pylint_rules() -> set[str]:
|
|
155
|
+
return set(python_lint_config()["tool"]["pylint"]["messages control"]["enable"])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def selected_ruff_prefixes() -> set[str]:
|
|
159
|
+
return set(python_lint_config()["tool"]["ruff"]["lint"]["select"])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def repo_selected_ruff_prefixes() -> set[str]:
|
|
163
|
+
return set(python_lint_config(REPO_PYTHON_CONFIG)["tool"]["ruff"]["lint"]["select"])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def all_selected_ruff_prefixes() -> set[str]:
|
|
167
|
+
return selected_ruff_prefixes() | repo_selected_ruff_prefixes()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ruff_code_is_selected(code: str, selected_prefixes: set[str]) -> bool:
|
|
171
|
+
return any(code.startswith(prefix) for prefix in selected_prefixes)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main() -> None:
|
|
175
|
+
assert set(ESLINT_TRIGGERS) == EXPECTED_ESLINT_TRIGGERS
|
|
176
|
+
assert set(PYLINT_TRIGGERS) == EXPECTED_PYLINT_TRIGGERS
|
|
177
|
+
assert set(RUFF_TRIGGERS) == EXPECTED_RUFF_TRIGGERS
|
|
178
|
+
assert enabled_eslint_rules() <= set(ESLINT_TRIGGERS) | ESLINT_SEMANTIC_EXCLUSIONS
|
|
179
|
+
assert enabled_pylint_rules() <= set(PYLINT_TRIGGERS) | PYLINT_SEMANTIC_EXCLUSIONS
|
|
180
|
+
assert selected_ruff_prefixes() >= RUFF_CURATED_SELECT_PREFIXES
|
|
181
|
+
assert all(ruff_code_is_selected(code, all_selected_ruff_prefixes()) for code in RUFF_TRIGGERS)
|
|
182
|
+
|
|
183
|
+
eslint_results = [
|
|
184
|
+
{
|
|
185
|
+
"filePath": "/repo/src/pricing.ts",
|
|
186
|
+
"messages": [
|
|
187
|
+
{
|
|
188
|
+
"ruleId": "max-lines-per-function",
|
|
189
|
+
"message": "Function has too many lines.",
|
|
190
|
+
"line": 12,
|
|
191
|
+
"column": 1,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"ruleId": "clean-code/no-boolean-flag-arguments",
|
|
195
|
+
"message": "Boolean literal selects behavior.",
|
|
196
|
+
"line": 44,
|
|
197
|
+
"column": 22,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"ruleId": "semi",
|
|
201
|
+
"message": "Formatting-only rules are not semantic triggers.",
|
|
202
|
+
"line": 45,
|
|
203
|
+
"column": 1,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
pylint_results = [
|
|
209
|
+
{
|
|
210
|
+
"path": "sample-apps/python-app/src/smelly_pricing.py",
|
|
211
|
+
"obj": "calculate_total",
|
|
212
|
+
"line": 5,
|
|
213
|
+
"column": 0,
|
|
214
|
+
"symbol": "too-many-arguments",
|
|
215
|
+
"message": "Too many arguments (6/5)",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"path": "sample-apps/python-app/src/smelly_pricing.py",
|
|
219
|
+
"obj": "calculate_total",
|
|
220
|
+
"line": 7,
|
|
221
|
+
"column": 4,
|
|
222
|
+
"symbol": "clean-code-output-argument-mutation",
|
|
223
|
+
"message": "Avoid mutating parameter 'order' as an output argument.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"path": "sample-apps/python-app/src/smelly_pricing.py",
|
|
227
|
+
"obj": "calculate_total",
|
|
228
|
+
"line": 5,
|
|
229
|
+
"column": 0,
|
|
230
|
+
"symbol": "missing-function-docstring",
|
|
231
|
+
"message": "Not part of semantic trigger handoff.",
|
|
232
|
+
},
|
|
233
|
+
]
|
|
234
|
+
ruff_results = [
|
|
235
|
+
{
|
|
236
|
+
"filename": "sample-apps/python-app/src/smelly_pricing.py",
|
|
237
|
+
"location": {"row": 12, "column": 8},
|
|
238
|
+
"code": "PLR2004",
|
|
239
|
+
"message": "Magic value used in comparison.",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"filename": "sample-apps/python-app/src/smelly_pricing.py",
|
|
243
|
+
"location": {"row": 18, "column": 5},
|
|
244
|
+
"code": "TD003",
|
|
245
|
+
"message": "Missing issue link for this TODO.",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"filename": "sample-apps/python-app/src/smelly_pricing.py",
|
|
249
|
+
"location": {"row": 20, "column": 1},
|
|
250
|
+
"code": "E501",
|
|
251
|
+
"message": "Formatting-only rules are not semantic triggers.",
|
|
252
|
+
},
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
candidates = merge_candidates(
|
|
256
|
+
[
|
|
257
|
+
*eslint_candidates(eslint_results),
|
|
258
|
+
*pylint_candidates(pylint_results),
|
|
259
|
+
*ruff_candidates(ruff_results),
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
payload = candidate_payload(candidates)
|
|
263
|
+
|
|
264
|
+
assert payload["schema"] == "clean-code-review-candidates/v1"
|
|
265
|
+
assert payload["candidate_count"] == 6
|
|
266
|
+
|
|
267
|
+
assert {candidate["skill"] for candidate in payload["candidates"]} == {
|
|
268
|
+
"clean-code-mcp-reviewer"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
typescript_candidates = [
|
|
272
|
+
candidate for candidate in payload["candidates"] if candidate["language"] == "typescript"
|
|
273
|
+
]
|
|
274
|
+
python_candidates = [
|
|
275
|
+
candidate for candidate in payload["candidates"] if candidate["language"] == "python"
|
|
276
|
+
]
|
|
277
|
+
assert len(typescript_candidates) == 2
|
|
278
|
+
assert {candidate["anchor"] for candidate in typescript_candidates} == {"line 12", "line 44"}
|
|
279
|
+
assert any(candidate["symbol"] == "calculate_total" for candidate in python_candidates)
|
|
280
|
+
assert any("too many arguments" in candidate["mcp_queries"][0] for candidate in python_candidates)
|
|
281
|
+
assert any(
|
|
282
|
+
trigger["tool"] == "pylint" and trigger["rule"] == "clean-code-output-argument-mutation"
|
|
283
|
+
for candidate in python_candidates
|
|
284
|
+
for trigger in candidate["triggers"]
|
|
285
|
+
)
|
|
286
|
+
assert any(
|
|
287
|
+
trigger["tool"] == "ruff" and trigger["rule"] == "TD003"
|
|
288
|
+
for candidate in python_candidates
|
|
289
|
+
for trigger in candidate["triggers"]
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
markdown = markdown_payload(candidates)
|
|
293
|
+
assert "Clean-Code Semantic Review Candidates" in markdown
|
|
294
|
+
assert "clean-code/no-boolean-flag-arguments" in markdown
|
|
295
|
+
assert "calculate_total" in markdown
|
|
296
|
+
assert "ruff/PLR2004" in markdown
|
|
297
|
+
|
|
298
|
+
print("clean_code_review_candidates_check=ok")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
main()
|