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]>
This commit is contained in:
Marius Ciepluch
2026-10-14 09:35:16 +02:00
co-authored by Claude Fable 5.1
parent 6fe46c3062
commit 4063fd1562
13 changed files with 354 additions and 9 deletions
+69
View File
@@ -68,3 +68,72 @@ def test_c_constant_system_demotes(tmp_path: Path) -> None:
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)