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.
|
"""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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user