This commit is contained in:
2025-09-28 22:02:24 +00:00
parent d3e4174545
commit 55547a7f3d
2 changed files with 112 additions and 4 deletions

View File

@@ -12,10 +12,12 @@ import os
import re
import subprocess
import sys
import tokenize
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
from datetime import UTC, datetime
from io import StringIO
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TypedDict, cast
@@ -731,6 +733,37 @@ def _detect_any_usage(content: str) -> list[str]:
]
def _detect_type_ignore_usage(content: str) -> list[str]:
"""Detect forbidden # type: ignore usage in proposed content."""
pattern = re.compile(r"#\s*type:\s*ignore(?:\b|\[)", re.IGNORECASE)
lines_with_type_ignore: set[int] = set()
try:
for token_type, token_string, start, _, _ in tokenize.generate_tokens(
StringIO(content).readline,
):
if token_type == tokenize.COMMENT and pattern.search(token_string):
lines_with_type_ignore.add(start[0])
except tokenize.TokenError:
for index, line in enumerate(content.splitlines(), start=1):
if pattern.search(line):
lines_with_type_ignore.add(index)
if not lines_with_type_ignore:
return []
sorted_lines = sorted(lines_with_type_ignore)
display_lines = ", ".join(str(num) for num in sorted_lines[:5])
if len(sorted_lines) > 5:
display_lines += ", …"
return [
"⚠️ Forbidden # type: ignore usage at line(s) "
f"{display_lines}; remove the suppression and fix typing issues instead",
]
def _perform_quality_check(
file_path: str,
content: str,
@@ -877,6 +910,8 @@ def pretooluse_hook(hook_data: JsonObject, config: QualityConfig) -> JsonObject:
enable_type_checks = tool_name == "Write"
any_usage_issues = _detect_any_usage(content)
type_ignore_issues = _detect_type_ignore_usage(content)
precheck_issues = any_usage_issues + type_ignore_issues
try:
_has_issues, issues = _perform_quality_check(
@@ -886,12 +921,12 @@ def pretooluse_hook(hook_data: JsonObject, config: QualityConfig) -> JsonObject:
enable_type_checks=enable_type_checks,
)
all_issues = any_usage_issues + issues
all_issues = precheck_issues + issues
if not all_issues:
return _create_hook_response("PreToolUse", "allow")
if any_usage_issues:
if precheck_issues:
return _handle_quality_issues(
file_path,
all_issues,
@@ -901,10 +936,10 @@ def pretooluse_hook(hook_data: JsonObject, config: QualityConfig) -> JsonObject:
return _handle_quality_issues(file_path, all_issues, config)
except Exception as e: # noqa: BLE001
if any_usage_issues:
if precheck_issues:
return _handle_quality_issues(
file_path,
any_usage_issues,
precheck_issues,
config,
forced_permission="deny",
)

View File

@@ -439,3 +439,76 @@ class TestPreToolUseHook:
result = pretooluse_hook(hook_data, config)
assert result["permissionDecision"] == "deny"
assert "any" in result["reason"].lower()
def test_type_ignore_usage_denied_on_analysis_failure(self):
config = QualityConfig()
hook_data = {
"tool_name": "Write",
"tool_input": {
"file_path": "example.py",
"content": (
"def sample() -> None:\n"
" value = unknown # type: ignore[attr-defined]\n"
),
},
}
with patch(
"code_quality_guard._perform_quality_check",
side_effect=RuntimeError("boom"),
):
result = pretooluse_hook(hook_data, config)
assert result["permissionDecision"] == "deny"
assert "type: ignore" in result["reason"].lower()
assert "fix these issues" in result["reason"].lower()
def test_type_ignore_usage_denied(self):
config = QualityConfig(enforcement_mode="strict")
hook_data = {
"tool_name": "Write",
"tool_input": {
"file_path": "example.py",
"content": (
"def example() -> None:\n" " value = unknown # type: ignore\n"
),
},
}
with patch("code_quality_guard.analyze_code_quality") as mock_analyze:
mock_analyze.return_value = {}
result = pretooluse_hook(hook_data, config)
assert result["permissionDecision"] == "deny"
assert "type: ignore" in result["reason"].lower()
def test_type_ignore_usage_detected_in_multiedit(self):
config = QualityConfig()
hook_data = {
"tool_name": "MultiEdit",
"tool_input": {
"file_path": "example.py",
"edits": [
{
"old_string": "pass",
"new_string": (
"def helper() -> None:\n" " pass # type: ignore\n"
),
},
{
"old_string": "pass",
"new_string": (
"def handler() -> None:\n"
" value = unknown # type: ignore[attr-defined]\n"
),
},
],
},
}
with patch("code_quality_guard.analyze_code_quality") as mock_analyze:
mock_analyze.return_value = {}
result = pretooluse_hook(hook_data, config)
assert result["permissionDecision"] == "deny"
assert "type: ignore" in result["reason"].lower()