Files
Marius CiepluchandClaude Fable 5.1 4063fd1562 feat(semantic): the hunter's semantic producer covers Java and PHP
Java parsed already, but the producer failed every Java row because no obligation facts existed for the
language and that failure took roles and def_use down with it. PHP had no grammar. Now: tree-sitter-php in the
semantic extra with PHP node kinds in the CST reader; obligation facts for Jenkins/Stapler, Spring Security and
Servlet code (java.toml) and for Laravel, WordPress and Symfony (php.toml), each header naming the training
clones and references the names were checked against; a language with an IR but no obligation facts keeps
roles and def_use and records obligations as unchecked (None), which the engineered features treat as missing,
never as zero. Proven on the committed pool: 6 of 6 Java rows and 33 of 33 PHP rows of one fix change each now
carry semantic features. Implemented by Codex; gated here with ruff, mypy and the full suite.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 09:35:16 +02:00

140 lines
5.3 KiB
Python

"""Extra-on CST walker and overlay. Skips when wheels are absent."""
from __future__ import annotations
from pathlib import Path
import pytest
from openultrasast.findings import quick_scan_findings
from openultrasast.preprocess import preprocess_repository
from openultrasast.rank import rank_targets
from openultrasast.semantic import adjudicate, has_semantic_extra
from openultrasast.semantic.ir import parse_file
pytestmark = pytest.mark.semantic
def _require_extra() -> None:
if not has_semantic_extra():
pytest.skip("requires openultrasast[semantic]")
def test_javascript_eval_query_is_tree_sitter_call() -> None:
_require_extra()
ir = parse_file("app.js", "eval(req.query.x)\n", "javascript")
assert ir.parse_ok is True
assert ir.engine == "tree-sitter"
calls = [call for function in ir.functions for call in function.calls]
eval_calls = [call for call in calls if call.name == "eval"]
assert eval_calls
assert any("req.query" in arg for arg in eval_calls[0].arg_texts)
def test_broken_javascript_is_parse_failed() -> None:
_require_extra()
ir = parse_file("app.js", "eval(req.query.x\n", "javascript")
assert ir.parse_ok is False
assert ir.reason == "parse_failed"
def test_javascript_eval_promotes(tmp_path: Path) -> None:
_require_extra()
root = tmp_path / "repo"
root.mkdir()
(root / "app.js").write_text("eval(req.query.x)\n")
_, targets = preprocess_repository(root)
findings = quick_scan_findings(root, targets, rank_targets(targets))
records = adjudicate(root=root, targets=targets, findings=findings)
eval_records = [record for record in records if "js-eval" in record.proposal_id]
assert eval_records
assert eval_records[0].disposition == "promote"
assert eval_records[0].evidence_level == "static_corroboration"
assert eval_records[0].engine == "tree-sitter"
def test_c_constant_system_demotes(tmp_path: Path) -> None:
_require_extra()
root = tmp_path / "repo"
root.mkdir()
(root / "run.c").write_text('void f(void) { system("ls"); }\n')
_, targets = preprocess_repository(root)
findings = quick_scan_findings(root, targets, rank_targets(targets))
records = adjudicate(root=root, targets=targets, findings=findings)
hits = [record for record in records if record.path == "run.c"]
assert hits
assert all(record.disposition != "unadjudicated" or record.reason != "language_unsupported" for record in hits)
demoted = [record for record in hits if record.disposition == "demote"]
assert demoted
assert demoted[0].dominating_fact == "constant"
assert demoted[0].evidence_level == "static_corroboration"
def test_php_functions_binds_calls_and_parameters(tmp_path: Path) -> None:
_require_extra()
from tree_sitter import Parser
from openultrasast.semantic.cst import _KIND, _walk
from openultrasast.semantic.extra import grammar_for
text = """<p>Text mode</p>
<?php
class Example {
public function run(?Thing $x, &$y, ...$rest) {
$value = $this->g($y);
$value .= "hello $x";
$allowed = Gate::authorize('read', $x);
$this?->g($y);
}
}
function free_fn($x) { return consume($x); }
$closure = function ($z) { return consume($z); };
$arrow = fn($a) => consume($a);
"""
path = tmp_path / "sample.php"
path.write_text(text)
data = path.read_bytes()
assert len(data) == len(text.encode()) > 0
tree = Parser(grammar_for("php")).parse(data)
assert not tree.root_node.has_error
seen = {node.type for node in _walk(tree.root_node)}
# Exercise every registered kind against the installed grammar, including fn.
assert set().union(*_KIND["php"].values()) <= seen
ir = parse_file(path.name, data.decode(), "php")
assert ir.parse_ok and ir.engine == "tree-sitter"
assert [(fn.name, fn.start_line, fn.end_line, fn.params) for fn in ir.functions] == [
("<module>", 1, 14, ()),
("run", 4, 9, ("$x", "$y", "$rest")),
("free_fn", 11, 11, ("$x",)),
("<anon>", 12, 12, ("$z",)),
("<anon>", 13, 13, ("$a",)),
]
module, method, free, closure, arrow = ir.functions
assert module.calls == ()
assert [b.name for b in module.binds] == ["$closure", "$arrow"]
assert [(b.name, b.line, b.names, b.call_name) for b in method.binds] == [
("$value", 5, ("$this", "$y"), "g"),
("$value", 6, ("$value", "$x"), None),
("$allowed", 7, ("$x",), "authorize"),
]
assert [(c.name, c.line, c.arg_texts, c.arg_names) for c in method.calls] == [
("g", 5, ("$y",), (("$y",),)),
("authorize", 7, ("'read'", "$x"), ((), ("$x",))),
("g", 8, ("$y",), (("$y",),)),
]
assert method.calls[1].arg_is_constant == (True, False)
for fn in (free, closure, arrow):
assert [call.name for call in fn.calls] == ["consume"]
assert fn.calls[0].arg_names == (fn.params,)
def test_php_interpolation_and_named_arguments() -> None:
_require_extra()
ir = parse_file("sample.php", '<?php function f($x) { $s = "hello $x"; consume(value: "hello", other: "$x"); }', "php")
assert ir.parse_ok
fn = ir.functions[1]
assert fn.binds[0].names == ("$x",) and not fn.binds[0].is_constant
assert fn.calls[0].arg_texts == ('"hello"', '"$x"')
assert fn.calls[0].arg_names == ((), ("$x",))
assert fn.calls[0].arg_is_constant == (True, False)