From 1a7d690aa65d4e5f8dea4b3f7227ab4fcd2cf967 Mon Sep 17 00:00:00 2001 From: anhnh2002 Date: Tue, 13 Oct 2026 09:06:21 +0700 Subject: [PATCH] fix: stop analyzers from swallowing the per-file parse timeout TimeoutError was an Exception, so when SIGALRM fired inside a language analyzer's own 'except Exception' block (e.g. JS _extract_call_from_node) the error was swallowed, the one-shot alarm was spent, and a slow file such as a 5.7 MB vendored JS bundle hung dependency analysis forever. Make it a BaseException so only _analyze_code_file catches it. --- .../analysis/call_graph_analyzer.py | 11 ++++- tests/test_parse_timeout.py | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/test_parse_timeout.py diff --git a/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py b/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py index 1cc33ec0..b95c93f8 100644 --- a/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py +++ b/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py @@ -28,8 +28,15 @@ logger = logging.getLogger(__name__) -class TimeoutError(Exception): - """Raised when file parsing exceeds timeout.""" +class TimeoutError(BaseException): + """Raised when file parsing exceeds timeout. + + A ``BaseException`` on purpose: the language analyzers wrap many small + steps in ``except Exception`` blocks. If the alarm fired inside one of + them, the error was swallowed, the alarm was already spent, and a slow + file (e.g. a multi-megabyte vendored JS bundle) ran forever. Only + ``_analyze_code_file`` catches it. + """ @contextmanager diff --git a/tests/test_parse_timeout.py b/tests/test_parse_timeout.py new file mode 100644 index 00000000..205015c3 --- /dev/null +++ b/tests/test_parse_timeout.py @@ -0,0 +1,41 @@ +"""The per-file parse timeout must not be swallowed by an analyzer's own +``except Exception`` blocks.""" + +import signal +import time + +import pytest + +from codewiki.src.be.dependency_analyzer.analysis import call_graph_analyzer +from codewiki.src.be.dependency_analyzer.analysis.call_graph_analyzer import ( + CallGraphAnalyzer, +) + +pytestmark = pytest.mark.skipif( + not hasattr(signal, "SIGALRM"), reason="the timeout relies on SIGALRM" +) + + +def _swallowing_analyzer(self, file_path, content, repo_dir): + # Mimics a language analyzer that guards each small step with + # ``except Exception`` and loops for a very long time. + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + try: + time.sleep(0.01) + except Exception: # noqa: BLE001 + pass + + +def test_timeout_escapes_except_exception(tmp_path, monkeypatch): + (tmp_path / "slow.js").write_text("f();\n", encoding="utf-8") + monkeypatch.setattr(CallGraphAnalyzer, "_analyze_javascript_file", _swallowing_analyzer) + + real_timeout = call_graph_analyzer.timeout + monkeypatch.setattr(call_graph_analyzer, "timeout", lambda seconds: real_timeout(1)) + + started = time.monotonic() + CallGraphAnalyzer()._analyze_code_file( + str(tmp_path), {"path": "slow.js", "language": "javascript"} + ) + assert time.monotonic() - started < 10