mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
An ambiguous extension (`.inc`, `.h`, TS-family, ...) is hard-mapped to one global default; when that is wrong the file does not error — it parses as the wrong language and yields a few incidental nodes, so the graph looks populated while the real symbols are missing. A project can now declare what an extension means via `language.<ext>=<language|.ext>` lines in `.graphifyrc` (which already holds `viz_node_limit`). The declaration flows through file classification, extractor selection, grammar/resolution, imports, watch, and the AST cache (salt + manifest language field + incremental invalidation), is thread-local per scan root, and is forwarded to the worker pool. Default detection is byte-for-byte unchanged when no declaration is present. Landed from PR #3789 (squashed), which carried @abhay-codes07's original #3075 forward with reviewed follow-ups. Authorship credited to both contributors. Co-Authored-By: Andre Brait <andrebrait@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
"""PR10: inferred candidates and call scope honor declared languages."""
|
|
|
|
import pytest
|
|
|
|
from graphify.extractors import resolution
|
|
from graphify.rcfile import activate_language_overrides, set_language_overrides
|
|
from tests.test_language_overrides import _quiet_extract
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _restore_language_overrides():
|
|
set_language_overrides(None)
|
|
yield
|
|
set_language_overrides(None)
|
|
|
|
|
|
@pytest.mark.parametrize("remap_native", [False, True])
|
|
@pytest.mark.parametrize("layout", ["file", "index", "esm"])
|
|
def test_js_inferred_candidates_respect_language_and_native_precedence(
|
|
tmp_path, layout, remap_native
|
|
):
|
|
config = "language.tpl=typescript\n"
|
|
if remap_native:
|
|
config += "language.ts=php\n"
|
|
(tmp_path / ".graphifyrc").write_text(config, encoding="utf-8")
|
|
activate_language_overrides(tmp_path)
|
|
candidate = tmp_path / ("dep.js" if layout == "esm" else "dep")
|
|
if layout == "index":
|
|
candidate.mkdir()
|
|
native, declared = candidate / "index.ts", candidate / "index.tpl"
|
|
else:
|
|
native, declared = tmp_path / "dep.ts", tmp_path / "dep.tpl"
|
|
native.write_text(
|
|
"<?php function foreign() {}" if remap_native else "export function native() {}",
|
|
encoding="utf-8",
|
|
)
|
|
declared.write_text("export function declared() {}", encoding="utf-8")
|
|
|
|
# A missing explicit .js path stays unresolved; it must not substitute PHP.
|
|
expected = (candidate if layout == "esm" else declared) if remap_native else native
|
|
assert resolution._resolve_js_module_path(candidate) == expected
|
|
|
|
|
|
@pytest.mark.parametrize("remap_native", [False, True])
|
|
@pytest.mark.parametrize("layout", ["module", "package"])
|
|
def test_python_inferred_candidates_respect_language_and_native_precedence(
|
|
tmp_path, layout, remap_native
|
|
):
|
|
config = "language.pyx=python\n"
|
|
if remap_native:
|
|
config += "language.py=javascript\n"
|
|
(tmp_path / ".graphifyrc").write_text(config, encoding="utf-8")
|
|
activate_language_overrides(tmp_path)
|
|
if layout == "package":
|
|
package = tmp_path / "dep"
|
|
package.mkdir()
|
|
native, declared = package / "__init__.py", package / "__init__.pyx"
|
|
else:
|
|
native, declared = tmp_path / "dep.py", tmp_path / "dep.pyx"
|
|
native.write_text(
|
|
"export function foreign() {}" if remap_native else "def native(): pass\n",
|
|
encoding="utf-8",
|
|
)
|
|
declared.write_text("def declared(): pass\n", encoding="utf-8")
|
|
|
|
assert resolution._resolve_python_module_path(
|
|
"dep", tmp_path / "caller.pyx", tmp_path, 0
|
|
) == (declared if remap_native else native)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config, marker, expected",
|
|
[
|
|
("language.pyx=python\n", "__init__.py", True),
|
|
("language.py=javascript\nlanguage.pyx=python\n", "__init__.py", False),
|
|
("language.py=javascript\nlanguage.pyx=python\n", "__init__.pyx", True),
|
|
],
|
|
)
|
|
def test_python_package_marker_must_be_python(tmp_path, config, marker, expected):
|
|
(tmp_path / ".graphifyrc").write_text(config, encoding="utf-8")
|
|
(tmp_path / marker).write_text("", encoding="utf-8")
|
|
activate_language_overrides(tmp_path)
|
|
|
|
assert resolution._is_python_package_dir(tmp_path) is expected
|
|
|
|
|
|
@pytest.mark.parametrize("caller_suffix", [".ts", ".tpl"])
|
|
@pytest.mark.parametrize("has_import", [False, True])
|
|
def test_js_cross_file_call_requires_an_import(tmp_path, caller_suffix, has_import):
|
|
(tmp_path / ".graphifyrc").write_text("language.tpl=typescript\n", encoding="utf-8")
|
|
provider = tmp_path / "provider.ts"
|
|
caller = tmp_path / f"caller{caller_suffix}"
|
|
provider.write_text("export function hidden() {}\n", encoding="utf-8")
|
|
caller.write_text(
|
|
("import { hidden } from './provider';\n" if has_import else "")
|
|
+ "export function caller() { hidden(); }\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = _quiet_extract([provider, caller], root=tmp_path, cache_root=tmp_path)
|
|
caller_id = next(
|
|
node["id"] for node in result["nodes"]
|
|
if node.get("source_file") == caller.name and node["label"] == "caller()"
|
|
)
|
|
provider_id = next(
|
|
node["id"] for node in result["nodes"]
|
|
if node.get("source_file") == provider.name and node["label"] == "hidden()"
|
|
)
|
|
assert any(
|
|
edge["source"] == caller_id and edge["target"] == provider_id
|
|
and edge["relation"] == "calls"
|
|
for edge in result["edges"]
|
|
) is has_import
|