mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 16:56:36 +00:00
fix(ingest): recognize Cargo.toml as a package manifest (#2434)
This commit is contained in:
+1
-1
@@ -490,7 +490,7 @@ def _shebang_file_type(path: Path) -> FileType | None:
|
||||
|
||||
|
||||
def classify_file(path: Path) -> FileType | None:
|
||||
# Package manifests (apm.yml, pyproject.toml, go.mod, pom.xml) are parsed
|
||||
# Package manifests (apm.yml, pyproject.toml, Cargo.toml, go.mod, pom.xml) are parsed
|
||||
# deterministically, so route them to the AST path (CODE) rather than the LLM
|
||||
# document path — otherwise apm.yml (a .yml "document") would be LLM-extracted
|
||||
# and a package would split into duplicate file-anchored nodes (#1377).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Deterministic package-manifest ingestion (#1377).
|
||||
|
||||
Package manifests (``apm.yml``, ``pyproject.toml``, ``go.mod``, ``pom.xml``)
|
||||
declare a package and its dependencies. Left to the LLM document path, the same
|
||||
Package manifests (``apm.yml``, ``pyproject.toml``, ``Cargo.toml``, ``go.mod``,
|
||||
``pom.xml``) declare a package and its dependencies. Left to the LLM document path, the same
|
||||
package gets a different file-anchored node id from its own manifest than from
|
||||
each dependent's dependency reference, so it splits into duplicate nodes. This
|
||||
module parses manifests deterministically and emits ONE canonical package node
|
||||
@@ -29,6 +29,7 @@ PACKAGE_MANIFEST_NAMES: dict[str, str] = {
|
||||
"apm.yml": "apm",
|
||||
"apm.yaml": "apm",
|
||||
"pyproject.toml": "python",
|
||||
"cargo.toml": "cargo",
|
||||
"go.mod": "go",
|
||||
"pom.xml": "maven",
|
||||
}
|
||||
@@ -193,6 +194,43 @@ def _parse_pyproject(text: str) -> dict | None:
|
||||
return {"name": name, "version": proj.get("version") or (poetry.get("version") if isinstance(poetry, dict) else None), "deps": deps}
|
||||
|
||||
|
||||
def _parse_cargo(text: str) -> dict | None:
|
||||
"""Cargo.toml: name/version from ``[package]``, runtime deps from
|
||||
``[dependencies]`` plus every ``[target.<cfg>.dependencies]`` table (mirrors
|
||||
``_parse_pyproject``'s runtime-only scope; dev-/build-dependencies excluded)."""
|
||||
try:
|
||||
import tomllib as _toml
|
||||
except ImportError: # pragma: no cover — Python < 3.11 without tomli only
|
||||
try:
|
||||
import tomli as _toml # type: ignore
|
||||
except ImportError:
|
||||
return None
|
||||
data = _toml.loads(text)
|
||||
pkg = data.get("package", {}) if isinstance(data.get("package"), dict) else {}
|
||||
name = pkg.get("name")
|
||||
# A virtual workspace root (``[workspace]``, no ``[package]``) declares no
|
||||
# package of its own — emit nothing rather than a fabricated node. ``name`` is
|
||||
# never workspace-inheritable in Cargo, but guard on the type anyway.
|
||||
if not isinstance(name, str) or not name:
|
||||
return None
|
||||
# ``version`` may be workspace-inherited (``version.workspace = true``), which
|
||||
# parses to a table; keep only a concrete string version.
|
||||
version = pkg.get("version")
|
||||
if not isinstance(version, str):
|
||||
version = None
|
||||
# A dependency value is a bare version string or an inline table; either way
|
||||
# _coerce_deps keys it by the dependency NAME (the table/map key).
|
||||
deps = _coerce_deps(data.get("dependencies"))
|
||||
# Platform-conditional deps live under ``[target.<cfg>.dependencies]``; fold
|
||||
# them in so a crate whose deps are entirely cfg-gated still emits its edges.
|
||||
targets = data.get("target")
|
||||
if isinstance(targets, dict):
|
||||
for cfg in targets.values():
|
||||
if isinstance(cfg, dict):
|
||||
deps += _coerce_deps(cfg.get("dependencies"))
|
||||
return {"name": name, "version": version, "deps": deps}
|
||||
|
||||
|
||||
def _parse_gomod(text: str) -> dict | None:
|
||||
name = None
|
||||
deps: list[str] = []
|
||||
@@ -242,6 +280,7 @@ def _parse_pom(text: str) -> dict | None:
|
||||
_PARSERS = {
|
||||
"apm": _parse_apm,
|
||||
"python": _parse_pyproject,
|
||||
"cargo": _parse_cargo,
|
||||
"go": _parse_gomod,
|
||||
"maven": _parse_pom,
|
||||
}
|
||||
|
||||
@@ -112,3 +112,54 @@ def test_malformed_manifest_does_not_crash(tmp_path):
|
||||
p = _write(tmp_path / "pom.xml", "<project><not closed")
|
||||
r = extract_package_manifest(p) # parse error -> empty, no exception
|
||||
assert r["nodes"] == [] and r["edges"] == []
|
||||
|
||||
|
||||
# ── #2434: Cargo.toml joins pyproject.toml/go.mod/pom.xml as a package manifest ─
|
||||
|
||||
def test_cargo_classifies_as_code_manifest(tmp_path):
|
||||
p = _write(tmp_path / "Cargo.toml", '[package]\nname = "x"\n')
|
||||
assert is_package_manifest_path(p)
|
||||
assert classify_file(p) is FileType.CODE
|
||||
|
||||
|
||||
def test_cargo_parses_name_version_and_deps(tmp_path):
|
||||
# A crate declares its name/version under [package]; deps appear both as a
|
||||
# bare version string and as an inline table (version + features).
|
||||
p = _write(tmp_path / "Cargo.toml",
|
||||
'[package]\nname = "my-crate"\nversion = "0.3.1"\nedition = "2021"\n\n'
|
||||
'[dependencies]\nserde = "1.0"\ntokio = { version = "1", features = ["full"] }\n')
|
||||
r = extract_package_manifest(p)
|
||||
pkg = _pkg_nodes(r)[0]
|
||||
assert pkg["label"] == "my-crate" and pkg["version"] == "0.3.1"
|
||||
assert pkg["ecosystem"] == "cargo"
|
||||
deps = {e["target"] for e in r["edges"] if e["relation"] == "depends_on"}
|
||||
assert {"pkg_serde", "pkg_tokio"} <= deps # inline-table dep keyed by name
|
||||
|
||||
|
||||
def test_cargo_virtual_workspace_manifest_emits_no_package(tmp_path):
|
||||
# A virtual workspace root has no [package] table, so it declares no package
|
||||
# of its own — it must not fabricate a node.
|
||||
p = _write(tmp_path / "Cargo.toml", '[workspace]\nmembers = ["a", "b"]\n')
|
||||
r = extract_package_manifest(p)
|
||||
assert _pkg_nodes(r) == []
|
||||
|
||||
|
||||
def test_cargo_target_conditional_deps_are_collected(tmp_path):
|
||||
# Platform-gated deps under [target.'cfg(...)'.dependencies] are common in
|
||||
# real crates and must not be dropped just because they are conditional.
|
||||
p = _write(tmp_path / "Cargo.toml",
|
||||
'[package]\nname = "portable"\n\n'
|
||||
'[dependencies]\nserde = "1"\n\n'
|
||||
'[target."cfg(windows)".dependencies]\nwinapi = "0.3"\n')
|
||||
r = extract_package_manifest(p)
|
||||
deps = {e["target"] for e in r["edges"] if e["relation"] == "depends_on"}
|
||||
assert {"pkg_serde", "pkg_winapi"} <= deps
|
||||
|
||||
|
||||
def test_cargo_workspace_inherited_version_does_not_crash(tmp_path):
|
||||
# `version.workspace = true` yields a table, not a string. It must be ignored
|
||||
# (no bogus version attribute) rather than crash the parse.
|
||||
p = _write(tmp_path / "Cargo.toml",
|
||||
'[package]\nname = "member"\nversion.workspace = true\n')
|
||||
pkg = _pkg_nodes(extract_package_manifest(p))[0]
|
||||
assert pkg["label"] == "member" and "version" not in pkg
|
||||
|
||||
Reference in New Issue
Block a user