mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
perf(hunt): the caller search prepares each product file once per change
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]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
86d70831c8
commit
4cf9c85d22
@@ -56,6 +56,7 @@ class Context:
|
|||||||
spans: dict[str, tuple[source.FunctionRange, ...]] = field(default_factory=dict)
|
spans: dict[str, tuple[source.FunctionRange, ...]] = field(default_factory=dict)
|
||||||
range_errors: dict[str, str] = field(default_factory=dict)
|
range_errors: dict[str, str] = field(default_factory=dict)
|
||||||
irs: dict[str, FileIR] = field(default_factory=dict)
|
irs: dict[str, FileIR] = field(default_factory=dict)
|
||||||
|
call_index: dict[str, tuple[source.CodeLine, ...]] = field(default_factory=dict)
|
||||||
diff_bytes: int = 0
|
diff_bytes: int = 0
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -211,7 +212,9 @@ def _source(root: Path, change: Change, path: str, line: int, ctx: Context) -> d
|
|||||||
ctx.read(path)
|
ctx.read(path)
|
||||||
if path in ctx.range_errors:
|
if path in ctx.range_errors:
|
||||||
raise _Absent(ctx.range_errors[path])
|
raise _Absent(ctx.range_errors[path])
|
||||||
facts = source.facts_for(root, path, line, entry_points=ctx.entry_points, files=ctx.product_files, texts=ctx.texts)
|
facts = source.facts_for(
|
||||||
|
root, path, line, entry_points=ctx.entry_points, files=ctx.product_files, texts=ctx.texts, index=ctx.call_index
|
||||||
|
)
|
||||||
# Select the occurrence at this line when several methods share a name.
|
# Select the occurrence at this line when several methods share a name.
|
||||||
spans = [r for r in ctx.ranges(path) if r.name == facts.function and r.start <= line <= r.end]
|
spans = [r for r in ctx.ranges(path) if r.name == facts.function and r.start <= line <= r.end]
|
||||||
span = min(spans, key=lambda r: r.end - r.start) if spans else None
|
span = min(spans, key=lambda r: r.end - r.start) if spans else None
|
||||||
|
|||||||
+46
-12
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
from collections.abc import Iterable, Mapping, Sequence
|
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||||
@@ -389,19 +389,37 @@ def spans_named(ranges: Sequence[FunctionRange], name: str) -> tuple[tuple[int,
|
|||||||
return tuple((item.start, item.end) for item in ranges if item.name == name)
|
return tuple((item.start, item.end) for item in ranges if item.name == name)
|
||||||
|
|
||||||
|
|
||||||
def call_sites(text: str, names: Iterable[str]) -> tuple[CallSite, ...]:
|
class CodeLine(NamedTuple):
|
||||||
"""Return calls in line order, excluding declarations and comments, once per name and line."""
|
"""One code line with the function names it declares: the part of a call search that is the same for every name."""
|
||||||
wanted = sorted(set(names) - RESERVED_FUNCTION_LABELS - {MODULE_FUNCTION}, key=lambda name: (-len(name), name))
|
|
||||||
if not wanted:
|
number: int
|
||||||
return ()
|
text: str
|
||||||
token = re.compile(r"(?<![A-Za-z0-9_$])(" + "|".join(re.escape(name) for name in wanted) + r")\s*\(")
|
declared: frozenset[str]
|
||||||
result = []
|
|
||||||
|
|
||||||
|
def code_index(text: str) -> tuple[CodeLine, ...]:
|
||||||
|
"""Prepare a file for repeated call searches; call_sites over many names of one file pays the declaration scan once."""
|
||||||
|
out = []
|
||||||
for number, line in enumerate(_code_lines(text), start=1):
|
for number, line in enumerate(_code_lines(text), start=1):
|
||||||
declared = {
|
declared = {
|
||||||
next((group for group in match.groups() if group), "") for pattern in DECLARATION.values() if (match := pattern.match(line))
|
next((group for group in match.groups() if group), "") for pattern in DECLARATION.values() if (match := pattern.match(line))
|
||||||
}
|
}
|
||||||
if "{" in line or "=>" in line:
|
if "{" in line or "=>" in line:
|
||||||
declared.add(declarator_name(line))
|
declared.add(declarator_name(line))
|
||||||
|
out.append(CodeLine(number, line, frozenset(name for name in declared if name)))
|
||||||
|
return tuple(out)
|
||||||
|
|
||||||
|
|
||||||
|
def call_sites(text: str, names: Iterable[str], *, index: Sequence[CodeLine] | None = None) -> tuple[CallSite, ...]:
|
||||||
|
"""Return calls in line order, excluding declarations and comments, once per name and line.
|
||||||
|
|
||||||
|
``index`` is ``code_index(text)`` prepared by the caller; without it the file is scanned here."""
|
||||||
|
wanted = sorted(set(names) - RESERVED_FUNCTION_LABELS - {MODULE_FUNCTION}, key=lambda name: (-len(name), name))
|
||||||
|
if not wanted:
|
||||||
|
return ()
|
||||||
|
token = re.compile(r"(?<![A-Za-z0-9_$])(" + "|".join(re.escape(name) for name in wanted) + r")\s*\(")
|
||||||
|
result = []
|
||||||
|
for number, line, declared in index if index is not None else code_index(text):
|
||||||
seen = set()
|
seen = set()
|
||||||
for match in token.finditer(line):
|
for match in token.finditer(line):
|
||||||
name = match.group(1)
|
name = match.group(1)
|
||||||
@@ -420,16 +438,31 @@ def _read_source(root: Path, path: str) -> str:
|
|||||||
return resolved.read_text(encoding="utf-8", errors="replace")
|
return resolved.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def callers_of(root: Path, name: str, *, files: Iterable[str], texts: Mapping[str, str] | None = None) -> tuple[tuple[str, int], ...]:
|
def callers_of(
|
||||||
|
root: Path,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
files: Iterable[str],
|
||||||
|
texts: Mapping[str, str] | None = None,
|
||||||
|
index: MutableMapping[str, tuple[CodeLine, ...]] | None = None,
|
||||||
|
) -> tuple[tuple[str, int], ...]:
|
||||||
"""Return sorted call locations from the supplied repository-relative product files.
|
"""Return sorted call locations from the supplied repository-relative product files.
|
||||||
|
|
||||||
``texts`` holds file contents already read by the caller; files absent from it are read here."""
|
``texts`` holds file contents already read by the caller; files absent from it are read here. ``index`` is a
|
||||||
|
cache of prepared files the caller keeps across calls (one checkout, many names); it is filled on first use."""
|
||||||
root = Path(root)
|
root = Path(root)
|
||||||
|
|
||||||
def text_of(path: str) -> str:
|
def text_of(path: str) -> str:
|
||||||
return texts[path] if texts is not None and path in texts else _read_source(root, path)
|
return texts[path] if texts is not None and path in texts else _read_source(root, path)
|
||||||
|
|
||||||
return tuple((path, call.line) for path in sorted(set(files)) for call in call_sites(text_of(path), (name,)))
|
def index_of(path: str) -> tuple[CodeLine, ...]:
|
||||||
|
if index is None:
|
||||||
|
return code_index(text_of(path))
|
||||||
|
if path not in index:
|
||||||
|
index[path] = code_index(text_of(path))
|
||||||
|
return index[path]
|
||||||
|
|
||||||
|
return tuple((path, call.line) for path in sorted(set(files)) for call in call_sites("", (name,), index=index_of(path)))
|
||||||
|
|
||||||
|
|
||||||
def facts_for(
|
def facts_for(
|
||||||
@@ -440,6 +473,7 @@ def facts_for(
|
|||||||
entry_points: Sequence[EntryPointRecord] | None = None,
|
entry_points: Sequence[EntryPointRecord] | None = None,
|
||||||
files: Sequence[str] | None = None,
|
files: Sequence[str] | None = None,
|
||||||
texts: Mapping[str, str] | None = None,
|
texts: Mapping[str, str] | None = None,
|
||||||
|
index: MutableMapping[str, tuple[CodeLine, ...]] | None = None,
|
||||||
) -> SourceFacts:
|
) -> SourceFacts:
|
||||||
"""Compute repository facts at a line; supplied entry points, product files and texts avoid repeated work.
|
"""Compute repository facts at a line; supplied entry points, product files and texts avoid repeated work.
|
||||||
|
|
||||||
@@ -456,7 +490,7 @@ def facts_for(
|
|||||||
|
|
||||||
entry_points = analyze_entry_points(root, [build_file_target(root, root / file) for file in files])
|
entry_points = analyze_entry_points(root, [build_file_target(root, root / file) for file in files])
|
||||||
return SourceFacts(
|
return SourceFacts(
|
||||||
language, function, bounds(spans, function), callers_of(root, function, files=files, texts=texts), tuple(entry_points)
|
language, function, bounds(spans, function), callers_of(root, function, files=files, texts=texts, index=index), tuple(entry_points)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -148,3 +148,17 @@ def test_facts_for_reuses_supplied_product_files_and_texts(tmp_path, monkeypatch
|
|||||||
(tmp_path / "other.py").write_text("") # the supplied text, not the disk, is what callers_of reads
|
(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)
|
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),)
|
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),)
|
||||||
|
|||||||
Reference in New Issue
Block a user