mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
src/openultrasast/source.py replaces the copies in semantic/functions.py, plane/tasks/repo_facts.py, learn/features.py, learn/excerpt.py and plane/tasks/verify.py. Two engines share one sentinel (<module>), one DECLARATION/KEYWORDS/RESERVED set and one alias map: the parser engine (ranges, enclosing, bounds, call_sites, callers_of, facts_for) for the scorer, the overlay and the hunter's annotations; the line-pattern engine (declared_functions, enclosing_at, function_span, function_bounds, the brace matcher) that labels, features, roles, excerpts and plane facts were built on. learn no longer imports plane.tasks. Frozen records that store the old sentinel are mapped on read (module_function). Gate: labels rebuilt before and after are identical, 7,921 rows, after sentinel normalisation (benchmarks/measurements/2026-10-13-source-module- label-diff/record.json). 490 targeted tests pass; 3,312 collect. Net: 657 lines deleted, 212 added. benchmarks/unseen keeps <global> for its own frozen manifests. Co-Authored-By: Claude Fable 5.1 <[email protected]>
138 lines
5.6 KiB
Python
138 lines
5.6 KiB
Python
"""Repository facts use the same names, ranges and calls in every consumer."""
|
|
|
|
import pytest
|
|
|
|
from openultrasast import source
|
|
from openultrasast.semantic import cst
|
|
from openultrasast.semantic.extra import grammar_for
|
|
|
|
SAMPLES = [
|
|
("python", "app.py", "def run(x):\n return x\n", (1, 2)),
|
|
("javascript", "app.js", "function run(x) {\n return x;\n}\n", (1, 3)),
|
|
("java", "App.java", "class App {\n public int run(int x) {\n return x;\n }\n}\n", (2, 4)),
|
|
("go", "app.go", "package app\nfunc (r *Repo) run(x int) int {\n return x\n}\n", (2, 4)),
|
|
("ruby", "app.rb", "class App\n def run(x)\n x\n end\nend\n", (2, 4)),
|
|
("php", "app.php", "<?php\nfunction run($x) {\n return $x;\n}\n", (2, 4)),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(("language", "path", "text", "expected"), SAMPLES)
|
|
@pytest.mark.parametrize("with_grammar", [False, True])
|
|
def test_ranges_with_and_without_grammars(monkeypatch, language, path, text, expected, with_grammar):
|
|
if not with_grammar:
|
|
monkeypatch.setenv("OPENULTRASAST_TREE_SITTER_PROBE", "off")
|
|
else:
|
|
monkeypatch.delenv("OPENULTRASAST_TREE_SITTER_PROBE", raising=False)
|
|
items = source.ranges(path, text, language)
|
|
assert source.bounds(items, "run") == expected
|
|
assert source.enclosing(items, expected[0]) == "run"
|
|
assert source.enclosing(items, expected[1]) == "run"
|
|
if with_grammar and grammar_for(language) is not None:
|
|
assert cst.parse_with_cst(path, text, language).engine == "tree-sitter"
|
|
|
|
|
|
def test_nested_ranges_and_module_bounds():
|
|
text = "def outer():\n def inner():\n pass\n inner()\nouter()\n"
|
|
items = source.ranges("app.py", text, "python")
|
|
assert source.enclosing(items, 3) == "inner"
|
|
assert source.enclosing(items, 4) == "outer"
|
|
assert source.enclosing(items, 5) == source.MODULE_FUNCTION
|
|
assert source.enclosing(items, None) == source.MODULE_FUNCTION
|
|
assert source.bounds(items, "outer") == (1, 4)
|
|
assert source.bounds(items, source.MODULE_FUNCTION) == (1, 5)
|
|
assert source.bounds(items, "missing") is None
|
|
|
|
|
|
def test_fallback_does_not_include_siblings_or_strings(monkeypatch):
|
|
monkeypatch.setenv("OPENULTRASAST_TREE_SITTER_PROBE", "off")
|
|
items = source.ranges("app.js", 'function first() {\n const s = "}"; // }\n}\nfunction last() {}\nfirst();', "javascript")
|
|
assert source.bounds(items, "first") == (1, 3)
|
|
assert source.bounds(items, "last") == (4, 4)
|
|
assert source.enclosing(items, 5) == source.MODULE_FUNCTION
|
|
|
|
|
|
def test_calls_skip_declarations_comments_strings_and_duplicate_mentions():
|
|
text = 'def run():\n target(1); target(2)\n# target(3)\ns = "target(4)"\nobj.target(5)\nrun()\n'
|
|
assert source.call_sites(text, ["target", "run"]) == (
|
|
source.CallSite(2, "target"),
|
|
source.CallSite(5, "target"),
|
|
source.CallSite(6, "run"),
|
|
)
|
|
assert source.call_sites(text, []) == ()
|
|
assert source.call_sites("function run() { return run(); }", ["run"]) == (source.CallSite(1, "run"),)
|
|
|
|
|
|
def test_callers_scan_only_supplied_files(tmp_path):
|
|
(tmp_path / "app.py").write_text("def run():\n pass\nrun()\n")
|
|
(tmp_path / "web.py").write_text("def handler():\n run()\n")
|
|
assert source.callers_of(tmp_path, "run", files=["web.py", "app.py"]) == (("app.py", 3), ("web.py", 2))
|
|
assert source.callers_of(tmp_path, "run", files=["web.py"]) == (("web.py", 2),)
|
|
assert source.callers_of(tmp_path, "absent", files=["web.py"]) == ()
|
|
with pytest.raises(OSError):
|
|
source.callers_of(tmp_path, "run", files=["missing.py"])
|
|
with pytest.raises(ValueError):
|
|
source.callers_of(tmp_path, "run", files=["../outside.py"])
|
|
|
|
|
|
def test_product_files_exclude_vendor_tests_and_private_paths(tmp_path):
|
|
names = [
|
|
"src/app.py",
|
|
"vendor/a.py",
|
|
"node_modules/a.js",
|
|
"tests/a.py",
|
|
"spec/a.rb",
|
|
"src/a_test.go",
|
|
"src/AppTest.java",
|
|
"src/a.test.js",
|
|
"dist/a.js",
|
|
".hidden/a.py",
|
|
".env",
|
|
"src/a.min.js",
|
|
]
|
|
for name in names:
|
|
path = tmp_path / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("run()\n")
|
|
(tmp_path / "link.py").symlink_to(tmp_path / "src/app.py")
|
|
assert source.product_files(tmp_path) == ["src/app.py"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("name", "expected"),
|
|
[
|
|
("py", "python"),
|
|
("python_web", "python"),
|
|
("node", "javascript"),
|
|
("js", "javascript"),
|
|
("ts", "typescript"),
|
|
("typescript", "typescript"),
|
|
("c_cpp", "cpp"),
|
|
("c++", "cpp"),
|
|
("cpp", "cpp"),
|
|
("Ruby", "ruby"),
|
|
("unknown", "unknown"),
|
|
],
|
|
)
|
|
def test_canonical_language(name, expected):
|
|
assert source.canonical_language(name) == expected
|
|
|
|
|
|
def test_facts_reuse_supplied_entry_points_and_read_only_repository(tmp_path, monkeypatch):
|
|
from openultrasast import mapping
|
|
|
|
(tmp_path / "app.py").write_text("def run(x):\n return x\n")
|
|
(tmp_path / "web.py").write_text("def handler():\n run(1)\n")
|
|
monkeypatch.setattr(mapping, "analyze_entry_points", lambda *_: pytest.fail("entry points were recomputed"))
|
|
result = source.facts_for(tmp_path, "app.py", 2, entry_points=[])
|
|
assert result == source.SourceFacts("python", "run", (1, 2), (("web.py", 2),), ())
|
|
|
|
|
|
def test_parse_rejection_is_not_an_empty_result(monkeypatch):
|
|
from openultrasast.semantic.ir import FileIR
|
|
|
|
monkeypatch.setattr(
|
|
cst, "parse_with_cst", lambda path, text, language: FileIR(path, language, "tree-sitter", (), False, "parse_failed")
|
|
)
|
|
with pytest.raises(ValueError, match="source parse failed"):
|
|
source.ranges("app.py", "def broken(", "python")
|