From 55547a7f3dff090ea97271e5f3d8fb40f3b188f6 Mon Sep 17 00:00:00 2001 From: Travis Vasceannie Date: Sun, 28 Sep 2025 22:02:24 +0000 Subject: [PATCH] ok --- hooks/code_quality_guard.py | 43 ++++++++++++++++++-- tests/hooks/test_pretooluse.py | 73 ++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/hooks/code_quality_guard.py b/hooks/code_quality_guard.py index 8457153..d209f67 100644 --- a/hooks/code_quality_guard.py +++ b/hooks/code_quality_guard.py @@ -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", ) diff --git a/tests/hooks/test_pretooluse.py b/tests/hooks/test_pretooluse.py index f07da80..f131674 100644 --- a/tests/hooks/test_pretooluse.py +++ b/tests/hooks/test_pretooluse.py @@ -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()