From 45754a29485915e3b4dd5de5fa90aa08a1ce023c Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Mon, 28 Sep 2026 13:00:33 +0800 Subject: [PATCH] Flag a catch that only discards the error with void. A log line, including console.error, still counts as handling the exception. Co-authored-by: Cursor Change-Id: I9440b5b3f39e69c99b032041d0f298b03c5f29f3 --- engine/hooks/explicit-failures/README.md | 11 ++++++----- engine/hooks/explicit-failures/detect.py | 13 +++++++++---- engine/hooks/explicit-failures/tests/test_hooks.py | 11 +++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/engine/hooks/explicit-failures/README.md b/engine/hooks/explicit-failures/README.md index f409fa0c..2159bf94 100644 --- a/engine/hooks/explicit-failures/README.md +++ b/engine/hooks/explicit-failures/README.md @@ -22,8 +22,9 @@ Python (`.py`): JS/TS (`.js .jsx .ts .tsx .mjs .cjs .vue .svelte`): -- `catch {}` / `catch (e) {}` with an empty or comment-only body, or a body - that only `continue`s / `break`s / returns bare. +- `catch {}` / `catch (e) {}` with an empty or comment-only body, a body + whose only statements are `void err`, or a body that only `continue`s / + `break`s / returns bare. - `.catch(() => {})`, `.catch(e => null)`, `.catch(function () {})`. - `if (!x)` / `if (x == null)` / `if (x === undefined)` / `if (x.length === 0)` whose only statement is `continue`, `break`, or a bare / `null` / `[]` return. @@ -32,9 +33,9 @@ Bash: every heredoc body in the command is scanned; the redirect target (`cat > build.py <<'EOF'`) picks the grammar, a heredoc with no target (`python3 - < {})` no-op, and an `if (!x)` / `if (x == null)` guard whose -only statement is a bare exit. The substring `explicit-failures` on the +comment-only body, a body whose only statements are `void err`, a body that +only continues / returns bare, a `.catch(() => {})` no-op, and an +`if (!x)` / `if (x == null)` guard whose only statement is a bare exit. The substring `explicit-failures` on the header line, the line before it, or inside the block suppresses a hit (`# explicit-failures: allow`, `# pragma: explicit-failures: allow`, `// eslint-disable-next-line explicit-failures`). @@ -35,7 +35,10 @@ PY_SUFFIXES = (".py", ".pyi") JS_SUFFIXES = (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte") -ALLOW_TOKEN_RE = re.compile(r"(?i)(?:\blog|_log\b|\braise\b|\bthrow\b|status|reason|warn|\bprint\s*\()") +ALLOW_TOKEN_RE = re.compile( + r"(?i)(?:\blog|_log\b|\braise\b|\bthrow\b|status|reason|warn|\bprint\s*\(|console\.error)" +) +JS_VOID_ONLY_RE = re.compile(r"(?:void\s+\S+\s*;\s*)*void\s+\S+") ALLOW_MARK = "explicit-failures" PY_EXCEPT_RE = re.compile(r"^(\s*)except\b[^:]*:\s*(\S.*)?$") @@ -189,6 +192,8 @@ def scan_js(text: str) -> list[tuple[int, str]]: continue if stripped == "": hits.append((_line_of(text, m.start()), "`catch {}` with an empty body")) + elif JS_VOID_ONLY_RE.fullmatch(stripped) and not ALLOW_TOKEN_RE.search(body): + hits.append((_line_of(text, m.start()), "catch block that only discards the error with `void`")) elif re.fullmatch(JS_EXIT, stripped) and not ALLOW_TOKEN_RE.search(body): hits.append((_line_of(text, m.start()), f"catch block that only `{stripped}`s")) for m in JS_PROMISE_CATCH_RE.finditer(text): diff --git a/engine/hooks/explicit-failures/tests/test_hooks.py b/engine/hooks/explicit-failures/tests/test_hooks.py index 14115289..f07c5700 100644 --- a/engine/hooks/explicit-failures/tests/test_hooks.py +++ b/engine/hooks/explicit-failures/tests/test_hooks.py @@ -108,6 +108,17 @@ def test_fires_on_js_comment_only_catch_body(self): hits = detect.scan_js(text) self.assertEqual(hits, [(1, "`catch {}` with an empty body")]) + def test_fires_on_void_discard_in_catch(self): + text = "try { a() } catch (reportingFailure) {\n void reportingFailure;\n}\n" + hits = detect.scan_js(text) + self.assertEqual(hits, [(1, "catch block that only discards the error with `void`")]) + + def test_silent_on_console_error_and_void_console_error(self): + logged = "try { a() } catch (e) {\n console.error('metric failed', e);\n}\n" + discarded_return = "try { a() } catch (e) {\n void console.error('metric failed', e);\n}\n" + self.assertEqual(detect.scan_js(logged), []) + self.assertEqual(detect.scan_js(discarded_return), []) + def test_fires_on_promise_catch_noop_fixture(self): hits = lines_for(write_payload("promise_catch_noop_fires.js")) self.assertEqual([h.split(": ", 1)[1].split(" — ")[0] for h in hits], ["`.catch(() => {})` no-op rejection handler"] * 2)