mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
call_sites rescanned every line of every product file for declarations on each call; with one name per candidate row that was the whole cost of the source producer. code_index prepares a file once (the names each line declares), call_sites takes it, and callers_of keeps a cache of prepared files the producer context owns. Same calls, tested against the unprepared scan. One Laravel change of 33 rows: 249 seconds before this pair of commits, 20 now. Co-Authored-By: Claude Fable 5.1 <[email protected]>
165 lines
7.3 KiB
Python
165 lines
7.3 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")
|
|
|
|
|
|
def test_facts_for_reuses_supplied_product_files_and_texts(tmp_path, monkeypatch):
|
|
"""With files and texts supplied, facts_for neither walks the tree nor reads the files it was given."""
|
|
from openultrasast import source
|
|
|
|
(tmp_path / "app.py").write_text("def handler(value):\n return value\n")
|
|
(tmp_path / "other.py").write_text("from app import handler\n\n\ndef run():\n return handler(1)\n")
|
|
monkeypatch.setattr(source, "product_files", lambda root: (_ for _ in ()).throw(AssertionError("walked the tree")))
|
|
texts = {"other.py": (tmp_path / "other.py").read_text()}
|
|
(tmp_path / "other.py").write_text("") # the supplied text, not the disk, is what callers_of reads
|
|
facts = source.facts_for(tmp_path, "app.py", 2, entry_points=[], files=["app.py", "other.py"], texts=texts)
|
|
assert facts.function == "handler" and facts.callers == (("other.py", 5),)
|
|
|
|
|
|
def test_call_index_gives_the_same_call_sites_and_is_reused_across_names(tmp_path):
|
|
from openultrasast import source
|
|
|
|
text = "def handler(value):\n return value\n\n\ndef run():\n x = handler(1) # handler\n return other(x)\n"
|
|
index = source.code_index(text)
|
|
assert source.call_sites(text, ("handler", "other")) == source.call_sites("", ("handler", "other"), index=index)
|
|
assert [call.line for call in source.call_sites("", ("handler",), index=index)] == [6]
|
|
(tmp_path / "other.py").write_text(text)
|
|
cache: dict = {}
|
|
assert source.callers_of(tmp_path, "handler", files=["other.py"], index=cache) == (("other.py", 6),)
|
|
(tmp_path / "other.py").write_text("") # the cached index, not the disk, answers the second name
|
|
assert source.callers_of(tmp_path, "other", files=["other.py"], index=cache) == (("other.py", 7),)
|