This commit is contained in:
2025-09-17 17:01:02 +00:00
parent f1b61a6ae7
commit 0649677e4d
12 changed files with 60 additions and 21 deletions

0
hooks/__init__.py Normal file
View File

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
"""Unified quality hook for Claude Code supporting both PreToolUse and PostToolUse. """Unified quality hook for Claude Code supporting both PreToolUse and PostToolUse.
Prevents writing duplicate, complex, or non-modernized code and verifies quality 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) any_usage_issues = _detect_any_usage(content)
try: try:
has_issues, issues = _perform_quality_check( _has_issues, issues = _perform_quality_check(
file_path, file_path,
content, content,
config, config,

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
"""Internal duplicate detection for analyzing code blocks within a single file. """Internal duplicate detection for analyzing code blocks within a single file.
Uses AST analysis and multiple similarity algorithms to detect redundant patterns. Uses AST analysis and multiple similarity algorithms to detect redundant patterns.
@@ -12,7 +11,6 @@ from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
COMMON_DUPLICATE_METHODS = { COMMON_DUPLICATE_METHODS = {
"__init__", "__init__",
"__enter__", "__enter__",
@@ -212,7 +210,9 @@ class InternalDuplicateDetector:
return return
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): 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): if block := create_block(node, block_type, lines):
blocks.append(block) blocks.append(block)
@@ -460,7 +460,9 @@ class InternalDuplicateDetector:
return False return False
if all(block.name in COMMON_DUPLICATE_METHODS for block in group.blocks): 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) max_complexity = max(block.complexity for block in group.blocks)
# Allow simple lifecycle dunder methods to repeat across classes. # Allow simple lifecycle dunder methods to repeat across classes.

View File

@@ -79,7 +79,7 @@ select = [
ignore = [ ignore = [
"D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107",
"S101", "B008", "PLR0913", "TRY003", "ANN204", "TID252", "RUF012", "S101", "B008", "PLR0913", "TRY003", "ANN204", "TID252", "RUF012",
"PLC0415", "PTH123", "UP038", "PLW0603", "PLR0915", "PLR0912", "PLC0415", "PTH123", "PLW0603", "PLR0915", "PLR0912",
"PLR0911", "C901", "PLR2004", "PLW1514", "SIM108", "SIM117" "PLR0911", "C901", "PLR2004", "PLW1514", "SIM108", "SIM117"
] ]
fixable = ["ALL"] fixable = ["ALL"]

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
"""Main CLI interface for code quality analysis.""" """Main CLI interface for code quality analysis."""
import ast import ast

View File

@@ -1,5 +1,6 @@
"""High-level complexity analysis interface.""" """High-level complexity analysis interface."""
import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -7,6 +8,8 @@ from ..config.schemas import ComplexityConfig
from .metrics import ComplexityMetrics from .metrics import ComplexityMetrics
from .radon_integration import RadonComplexityAnalyzer from .radon_integration import RadonComplexityAnalyzer
logger = logging.getLogger(__name__)
class ComplexityAnalyzer: class ComplexityAnalyzer:
"""High-level interface for code complexity analysis.""" """High-level interface for code complexity analysis."""
@@ -112,6 +115,12 @@ class ComplexityAnalyzer:
) )
) )
if should_suppress: if should_suppress:
if self.exception_filter.config.debug:
logger.debug(
"Suppressed complexity issue for %s (reason: %s)",
path,
reason or "<unspecified>",
)
continue continue
summary = self.get_complexity_summary(metrics) summary = self.get_complexity_summary(metrics)

View File

@@ -1,6 +1,7 @@
"""Exception handling system for quality analysis.""" """Exception handling system for quality analysis."""
import fnmatch import fnmatch
import logging
import re import re
from collections.abc import Callable from collections.abc import Callable
from datetime import UTC, datetime from datetime import UTC, datetime
@@ -12,6 +13,8 @@ from ..config.schemas import (
QualityConfig, QualityConfig,
) )
logger = logging.getLogger(__name__)
class ExceptionFilter: class ExceptionFilter:
"""Filters analysis results based on configured exception rules.""" """Filters analysis results based on configured exception rules."""
@@ -213,7 +216,14 @@ class ExceptionFilter:
if not should_suppress: if not should_suppress:
filtered_issues.append(issue) filtered_issues.append(issue)
elif self.config.debug: 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 return filtered_issues

View File

@@ -307,6 +307,13 @@ class DuplicateDetectionEngine:
block.content, block.content,
) )
if should_suppress: 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 should_suppress_match = True
break break

View File

@@ -68,7 +68,9 @@ class TestHelperFunctions:
store_pre_state(test_path, test_content) store_pre_state(test_path, test_content)
# Verify cache directory created # 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 # Verify state was written
mock_write.assert_called_once() mock_write.assert_called_once()
@@ -226,6 +228,9 @@ class TestHelperFunctions:
duplicate_enabled=False, duplicate_enabled=False,
complexity_enabled=False, complexity_enabled=False,
modernization_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: with patch("code_quality_guard.detect_internal_duplicates") as mock_dup:

View File

@@ -6,6 +6,8 @@ import tempfile
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest
class TestHookIntegration: class TestHookIntegration:
"""Test complete hook integration scenarios.""" """Test complete hook integration scenarios."""
@@ -179,11 +181,9 @@ class TestHookIntegration:
}, },
}, },
): ):
try: with pytest.raises(SystemExit) as exc_info:
main() main()
raise AssertionError("Expected SystemExit") assert exc_info.value.code == 2
except SystemExit as exc:
assert exc.code == 2
response = json.loads(mock_print.call_args[0][0]) response = json.loads(mock_print.call_args[0][0])
assert ( assert (
@@ -223,7 +223,11 @@ class TestHookIntegration:
"tool_name": "Write", "tool_name": "Write",
"tool_input": { "tool_input": {
"file_path": str(temp_python_file), "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"}: if expected in {"deny", "ask"}:
try: with pytest.raises(SystemExit) as exc_info:
main() main()
raise AssertionError("Expected SystemExit") assert exc_info.value.code == 2
except SystemExit as exc:
assert exc.code == 2
else: else:
main() main()

View File

@@ -149,7 +149,10 @@ class TestPostToolUseHook:
with patch("pathlib.Path.read_text", return_value=clean_code): with patch("pathlib.Path.read_text", return_value=clean_code):
result = posttooluse_hook(hook_data, config) result = posttooluse_hook(hook_data, config)
assert result["decision"] == "approve" 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): def test_no_message_when_success_disabled(self, clean_code):
"""Test no message when show_success is disabled.""" """Test no message when show_success is disabled."""

View File

@@ -356,7 +356,10 @@ class TestPreToolUseHook:
}, },
{ {
"old_string": "pass", "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"
),
}, },
], ],
}, },