diff --git a/diffgraph/structural.py b/diffgraph/structural.py index ab0e80f..2fef866 100644 --- a/diffgraph/structural.py +++ b/diffgraph/structural.py @@ -290,15 +290,27 @@ def visit(node, parents: Tuple[Tuple[str, str], ...] = ()) -> None: if occurrence == 0 else "{}#{}".format(base_qname, occurrence) ) - body = content[node.start_byte:node.end_byte] + # Tree-sitter wraps decorated declarations in a + # ``decorated_definition`` node. The decorator is part of the + # declaration's exact source-level behavior, so include that + # wrapper in the symbol span and hash. Otherwise changing + # ``@decorator`` could incorrectly leave the declaration + # unchanged in a deterministic DiffGraph artifact. + declaration = ( + node.parent + if node.parent is not None + and node.parent.type == "decorated_definition" + else node + ) + body = content[declaration.start_byte:declaration.end_byte] symbols.append( _Symbol( name, qname, kind, parent, - node.start_point[0] + 1, - node.end_point[0] + 1, + declaration.start_point[0] + 1, + declaration.end_point[0] + 1, hashlib.sha256(body).hexdigest(), ) ) diff --git a/tests/test_structural.py b/tests/test_structural.py index 99df344..676f245 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -1442,3 +1442,33 @@ def test_later_import_does_not_hide_earlier_local_call(tmp_path): assert len(calls) == 1 assert calls[0]["target_id"] == "sym::local_before_import.py::run" assert calls[0]["resolution_method"] == "resolved" + + +def test_decorator_change_is_part_of_python_declaration_evidence(tmp_path): + """Decorator edits modify the wrapped declaration rather than disappearing.""" + root = repo(tmp_path) + write( + root, + "decorated.py", + "@first\ndef decorated():\n return 1\n", + ) + commit(root) + write( + root, + "decorated.py", + "@second\ndef decorated():\n return 1\n", + ) + + artifact = analyze_local_diff(str(root)) + + assert_valid(artifact) + symbol = next(item for item in artifact["symbols"] if item["name"] == "decorated") + assert symbol["kind"] == "function" + assert symbol["change_kind"] == "modified" + assert symbol["evidence"] == [{ + "kind": "ast_parse", + "file": "decorated.py", + "line_start": 1, + "line_end": 3, + "detail": symbol["evidence"][0]["detail"], + }]