x
This commit is contained in:
0
hooks/__init__.py
Normal file
0
hooks/__init__.py
Normal file
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Unified quality hook for Claude Code supporting both PreToolUse and PostToolUse.
|
||||
|
||||
Prevents writing duplicate, complex, or non-modernized code and verifies quality
|
||||
@@ -872,7 +871,7 @@ def pretooluse_hook(hook_data: JsonObject, config: QualityConfig) -> JsonObject:
|
||||
any_usage_issues = _detect_any_usage(content)
|
||||
|
||||
try:
|
||||
has_issues, issues = _perform_quality_check(
|
||||
_has_issues, issues = _perform_quality_check(
|
||||
file_path,
|
||||
content,
|
||||
config,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Internal duplicate detection for analyzing code blocks within a single file.
|
||||
|
||||
Uses AST analysis and multiple similarity algorithms to detect redundant patterns.
|
||||
@@ -12,7 +11,6 @@ from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
COMMON_DUPLICATE_METHODS = {
|
||||
"__init__",
|
||||
"__enter__",
|
||||
@@ -212,7 +210,9 @@ class InternalDuplicateDetector:
|
||||
return
|
||||
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
block_type = "method" if isinstance(parent, ast.ClassDef) else "function"
|
||||
block_type = (
|
||||
"method" if isinstance(parent, ast.ClassDef) else "function"
|
||||
)
|
||||
if block := create_block(node, block_type, lines):
|
||||
blocks.append(block)
|
||||
|
||||
@@ -460,7 +460,9 @@ class InternalDuplicateDetector:
|
||||
return False
|
||||
|
||||
if all(block.name in COMMON_DUPLICATE_METHODS for block in group.blocks):
|
||||
max_lines = max(block.end_line - block.start_line + 1 for block in group.blocks)
|
||||
max_lines = max(
|
||||
block.end_line - block.start_line + 1 for block in group.blocks
|
||||
)
|
||||
max_complexity = max(block.complexity for block in group.blocks)
|
||||
|
||||
# Allow simple lifecycle dunder methods to repeat across classes.
|
||||
|
||||
@@ -79,7 +79,7 @@ select = [
|
||||
ignore = [
|
||||
"D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107",
|
||||
"S101", "B008", "PLR0913", "TRY003", "ANN204", "TID252", "RUF012",
|
||||
"PLC0415", "PTH123", "UP038", "PLW0603", "PLR0915", "PLR0912",
|
||||
"PLC0415", "PTH123", "PLW0603", "PLR0915", "PLR0912",
|
||||
"PLR0911", "C901", "PLR2004", "PLW1514", "SIM108", "SIM117"
|
||||
]
|
||||
fixable = ["ALL"]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Main CLI interface for code quality analysis."""
|
||||
|
||||
import ast
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""High-level complexity analysis interface."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -7,6 +8,8 @@ from ..config.schemas import ComplexityConfig
|
||||
from .metrics import ComplexityMetrics
|
||||
from .radon_integration import RadonComplexityAnalyzer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComplexityAnalyzer:
|
||||
"""High-level interface for code complexity analysis."""
|
||||
@@ -112,6 +115,12 @@ class ComplexityAnalyzer:
|
||||
)
|
||||
)
|
||||
if should_suppress:
|
||||
if self.exception_filter.config.debug:
|
||||
logger.debug(
|
||||
"Suppressed complexity issue for %s (reason: %s)",
|
||||
path,
|
||||
reason or "<unspecified>",
|
||||
)
|
||||
continue
|
||||
|
||||
summary = self.get_complexity_summary(metrics)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Exception handling system for quality analysis."""
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
@@ -12,6 +13,8 @@ from ..config.schemas import (
|
||||
QualityConfig,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExceptionFilter:
|
||||
"""Filters analysis results based on configured exception rules."""
|
||||
@@ -213,7 +216,14 @@ class ExceptionFilter:
|
||||
if not should_suppress:
|
||||
filtered_issues.append(issue)
|
||||
elif self.config.debug:
|
||||
pass
|
||||
logger.debug(
|
||||
"Suppressed %s issue %s at %s:%s (reason: %s)",
|
||||
analysis_type,
|
||||
issue_type or "unknown",
|
||||
file_path or "<unknown>",
|
||||
line_number or "<unknown>",
|
||||
reason or "<unspecified>",
|
||||
)
|
||||
|
||||
return filtered_issues
|
||||
|
||||
|
||||
@@ -307,6 +307,13 @@ class DuplicateDetectionEngine:
|
||||
block.content,
|
||||
)
|
||||
if should_suppress:
|
||||
if self.config.debug:
|
||||
logging.debug(
|
||||
"Suppressed duplicate match in %s at line %s (reason: %s)",
|
||||
block.file_path,
|
||||
block.start_line,
|
||||
reason or "<unspecified>",
|
||||
)
|
||||
should_suppress_match = True
|
||||
break
|
||||
|
||||
|
||||
@@ -68,7 +68,9 @@ class TestHelperFunctions:
|
||||
store_pre_state(test_path, test_content)
|
||||
|
||||
# Verify cache directory created
|
||||
mock_mkdir.assert_called_once_with(exist_ok=True)
|
||||
mock_mkdir.assert_called_once()
|
||||
_, mkdir_kwargs = mock_mkdir.call_args
|
||||
assert mkdir_kwargs.get("exist_ok") is True
|
||||
|
||||
# Verify state was written
|
||||
mock_write.assert_called_once()
|
||||
@@ -226,6 +228,9 @@ class TestHelperFunctions:
|
||||
duplicate_enabled=False,
|
||||
complexity_enabled=False,
|
||||
modernization_enabled=False,
|
||||
sourcery_enabled=False,
|
||||
basedpyright_enabled=False,
|
||||
pyrefly_enabled=False,
|
||||
)
|
||||
|
||||
with patch("code_quality_guard.detect_internal_duplicates") as mock_dup:
|
||||
|
||||
@@ -6,6 +6,8 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestHookIntegration:
|
||||
"""Test complete hook integration scenarios."""
|
||||
@@ -179,11 +181,9 @@ class TestHookIntegration:
|
||||
},
|
||||
},
|
||||
):
|
||||
try:
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main()
|
||||
raise AssertionError("Expected SystemExit")
|
||||
except SystemExit as exc:
|
||||
assert exc.code == 2
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
response = json.loads(mock_print.call_args[0][0])
|
||||
assert (
|
||||
@@ -223,7 +223,11 @@ class TestHookIntegration:
|
||||
"tool_name": "Write",
|
||||
"tool_input": {
|
||||
"file_path": str(temp_python_file),
|
||||
"content": "def func1(): pass\ndef func2(): pass\ndef func3(): pass",
|
||||
"content": (
|
||||
"def func1(): pass\n"
|
||||
"def func2(): pass\n"
|
||||
"def func3(): pass"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -315,11 +319,9 @@ class TestHookIntegration:
|
||||
},
|
||||
):
|
||||
if expected in {"deny", "ask"}:
|
||||
try:
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main()
|
||||
raise AssertionError("Expected SystemExit")
|
||||
except SystemExit as exc:
|
||||
assert exc.code == 2
|
||||
assert exc_info.value.code == 2
|
||||
else:
|
||||
main()
|
||||
|
||||
|
||||
@@ -149,7 +149,10 @@ class TestPostToolUseHook:
|
||||
with patch("pathlib.Path.read_text", return_value=clean_code):
|
||||
result = posttooluse_hook(hook_data, config)
|
||||
assert result["decision"] == "approve"
|
||||
assert "passed post-write verification" in result["systemMessage"].lower()
|
||||
assert (
|
||||
"passed post-write verification"
|
||||
in result["systemMessage"].lower()
|
||||
)
|
||||
|
||||
def test_no_message_when_success_disabled(self, clean_code):
|
||||
"""Test no message when show_success is disabled."""
|
||||
|
||||
@@ -356,7 +356,10 @@ class TestPreToolUseHook:
|
||||
},
|
||||
{
|
||||
"old_string": "pass",
|
||||
"new_string": "def handler(arg: Any) -> str:\n return str(arg)\n",
|
||||
"new_string": (
|
||||
"def handler(arg: Any) -> str:\n"
|
||||
" return str(arg)\n"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user