ok
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user