Skip to content

Commit fc795c1

Browse files
authored
chore: release v0.3.0 (#38)
1 parent 4198276 commit fc795c1

7 files changed

Lines changed: 53 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ flowchart TD
9090
class CG data
9191
```
9292

93-
Node IDs follow the scheme `module:function`, `module:Class`, or `module:Class.method` for unambiguous lookup. Edge confidence is tagged as `certain` (direct calls, `self.method()`), `inferred` (attribute access on non-self objects), or `uncertain` (dynamic dispatch).
93+
Node IDs follow the scheme `module:function`, `module:Class`, or `module:Class.method` for unambiguous lookup. Directory parsing resolves bare cross-file calls when a unique definition exists; ambiguous cross-file calls are left at their original best-effort target and marked `uncertain`. Edge confidence is tagged as `certain` (direct calls, `self.method()`), `inferred` (attribute access on non-self objects), or `uncertain` (dynamic dispatch or ambiguous resolution).
9494

9595
### 2. Index
9696

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "trailmark"
7-
version = "0.2.2"
7+
version = "0.3.0"
88
description = "Parse source code into a queryable graph of functions, classes, calls, and semantic annotations"
99
readme = "README.md"
1010
requires-python = ">=3.12"

src/trailmark/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
"parse_file",
1414
"supported_languages",
1515
]
16-
__version__ = "0.2.2"
16+
__version__ = "0.3.0"

src/trailmark/parsers/_common.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def walk_source_files(
121121
"""
122122
for root, dirs, files in os.walk(dir_path, followlinks=False):
123123
dirs[:] = [d for d in dirs if not should_skip_dir(d)]
124+
dirs.sort()
124125
for fname in sorted(files):
125126
if any(fname.endswith(ext) for ext in extensions):
126127
yield os.path.join(root, fname)
@@ -339,8 +340,9 @@ def _link_cross_file_calls(graph: CodeGraph) -> None:
339340
if len(candidates) == 1:
340341
new_edges.append(dataclasses.replace(edge, target_id=candidates[0]))
341342
else:
342-
# Ambiguous — pick the candidate NOT in the caller's own module
343-
# to prefer the cross-file definition over a same-file shadow.
343+
# Ambiguous: prefer a single cross-file candidate when a same-file
344+
# declaration is also present, but do not invent an arbitrary call
345+
# target when multiple cross-file definitions share the same name.
344346
src_module = edge.source_id.rsplit(":", 1)[0] if ":" in edge.source_id else ""
345347
cross = [c for c in candidates if not c.startswith(src_module + ":")]
346348
if len(cross) == 1:
@@ -349,7 +351,6 @@ def _link_cross_file_calls(graph: CodeGraph) -> None:
349351
new_edges.append(
350352
dataclasses.replace(
351353
edge,
352-
target_id=candidates[0],
353354
confidence=EdgeConfidence.UNCERTAIN,
354355
)
355356
)

tests/test_common_parser.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,21 @@ def _cross_file_parse(file_path: str) -> CodeGraph:
221221
kind=EdgeKind.CALLS,
222222
)
223223
)
224+
elif stem == "ambiguous_caller":
225+
caller_id = f"{module_id}:invoke_shared"
226+
nodes[caller_id] = CodeUnit(
227+
id=caller_id,
228+
name="invoke_shared",
229+
kind=NodeKind.FUNCTION,
230+
location=location,
231+
)
232+
edges.append(
233+
CodeEdge(
234+
source_id=caller_id,
235+
target_id=f"{module_id}:shared_name",
236+
kind=EdgeKind.CALLS,
237+
)
238+
)
224239

225240
return CodeGraph(nodes=nodes, edges=edges, language="test", root_path=file_path)
226241

@@ -278,3 +293,28 @@ def test_cross_file_linker_leaves_unresolvable_calls(tmp_path: Path) -> None:
278293
call_edges = [e for e in graph.edges if e.kind == EdgeKind.CALLS]
279294
assert len(call_edges) == 1
280295
assert call_edges[0].target_id == "caller:do_work"
296+
297+
298+
def test_cross_file_linker_keeps_ambiguous_cross_file_calls_unresolved(
299+
tmp_path: Path,
300+
) -> None:
301+
"""Multiple cross-file definitions should not produce an arbitrary target."""
302+
(tmp_path / "ambiguous1.c").write_text("")
303+
(tmp_path / "ambiguous2.c").write_text("")
304+
(tmp_path / "ambiguous_caller.c").write_text("")
305+
306+
graph = parse_directory(
307+
_cross_file_parse,
308+
language="test",
309+
dir_path=str(tmp_path),
310+
extensions=(".c",),
311+
)
312+
313+
call_edges = [
314+
e
315+
for e in graph.edges
316+
if e.kind == EdgeKind.CALLS and e.source_id == "ambiguous_caller:invoke_shared"
317+
]
318+
assert len(call_edges) == 1
319+
assert call_edges[0].target_id == "ambiguous_caller:shared_name"
320+
assert call_edges[0].confidence == EdgeConfidence.UNCERTAIN

tests/test_readme.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ def test_readme_mentions_parse_only_api(self, readme_text: str) -> None:
228228
assert "from trailmark.parse import parse_directory, parse_file" in readme_text
229229
assert 'graph = parse_file("path/to/file.py")' in readme_text
230230

231+
def test_readme_documents_cross_file_call_resolution(self, readme_text: str) -> None:
232+
assert "Directory parsing resolves bare cross-file calls" in readme_text
233+
assert "ambiguous cross-file calls" in readme_text
234+
231235

232236
def _read_requires_python(pyproject_data: dict[str, object]) -> str:
233237
project_raw = pyproject_data.get("project")

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)