mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
Real fixes: split over-long functions (lock-decision chain, manual sync, validation, phone verification, status), hoist a dead lazy import, context-manage Xvfb and temp dirs, add encodings, share the silent-failure argv loop. lock_grab_handoff read lock._recovery, which gatelock's LockWindow does not have (its public property is recovery); a contract test now checks every attribute the handoff reads against the real class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
200 lines
7.5 KiB
Python
Executable File
200 lines
7.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Pre-commit hook: ban silent failures in Dart, mirroring the Python hook.
|
|
|
|
``check_silent_failures.py`` has enforced this rule for Python since the PC's
|
|
workout sync silently did nothing for weeks. It is gated on ``types: [python]``,
|
|
so it has never looked at a single Dart file — and the workout app is where the
|
|
same class of bug is most expensive, because a swallowed sync error there costs
|
|
the user a workout with nothing anywhere saying so.
|
|
|
|
The rule matches the Python hook: every ``catch`` must either rethrow or say
|
|
what happened somewhere a human will actually see. In Dart that means
|
|
|
|
* ``rethrow`` / ``throw`` (the failure is someone else's problem), or
|
|
* ``log(...)`` from ``dart:developer`` -- the release-safe channel, and the one
|
|
the sync services already use at ``level: 1000``, or
|
|
* ``debugPrint(...)`` -- accepted because this codebase uses it widely for
|
|
operator-facing storage/backup diagnostics, even though it is stripped in
|
|
release builds.
|
|
|
|
``print()`` does NOT count: it is lint-banned in this repo and vanishes in
|
|
release. A bare ``catchError((_) {})`` is the Dart spelling of ``except: pass``
|
|
and is always reported.
|
|
|
|
Deliberately regex/brace based rather than a real Dart parser: the alternative
|
|
is making `analyzer` a hook dependency, and the shapes this needs to recognise
|
|
are narrow. It errs toward reporting -- an unparsable handler is a finding,
|
|
not a skip, exactly like the Python hook's SyntaxError branch.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
from scripts._silent_cli import run_hook
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# `log` is dart:developer's; `debugPrint` is flutter/foundation's.
|
|
#
|
|
# `_setSyncStatus` / `ScaffoldMessenger` count as loud on purpose: telling the
|
|
# USER "could not start device flow: <reason>" in the UI is the loudest channel
|
|
# there is. The rule is "the failure must be visible", not "it must reach a
|
|
# log file", and a screen that reports the error is not a silent failure.
|
|
_LOUD_CALLS = (
|
|
"log(",
|
|
"debugPrint(",
|
|
"rethrow",
|
|
"throw ",
|
|
"_setSyncStatus(",
|
|
"ScaffoldMessenger",
|
|
)
|
|
|
|
# `} on Foo catch (e) {`, `} catch (e) {`, `} on Foo {`.
|
|
#
|
|
# The bare `on Foo {` form requires the preceding `}` of the try block: without
|
|
# it this also matched `extension StorageServiceBackup on StorageService {`,
|
|
# reporting an extension declaration as a swallowed exception.
|
|
_CATCH_RE = re.compile(r"\bcatch\s*\(|\}\s*on\s+\w+\s*\{")
|
|
|
|
# `.catchError((_) {})` and friends -- an empty body is always silent.
|
|
_EMPTY_CATCH_ERROR_RE = re.compile(r"\.catchError\(\s*\([^)]*\)\s*(?:=>\s*)?\{\s*\}")
|
|
|
|
|
|
def _strip_noise(source: str) -> str:
|
|
"""Blank out comments and string literals so they cannot match as code.
|
|
|
|
A comment mentioning ``log(`` would otherwise make a silent handler look
|
|
loud -- the exact false negative that lets the bug back in.
|
|
"""
|
|
out: list[str] = []
|
|
i = 0
|
|
n = len(source)
|
|
while i < n:
|
|
two = source[i : i + 2]
|
|
if two == "//":
|
|
end = source.find("\n", i)
|
|
end = n if end == -1 else end
|
|
out.append(" " * (end - i))
|
|
i = end
|
|
elif two == "/*":
|
|
end = source.find("*/", i + 2)
|
|
end = n if end == -1 else end + 2
|
|
# Keep newlines so line numbers stay correct.
|
|
out.append("".join(c if c == "\n" else " " for c in source[i:end]))
|
|
i = end
|
|
elif source[i] in "\"'":
|
|
quote = source[i]
|
|
j = i + 1
|
|
while j < n and source[j] != quote:
|
|
if source[j] == "\\":
|
|
j += 1
|
|
if source[j : j + 1] == "\n":
|
|
break
|
|
j += 1
|
|
j = min(j + 1, n)
|
|
out.append("".join(c if c == "\n" else " " for c in source[i:j]))
|
|
i = j
|
|
else:
|
|
out.append(source[i])
|
|
i += 1
|
|
return "".join(out)
|
|
|
|
|
|
def _block_after(source: str, start: int) -> tuple[str, int] | None:
|
|
"""Return the ``{...}`` block that follows ``start``, and its end offset."""
|
|
brace = source.find("{", start)
|
|
if brace == -1:
|
|
return None
|
|
depth = 0
|
|
for idx in range(brace, len(source)):
|
|
if source[idx] == "{":
|
|
depth += 1
|
|
elif source[idx] == "}":
|
|
depth -= 1
|
|
if not depth:
|
|
return source[brace : idx + 1], idx
|
|
return None
|
|
|
|
|
|
def _handler_hands_error_on(body: str, catch_clause: str) -> bool:
|
|
"""Return True if the handler passes the caught error to its caller.
|
|
|
|
``} on GitHubSyncError catch (e) { return e; }`` is not a silent failure:
|
|
the error becomes the function's result, and whoever asked for it decides
|
|
how loud to be. Same for stashing it in state the UI renders
|
|
(``setState(() => _error = '$e')``) or reassigning a variable that is
|
|
logged after the loop. Reporting these would push the codebase toward
|
|
duplicate logging, which is its own kind of noise.
|
|
"""
|
|
name = re.search(r"catch\s*\(\s*(\w+)", catch_clause)
|
|
if name is None:
|
|
return False
|
|
var = name.group(1)
|
|
return bool(
|
|
# `return e;`
|
|
re.search(rf"\breturn\s+{var}\b", body)
|
|
# `failure = retryError;` — reassigned for the caller to report.
|
|
or re.search(rf"=\s*{var}\s*[;,)]", body)
|
|
# `setState(() => _error = '$e')` — rendered by the widget.
|
|
or re.search(rf"=\s*'[^']*\$\{{?{var}\b", body)
|
|
)
|
|
|
|
|
|
def _check_source(path: Path, source: str) -> list[str]:
|
|
"""Return one message per silently-swallowing catch in ``source``."""
|
|
code = _strip_noise(source)
|
|
problems: list[str] = []
|
|
|
|
for match in _EMPTY_CATCH_ERROR_RE.finditer(code):
|
|
line = code.count("\n", 0, match.start()) + 1
|
|
problems.append(
|
|
f"{path}:{line}: .catchError with an empty body swallows the error "
|
|
f"without logging why — log(...) the reason, or rethrow"
|
|
)
|
|
|
|
for match in _CATCH_RE.finditer(code):
|
|
block = _block_after(code, match.start())
|
|
if block is None:
|
|
line = code.count("\n", 0, match.start()) + 1
|
|
problems.append(
|
|
f"{path}:{line}: could not find the catch body — reporting it "
|
|
f"rather than skipping, since it cannot be checked"
|
|
)
|
|
continue
|
|
body, _end = block
|
|
# The clause itself (`catch (retryError)`) names the variable; slice it
|
|
# from the match rather than searching the file, which would find the
|
|
# FIRST catch in the file for every handler.
|
|
clause = code[match.start() : code.find("{", match.start()) + 1]
|
|
# Check the RAW body too: `_error = '$e'` hands the error to the UI,
|
|
# but _strip_noise blanks the string literal it lives in, so the
|
|
# stripped body alone cannot see it.
|
|
raw_body = source[match.start() : match.start() + len(body) + 200]
|
|
if _handler_hands_error_on(body, clause) or _handler_hands_error_on(
|
|
raw_body, clause
|
|
):
|
|
continue
|
|
if not any(token in body for token in _LOUD_CALLS):
|
|
line = code.count("\n", 0, match.start()) + 1
|
|
problems.append(
|
|
f"{path}:{line}: catch swallows the error without logging why "
|
|
f"— rethrow, or log(..., level: 1000) / debugPrint the reason"
|
|
)
|
|
return problems
|
|
|
|
|
|
def main() -> int:
|
|
"""Return 1 if any Dart file swallows an error silently, else 0."""
|
|
return run_hook(_check_source)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|