diff --git a/diffgraph/structural.py b/diffgraph/structural.py index bbef6e0..e3985ec 100644 --- a/diffgraph/structural.py +++ b/diffgraph/structural.py @@ -726,6 +726,19 @@ def _resolution_warning(item: ResolutionWarning) -> Dict[str, str]: return _warning(code, item.path, "{}: {}".format(item.code, item.message)) +def _parse_failure_warning( + side: str, path: str, oid: Optional[str], error: Exception, +) -> Dict[str, str]: + """Preserve the exact immutable snapshot that Tree-sitter could not parse.""" + return _warning( + "PARSE_FAILURE", + path, + "{}-change blob {}: {}: {}".format( + side, oid or "absent", type(error).__name__, error, + ), + ) + + def _symbol_id(path: str, qualified_name: str) -> str: return "sym::{}::{}".format(path, qualified_name) @@ -974,7 +987,9 @@ def analyze_local_diff( _parse_python(old) if old is not None else ([], [], [], {}, []) ) except parser_errors as error: - warnings.append(_warning("PARSE_FAILURE", entry.old_path or path, "pre-change: {}: {}".format(type(error).__name__, error))) + warnings.append(_parse_failure_warning( + "pre", entry.old_path or path, entry.old_oid, error, + )) skipped += 1 continue try: @@ -982,7 +997,9 @@ def analyze_local_diff( _parse_python(new) if new is not None else ([], [], [], {}, []) ) except parser_errors as error: - warnings.append(_warning("PARSE_FAILURE", path, "post-change: {}: {}".format(type(error).__name__, error))) + warnings.append(_parse_failure_warning( + "post", path, entry.new_oid, error, + )) skipped += 1 continue analyzed += 1 diff --git a/tests/fixtures/python_parser_failure.json b/tests/fixtures/python_parser_failure.json index 00755d6..ee9216a 100644 --- a/tests/fixtures/python_parser_failure.json +++ b/tests/fixtures/python_parser_failure.json @@ -16,7 +16,7 @@ { "code": "PARSE_FAILURE", "file": "broken.py", - "detail": "post-change: ValueError: Tree-sitter reported a syntax error at line 1, column 12" + "detail": "post-change blob c440d6d7f7fca646fb897d0b5d279ec775aeddfd: ValueError: Tree-sitter reported a syntax error at line 1, column 12" } ] } diff --git a/tests/test_structural.py b/tests/test_structural.py index 69d4122..35d388b 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -421,6 +421,30 @@ def test_parse_failure_is_scoped_and_matches_the_golden_fixture(tmp_path): assert actual == expected +def test_pre_change_parse_failure_names_the_immutable_blob(tmp_path): + """A broken pre-change snapshot remains reproducible after it is fixed.""" + root = repo(tmp_path) + write(root, "recovered.py", "def broken(:\n") + commit(root) + old_oid = git(root, "rev-parse", "HEAD:recovered.py") + write(root, "recovered.py", "def recovered():\n return True\n") + + artifact = analyze_local_diff(str(root)) + + assert_valid(artifact) + assert artifact["metadata"]["files_analyzed"] == 0 + assert artifact["metadata"]["files_skipped"] == 1 + assert artifact["symbols"] == [] + assert artifact["metadata"]["warnings"] == [{ + "code": "PARSE_FAILURE", + "file": "recovered.py", + "detail": ( + "pre-change blob {}: ValueError: Tree-sitter reported a syntax error " + "at line 1, column 12" + ).format(old_oid), + }] + + def test_no_network_calls(monkeypatch, tmp_path): root = repo(tmp_path) write(root, "a.py", "def a():\n pass\n") @@ -1254,9 +1278,9 @@ def unavailable(): "code": "PARSE_FAILURE", "file": "app.py", "detail": ( - "post-change: StructuralDependencyError: " + "post-change blob {}: StructuralDependencyError: " "parser dependency is unavailable" - ), + ).format(git(root, "hash-object", "app.py")), }]