feat(audit): add lazy-loading sentinel fallback heuristic (Phase 12)
Adds a new heuristic to scripts/audit_exception_handling.py:_try_compliant_pattern
(heuristic B, after heuristic A) that recognizes the canonical lazy-loading
sentinel fallback pattern:
def _resolve(self):
try:
self._cached = getattr(mod, attr_name)
except AttributeError:
sub_mod_name = f'{module_name}.{attr_name}'
try:
self._cached = importlib.import_module(sub_mod_name)
except (ImportError, ModuleNotFoundError):
self._cached = _FiledialogStub()
The heuristic fires when:
- The enclosing function is in LAZY_LOADER_METHOD_NAMES
({_resolve, _load, _get, _try_load}) — the canonical naming
convention for proxy classes that defer a heavy import
- The except body does NOT re-raise
- The except set is in {AttributeError, ImportError, ModuleNotFoundError}
- The except body assigns to a self.<attr> (directly or via nested try)
Sites matching this pattern are classified INTERNAL_COMPLIANT (not
UNCLEAR). The sentinel is a documented graceful-degradation marker
with an 'available: bool = False' flag (or similar) that the UI can
check to detect the stub and offer an alternative path. This is
analogous to the nil-sentinel dataclass (Pattern 1 in error_handling.md).
Per error_handling.md:625-690 (Re-Raise Patterns) and the lazy-loading
pattern guidance, this is NOT silent-sliming. Reclassifies the 2
UNCLEAR sites in src/gui_2.py at L65 and L69 (_LazyModule._resolve).
Pre-Phase 12 baseline: 2 UNCLEAR sites. Post-Phase 12: 0 UNCLEAR.
gui_2.py: V=0, S=0, ?=0, C=56 (was V=0, S=0, ?=2, C=54).
Phase 12 result_migration_gui_2_20260619.
This commit is contained in:
@@ -222,6 +222,23 @@ PROGRAMMER_ERROR_EXCEPTIONS: frozenset[str] = frozenset({
|
||||
"NotImplementedError",
|
||||
})
|
||||
|
||||
# Lazy-loader method names: the canonical naming convention for proxy
|
||||
# classes that defer a heavy import until first attribute access or call
|
||||
# (e.g. _LazyModule._resolve, _load, _get, _try_load). The audit
|
||||
# recognizes these as the canonical context for the sentinel-fallback
|
||||
# pattern (Phase 12.1 result_migration_gui_2_20260619): when the import
|
||||
# or attribute access fails, the except body falls back to a documented
|
||||
# sentinel class instance with an `available: bool = False` flag (or
|
||||
# similar) so the UI can detect the stub and offer an alternative
|
||||
# path. This is the canonical graceful-degradation pattern per
|
||||
# error_handling.md:625-690 (Re-Raise Patterns).
|
||||
LAZY_LOADER_METHOD_NAMES: frozenset[str] = frozenset({
|
||||
"_resolve",
|
||||
"_load",
|
||||
"_get",
|
||||
"_try_load",
|
||||
})
|
||||
|
||||
# Categories that are considered violations
|
||||
VIOLATION_CATEGORIES: frozenset[str] = frozenset({
|
||||
"INTERNAL_SILENT_SWALLOW",
|
||||
@@ -743,6 +760,35 @@ class ExceptionVisitor(ast.NodeVisitor):
|
||||
f"Compliant: `try: ...; except ({', '.join(sorted(exc_set))}): return Result(data=..., errors=[...])` is the canonical Result-recovery pattern. The function-name-not-ending-in-`_result` is a smell (rename to `xxx_result`); the pattern itself is the data-oriented convention. (per result_migration_small_files_20260617 Phase 11.2)",
|
||||
)
|
||||
|
||||
# B. Lazy-loading sentinel fallback — Phase 12.1 (result_migration_gui_2_20260619)
|
||||
# Per error_handling.md:625-690 (Re-Raise Patterns) and the lazy-loading
|
||||
# pattern guidance, when a module is loaded lazily (e.g. numpy, tkinter
|
||||
# at first attribute access) and the import or attribute access fails,
|
||||
# falling back to a documented sentinel class instance with an
|
||||
# `available: bool = False` flag is the canonical graceful-degradation
|
||||
# pattern. The sentinel is NOT a silent swallow: the UI can detect the
|
||||
# stub via the `available` flag and offer an alternative code path
|
||||
# (e.g. ImGui file dialog when tkinter.filedialog is unavailable).
|
||||
# This is analogous to the nil-sentinel dataclass (Pattern 1 in
|
||||
# error_handling.md). The function-name heuristic (`_resolve`/`_load`/
|
||||
# `_get`/`_try_load`) is the standard lazy-loader naming convention.
|
||||
# The except body must NOT re-raise; the recovery is via assignment
|
||||
# to `self.<attr>` (directly or via a nested try/except).
|
||||
except_body_re_raises = any(
|
||||
isinstance(s, ast.Raise) and s.exc is None
|
||||
for s in ast.walk(ast.Module(body=except_body, type_ignores=[]))
|
||||
)
|
||||
if (
|
||||
self._current_func_name() in LAZY_LOADER_METHOD_NAMES
|
||||
and not except_body_re_raises
|
||||
and exc_set & {"AttributeError", "ImportError", "ModuleNotFoundError"}
|
||||
and self._has_self_attr_assign(except_body)
|
||||
):
|
||||
return (
|
||||
"INTERNAL_COMPLIANT",
|
||||
f"Compliant: lazy-loading sentinel fallback. `try: ...; except ({', '.join(sorted(exc_set))}): self.<attr> = <sentinel>()` in `{self._current_func_name()}` is the canonical graceful-degradation pattern. The sentinel class exposes an `available: bool = False` flag (or similar) so the UI can detect the stub and offer an alternative path. Per error_handling.md:625-690 and Phase 12.1 result_migration_gui_2_20260619.",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _has_string_return(self, stmts: list[ast.stmt]) -> bool:
|
||||
@@ -921,6 +967,37 @@ class ExceptionVisitor(ast.NodeVisitor):
|
||||
has_return_none_after = True
|
||||
return has_for_range_with_try and has_return_none_after
|
||||
|
||||
def _has_self_attr_assign(self, stmts: list[ast.stmt]) -> bool:
|
||||
"""True if any statement (recursively) assigns to a `self.<attr>` attribute.
|
||||
|
||||
Used by the lazy-loading sentinel fallback heuristic (Phase 12.1) to
|
||||
detect the canonical graceful-degradation pattern: the except body
|
||||
falls back to a sentinel class instance via `self._cached = _Stub()`
|
||||
either directly OR via a nested try/except (e.g., an outer try that
|
||||
catches AttributeError and a nested try that ultimately falls back
|
||||
to the stub). The recursive walk handles both cases:
|
||||
|
||||
- Direct: `try: getattr(...); except AttributeError: self._cached = _Stub()`
|
||||
- Nested: `try: getattr(...); except AttributeError: try: importlib...; except: self._cached = _Stub()`
|
||||
|
||||
Per the styleguide (error_handling.md:625-690), this is the canonical
|
||||
graceful-degradation pattern for lazy-loading modules that may not
|
||||
be present on every Python install. The sentinel's `available: bool = False`
|
||||
flag (or similar) lets the UI detect the stub and offer an alternative
|
||||
path (e.g., ImGui file dialog when tkinter.filedialog is unavailable).
|
||||
"""
|
||||
for s in stmts:
|
||||
for node in ast.walk(s):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if (
|
||||
isinstance(target, ast.Attribute)
|
||||
and isinstance(target.value, ast.Name)
|
||||
and target.value.id == "self"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_imgui_end_call(self, stmts: list[ast.stmt]) -> bool:
|
||||
"""True if any statement is a call to an imgui.end_* function."""
|
||||
for s in stmts:
|
||||
|
||||
Reference in New Issue
Block a user