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.
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,

View File

@@ -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.

View File

@@ -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"]

View File

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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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"
),
},
],
},