项目文件夹

文件
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

245 行
7.8 KiB
Python
可执行文件

#!/usr/bin/env python3
"""
Pre-commit hook to flag unsafe cleanup patterns.
Check 1 — Silent close() methods:
Detects `def close(self)` methods that contain bare `except Exception: pass`
or `except: pass` — cleanup failures should be logged via safe_close() from
utilities.resource_utils, or with explicit logger calls.
Check 2 — Bare .close() in finally/except blocks:
Detects `obj.close()` calls inside finally/except blocks that are not wrapped
in safe_close(). If .close() raises, it masks the original exception.
"""
import ast
import sys
# Files/patterns that are excluded from this check
ALLOWED_PATTERNS = {
"tests/",
"test_",
"_test.py",
# LLM wrapper close() methods delegate to _close_base_llm which has
# selective httpx-client introspection logic — not a simple .close() call.
"config/llm_config.py",
"rate_limiting/llm/wrapper.py",
}
class SilentCleanupChecker(ast.NodeVisitor):
"""AST visitor to detect silent exceptions in close() methods."""
def __init__(self, filename: str):
self.filename = filename
self.errors = []
def visit_FunctionDef(self, node):
# Only inspect `def close(self, ...):` methods
if node.name != "close":
self.generic_visit(node)
return
# Walk the close() body looking for try/except
for child in ast.walk(node):
if not isinstance(child, ast.ExceptHandler):
continue
# Check if the handler is "except Exception:" or bare "except:"
if child.type is not None and not (
isinstance(child.type, ast.Name)
and child.type.id == "Exception"
):
continue
# Check if handler body is *only* `pass` (no logging, no raise)
if self._is_silent_handler(child):
self.errors.append(
(
child.lineno,
"Silent exception in close() method — use "
"safe_close() from utilities.resource_utils "
"or add explicit logging so cleanup failures "
"are visible.",
)
)
self.generic_visit(node)
@staticmethod
def _is_silent_handler(handler: ast.ExceptHandler) -> bool:
"""Return True if handler body is only `pass` with no logging/raise."""
for stmt in handler.body:
# pass statement — continue checking
if isinstance(stmt, ast.Pass):
continue
# raise — not silent
if isinstance(stmt, ast.Raise):
return False
# Any expression that looks like logger.something(...)
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
func = stmt.value.func
if isinstance(func, ast.Attribute) and isinstance(
func.value, ast.Name
):
if func.value.id == "logger":
return False
# Any other statement means it's not just `pass`
return False
# If we got here, every statement was `pass`
return True
def _is_file_allowed(self) -> bool:
for pattern in ALLOWED_PATTERNS:
if pattern in self.filename:
return True
return False
class BareCloseInFinallyChecker(ast.NodeVisitor):
"""AST visitor to detect bare .close() calls in finally/except blocks.
A bare `obj.close()` in a finally or except block can mask the original
exception if .close() itself raises. Use safe_close() instead.
"""
def __init__(self, filename: str):
self.filename = filename
self.errors = []
def visit_Try(self, node):
# Check finally body
for stmt in node.finalbody:
self._check_block(stmt)
# Check except handlers
for handler in node.handlers:
for stmt in handler.body:
self._check_block(stmt)
self.generic_visit(node)
def _check_block(self, node):
"""Check a single statement for bare .close() calls.
Recurses into if/elif/else bodies but stops at nested try blocks
(which would provide their own exception protection).
"""
# Direct .close() call as a statement
if self._is_bare_close(node):
self.errors.append(
(
node.lineno,
"Bare .close() in finally/except block — if "
".close() raises, it masks the original exception. "
"Use safe_close() from utilities.resource_utils.",
)
)
return
# Recurse into if/elif/else bodies (common pattern: if x is not None: x.close())
if isinstance(node, ast.If):
for stmt in node.body:
self._check_block(stmt)
for stmt in node.orelse:
self._check_block(stmt)
# Names that are not resource handles — .close() on these is safe
_SAFE_CLOSE_NAMES = {"plt", "figure", "fig", "ax"}
@classmethod
def _is_bare_close(cls, node) -> bool:
"""Return True if node is an expression statement calling .close()."""
if not isinstance(node, ast.Expr):
return False
if not isinstance(node.value, ast.Call):
return False
call = node.value
if not (
isinstance(call.func, ast.Attribute)
and call.func.attr == "close"
and not call.args
and not call.keywords
):
return False
# Skip known-safe names (e.g. plt.close(), cursor.close())
if isinstance(call.func.value, ast.Name):
if call.func.value.id in cls._SAFE_CLOSE_NAMES:
return False
return True
def check_file(filename: str) -> bool:
"""Check a single Python file for silent cleanup patterns."""
if not filename.endswith(".py"):
return True
# Skip allowed files
for pattern in ALLOWED_PATTERNS:
if pattern in filename:
return True
try:
with open(filename, "r", encoding="utf-8") as f:
content = f.read()
except Exception as e:
print(f"Error reading {filename}: {e}")
return False
try:
tree = ast.parse(content, filename=filename)
checker = SilentCleanupChecker(filename)
checker.visit(tree)
bare_checker = BareCloseInFinallyChecker(filename)
bare_checker.visit(tree)
all_errors = checker.errors + bare_checker.errors
if all_errors:
print(f"\n{filename}:")
for line_num, error in sorted(all_errors):
print(f" Line {line_num}: {error}")
return False
except SyntaxError:
pass
except Exception as e:
print(f"Error parsing {filename}: {e}")
return False
return True
def main():
"""Main function to check all provided Python files."""
if len(sys.argv) < 2:
print("Usage: check-silent-cleanup.py <file1> <file2> ...")
sys.exit(1)
files_to_check = sys.argv[1:]
has_errors = False
for filename in files_to_check:
if not check_file(filename):
has_errors = True
if has_errors:
print("\n" + "=" * 70)
print("Silent Cleanup: Unsafe close() patterns detected!")
print("=" * 70)
print("\nTo fix, replace silent try/except blocks with safe_close():")
print(" from ...utilities.resource_utils import safe_close")
print(' safe_close(self.client, "client name")')
print("\nOr add explicit logging:")
print(' logger.warning("Failed to close resource", exc_info=True)')
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()