mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(prover): delivery D: control flow decides dominance; the trace carries each guard's relation, effect and the operation's control context
Opus agent delivery under spec access-control-prover, requirement 10. `semantic/cfg.py` builds a control-flow graph per function from the syntax tree (if/else, switch arms, loops, try/catch/finally, return, throw, break, continue, exit, assert) with dominators and a per-line control context, and leaves a function without a graph rather than a wrong one. `obligations/cfg_dominance.py::CfgDominance` discharges a statement guard only when it lies on every path to the operation and the operation is unreachable from its failing arm; a predicate whose result nothing consumes is `value_only` and never discharges; router, decorator and earlier-hop witnesses behave as before; without a graph the order rule applies and `cfg_unavailable` is recorded. The trace's guards carry `relation` and `effect`, the trace carries `operation_context`; Java parameter lists carry names only; a handler-named operation's arguments are the handler's parameters. The checker's default stays order-based. Smoke on the 23 fix changes: 15 caught, 55 of 93 locations, precision 15 of 15, no failure, no change over 120 s (unchanged from delivery B). Gate: 3575 tests pass; the reserved-repository guard passes on the export. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6c3d4a7d72
commit
03b114cbeb
@@ -295,3 +295,72 @@ def test_remix_missing_kinds_and_public_policy(tmp_path, public, expected_missin
|
||||
change["files"] = [{"path": "app/routes/items.ts", "head_lines": [[1, 1]]}]
|
||||
result = prove_change(root, change)
|
||||
assert [finding["missing"] for finding in result.findings] == expected_missing
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"guard,findings,relation,effect",
|
||||
[
|
||||
(
|
||||
[" if (shared) {", " Jenkins.get().checkPermission(Jenkins.ADMINISTER);", " }"],
|
||||
1,
|
||||
"some_paths",
|
||||
"throws",
|
||||
),
|
||||
(
|
||||
[" if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {", ' throw new AccessDeniedException("denied");', " }"],
|
||||
0,
|
||||
"all_paths",
|
||||
"throws",
|
||||
),
|
||||
([" boolean allowed = Jenkins.get().hasPermission(Jenkins.ADMINISTER);"], 1, "all_paths", "value_only"),
|
||||
],
|
||||
)
|
||||
def test_control_flow_decides_the_verdict_and_the_trace(tmp_path, guard, findings, relation, effect):
|
||||
body = "\n ".join(guard)
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
f"""class X extends Descriptor<Thing> {{
|
||||
public Object doTestConnection(@QueryParameter boolean shared) {{
|
||||
{body}
|
||||
return CredentialsProvider.lookupCredentials(Thing.class);
|
||||
}}
|
||||
}}""",
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == findings
|
||||
assert result.coverage.operations_discharged == 1 - findings
|
||||
trace = result.traces[0]
|
||||
assert trace["parameters"] == ["shared"]
|
||||
assert [(g["relation"], g["effect"]) for g in trace["hops"][0]["guards_before_call"]] == [(relation, effect)]
|
||||
assert not [item for item in result.degradations if item.get("reason") == "cfg_unavailable"]
|
||||
if relation == "some_paths":
|
||||
assert trace["missing"] == "path_guard" and trace["operation_context"]["conditions"] == []
|
||||
|
||||
|
||||
def test_handler_operation_trace_carries_handler_parameters(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"""class X extends Descriptor<Thing> {
|
||||
public Object doCheckValue(@QueryParameter String value, @AncestorInPath Item item) {
|
||||
return 1;
|
||||
}
|
||||
}""",
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert result.traces[0]["arguments"] == [["value", "request_input"], ["item", "request_input"]]
|
||||
|
||||
|
||||
def test_unmodelled_function_is_judged_by_order_and_recorded(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"""class X extends Descriptor<Thing> {
|
||||
public Object doTestConnection() {
|
||||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||||
outer: for (;;) { break outer; }
|
||||
return CredentialsProvider.lookupCredentials(Thing.class);
|
||||
}
|
||||
}""",
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert not result.findings and result.coverage.operations_discharged == 1
|
||||
assert any(item.get("reason") == "cfg_unavailable" and item.get("function") == "doTestConnection" for item in result.degradations)
|
||||
|
||||
@@ -239,3 +239,18 @@ export default function Items() { return <div>Items</div>; }
|
||||
},
|
||||
)
|
||||
assert [row.function_name for row in rows] == ["action"]
|
||||
|
||||
|
||||
def test_java_parameters_are_names_without_annotations():
|
||||
from openultrasast.semantic.ir import parse_file
|
||||
|
||||
text = """class Exposed extends Descriptor<Thing> {
|
||||
public Object doCheckUrl(@QueryParameter("url") final String value, @AncestorInPath Item item, StaplerRequest2 req, String... rest) {
|
||||
return FormValidation.ok();
|
||||
}
|
||||
}
|
||||
"""
|
||||
ir = parse_file("Fixture.java", text, "java")
|
||||
if not ir.parse_ok:
|
||||
pytest.skip("no Java grammar installed")
|
||||
assert [fn.params for fn in ir.functions if fn.name == "doCheckUrl"] == [("value", "item", "req", "rest")]
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""CfgDominance: a guard counts only when control flow keeps the operation from running after it fails."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.semantic.cfg import FunctionCfg
|
||||
from openultrasast.semantic.cst import parse_function_nodes
|
||||
from openultrasast.semantic.extra import grammar_for
|
||||
from openultrasast.semantic.obligations.cfg_dominance import CfgDominance, describe, operation_context
|
||||
from openultrasast.semantic.obligations.dominance import Dominance, OrderDominance
|
||||
from openultrasast.semantic.obligations.operations import Discharge, Operation
|
||||
from openultrasast.semantic.obligations.paths import PathRecord
|
||||
|
||||
PATH = "T.java"
|
||||
|
||||
|
||||
def setup(body, kind="ownership_check", marker="can("):
|
||||
if grammar_for("java") is None:
|
||||
pytest.skip("no Java grammar installed")
|
||||
text = "class T {\n Object handle(Item item) {\n" + "".join(f" {line}\n" for line in body) + " }\n}\n"
|
||||
assert len(text.encode()) > 0
|
||||
nodes = parse_function_nodes(PATH, text, "java")
|
||||
assert nodes, "the parser read no function"
|
||||
cfg = FunctionCfg.from_cst(nodes[0], "java")
|
||||
lines = text.splitlines()
|
||||
guard_line = next(number for number, line in enumerate(lines, 1) if marker in line)
|
||||
op_line = next(number for number, line in enumerate(lines, 1) if "read(" in line)
|
||||
witness = Discharge(PATH, guard_line, "handle", kind, "guard", "authenticated_context", "statement")
|
||||
operation = Operation(PATH, op_line, "handle", "protected_read", "read", None, ("path_guard", "ownership_check"))
|
||||
return CfgDominance({PATH: text}, {(PATH, "handle"): cfg}), witness, operation, text
|
||||
|
||||
|
||||
def test_protocol():
|
||||
dominance, *_ = setup(["read(item);"], marker="read(")
|
||||
assert isinstance(dominance, Dominance)
|
||||
|
||||
|
||||
def test_value_only_predicate_does_not_discharge():
|
||||
dominance, witness, operation, _ = setup(["boolean ok = can(item);", "return read(item);"])
|
||||
assert dominance.relation(witness, operation) == "all_paths"
|
||||
assert dominance.effect(witness, operation) == "value_only"
|
||||
assert not dominance.dominates(witness, operation, None)
|
||||
|
||||
|
||||
def test_same_predicate_guarding_a_throw_discharges():
|
||||
dominance, witness, operation, _ = setup(["if (!can(item)) {", " throw new AccessDeniedException();", "}", "return read(item);"])
|
||||
assert (dominance.relation(witness, operation), dominance.effect(witness, operation)) == ("all_paths", "throws")
|
||||
assert dominance.dominates(witness, operation, None)
|
||||
|
||||
|
||||
def test_operation_only_inside_passing_branch_discharges():
|
||||
dominance, witness, operation, _ = setup(["if (can(item)) {", " return read(item);", "}", "return null;"])
|
||||
assert dominance.effect(witness, operation) == "returns"
|
||||
assert dominance.dominates(witness, operation, None)
|
||||
dominance, witness, operation, _ = setup(["if (can(item)) {", " read(item);", "}"])
|
||||
assert dominance.effect(witness, operation) == "branches"
|
||||
assert dominance.dominates(witness, operation, None)
|
||||
assert operation_context(dominance, operation).conditions == ("can(item)",)
|
||||
|
||||
|
||||
def test_branch_only_guard_is_some_paths_and_no_discharge():
|
||||
dominance, witness, operation, text = setup(
|
||||
["if (item.isShared()) {", " checkPermission(ADMINISTER);", "}", "return read(item);"], kind="path_guard", marker="checkPermission"
|
||||
)
|
||||
assert dominance.relation(witness, operation) == "some_paths"
|
||||
assert dominance.effect(witness, operation) == "throws"
|
||||
assert not dominance.dominates(witness, operation, None)
|
||||
# The order-based default accepts the same guard: it reads only that the guard comes first.
|
||||
assert OrderDominance({PATH: text}).dominates(witness, operation, None)
|
||||
|
||||
|
||||
def test_throwing_guard_in_swallowing_try_does_not_dominate():
|
||||
dominance, witness, operation, _ = setup(
|
||||
["try {", " checkPermission(ADMINISTER);", "} catch (AccessDeniedException e) {", " log(e);", "}", "return read(item);"],
|
||||
kind="path_guard",
|
||||
marker="checkPermission",
|
||||
)
|
||||
assert not dominance.dominates(witness, operation, None)
|
||||
|
||||
|
||||
def test_scopes_and_path_records_keep_their_semantics():
|
||||
dominance, witness, operation, _ = setup(["boolean ok = can(item);", "return read(item);"])
|
||||
for scope in ("decorator", "router"):
|
||||
scoped = Discharge(PATH, None, "handle", "path_guard", "guard", "authenticated_context", scope)
|
||||
assert dominance.dominates(scoped, operation, None)
|
||||
assert describe(dominance, scoped, operation, {}) == ("all_paths", "exits")
|
||||
caller = Discharge("Caller.java", 3, "entry", "path_guard", "guard", "authenticated_context", "statement")
|
||||
record = PathRecord((("Caller.java::entry", 4), (f"{PATH}::handle", None)), "Caller.java::entry", True)
|
||||
assert dominance.dominates(caller, operation, record)
|
||||
assert not dominance.dominates(caller, operation, None)
|
||||
later = PathRecord(((f"{PATH}::handle", 3), ("Caller.java::entry", None)), f"{PATH}::handle", True)
|
||||
assert not dominance.dominates(caller, operation, later)
|
||||
|
||||
|
||||
def test_missing_graph_falls_back_to_order_and_is_recorded():
|
||||
text = "class T {\n Object handle(Item item) {\n checkPermission(ADMINISTER);\n return read(item);\n }\n}\n"
|
||||
witness = Discharge(PATH, 3, "handle", "path_guard", "guard", "authenticated_context", "statement")
|
||||
operation = Operation(PATH, 4, "handle", "protected_read", "read", None, ("path_guard",))
|
||||
dominance = CfgDominance({PATH: text}, {})
|
||||
assert dominance.dominates(witness, operation, None) == OrderDominance({PATH: text}).dominates(witness, operation, None)
|
||||
assert [(item["reason"], item["path"], item["function"]) for item in dominance.degradations] == [("cfg_unavailable", PATH, "handle")]
|
||||
unavailable = FunctionCfg(1, 6, False, "labelled_jump")
|
||||
dominance = CfgDominance({PATH: text}, {(PATH, "handle"): unavailable})
|
||||
assert dominance.dominates(witness, operation, None)
|
||||
assert dominance.degradations[0]["detail"] == "labelled_jump"
|
||||
assert not {"language", "framework", "repository"} & set(dominance.degradations[0])
|
||||
|
||||
|
||||
def test_order_description_without_graph():
|
||||
text = "def f():\n ok = can()\n if not ok:\n return\n read()\n"
|
||||
witness = Discharge("a.py", 2, "f", "ownership_check", "guard", "authenticated_context", "statement")
|
||||
operation = Operation("a.py", 5, "f", "protected_read", "read", None, ("ownership_check",))
|
||||
assert describe(OrderDominance({"a.py": text}), witness, operation, {"a.py": text}) == ("all_paths", "returns")
|
||||
assert operation_context(OrderDominance({"a.py": text}), operation) == operation_context(OrderDominance({}), operation)
|
||||
@@ -196,3 +196,63 @@ def test_guards_after_follow_return_order():
|
||||
missing="path_guard",
|
||||
)
|
||||
assert [guard.fact_id for guard in result.guards_after] == ["final-after", "middle-after", "entry-after"]
|
||||
|
||||
|
||||
def test_control_components_include_branch_guard_without_discharge():
|
||||
from openultrasast.semantic.cfg import FunctionCfg
|
||||
from openultrasast.semantic.cst import parse_function_nodes
|
||||
from openultrasast.semantic.obligations.cfg_dominance import CfgDominance
|
||||
|
||||
text = "def entry(item):\n if enabled:\n guard()\n read(item)\n"
|
||||
ir = parse_python_ast("app.py", text)
|
||||
facts = ObligationFacts("1", (OperationFact("read", "protected_read", "python", ("read",), None, ("path_guard",), "high"),), ())
|
||||
nodes = parse_function_nodes("app.py", text, "python")
|
||||
if not nodes:
|
||||
import pytest
|
||||
|
||||
pytest.skip("no Python grammar installed")
|
||||
dominance = CfgDominance({"app.py": text}, {("app.py", "entry"): FunctionCfg.from_cst(nodes[0], "python")})
|
||||
result = build_trace(
|
||||
path=PathRecord((("app.py::entry", None),), "app.py::entry", True),
|
||||
entry=SimpleNamespace(kind="route", trust_boundary="http_request", access_level="unknown"),
|
||||
operation=Operation("app.py", 4, "entry", "protected_read", "read", None, ("path_guard",)),
|
||||
irs={"app.py": (ir, text)},
|
||||
facts=facts,
|
||||
witnesses=(Discharge("app.py", 3, "entry", "path_guard", "guard", "authenticated_context", "statement"),),
|
||||
dominance=dominance,
|
||||
dominating=(),
|
||||
missing="path_guard",
|
||||
)
|
||||
guard = result.hops[0].guards_before_call[0]
|
||||
assert (guard.relation, guard.effect) == ("some_paths", "throws")
|
||||
assert result.operation_context.conditions == () and result.record()["operation_context"]["in_loop"] is False
|
||||
assert '"relation":"some_paths"' in result.text()
|
||||
assert '"operation_context":' in result.text().splitlines()[-2]
|
||||
|
||||
|
||||
def test_order_dominance_trace_names_relation_and_effect():
|
||||
record = trace().record()
|
||||
assert record["hops"][0]["guards_before_call"][0]["relation"] == "all_paths"
|
||||
assert record["hops"][0]["guards_before_call"][0]["effect"] == "throws"
|
||||
assert record["guards_after"][0]["relation"] == "after"
|
||||
assert record["operation_context"] == {"catches": [], "conditions": [], "in_loop": False, "in_try": False, "on_exception_path": False}
|
||||
|
||||
|
||||
def test_handler_operation_arguments_are_parameter_origins():
|
||||
text = "def entry(self, item):\n return item\n"
|
||||
ir = parse_python_ast("app.py", text)
|
||||
facts = ObligationFacts(
|
||||
"1", (OperationFact("handler", "protected_read", "python", (), None, ("path_guard",), "high", handlers=("entry",)),), ()
|
||||
)
|
||||
result = build_trace(
|
||||
path=PathRecord((("app.py::entry", None),), "app.py::entry", True),
|
||||
entry=SimpleNamespace(kind="route", trust_boundary="http_request", access_level="unknown"),
|
||||
operation=Operation("app.py", 1, "entry", "protected_read", "handler", None, ("path_guard",)),
|
||||
irs={"app.py": (ir, text)},
|
||||
facts=facts,
|
||||
witnesses=(),
|
||||
dominance=OrderDominance({"app.py": text}),
|
||||
dominating=(),
|
||||
missing="path_guard",
|
||||
)
|
||||
assert result.arguments == (("item", "request_input"),)
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"""The structural control-flow graph decides dominance per language and reports a line's control context."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.semantic.cfg import FunctionCfg
|
||||
from openultrasast.semantic.cst import parse_function_nodes
|
||||
from openultrasast.semantic.extra import grammar_for
|
||||
|
||||
SOURCES = {
|
||||
"java": (
|
||||
"T.java",
|
||||
"""class T {
|
||||
Object f(int x) {
|
||||
guardA();
|
||||
if (x > 0) {
|
||||
guardB();
|
||||
}
|
||||
for (int i = 0; i < x; i++) {
|
||||
guardC();
|
||||
}
|
||||
try {
|
||||
guardD();
|
||||
} catch (IOException e) {
|
||||
recoverE();
|
||||
}
|
||||
return opF();
|
||||
}
|
||||
}
|
||||
""",
|
||||
"x > 0",
|
||||
"IOException",
|
||||
),
|
||||
"php": (
|
||||
"t.php",
|
||||
"""<?php
|
||||
function f($x) {
|
||||
guardA();
|
||||
if ($x > 0) {
|
||||
guardB();
|
||||
}
|
||||
for ($i = 0; $i < $x; $i++) {
|
||||
guardC();
|
||||
}
|
||||
try {
|
||||
guardD();
|
||||
} catch (IOException $e) {
|
||||
recoverE();
|
||||
}
|
||||
return opF();
|
||||
}
|
||||
""",
|
||||
"$x > 0",
|
||||
"IOException",
|
||||
),
|
||||
"python": (
|
||||
"t.py",
|
||||
"""def f(x):
|
||||
guardA()
|
||||
if x > 0:
|
||||
guardB()
|
||||
for i in range(x):
|
||||
guardC()
|
||||
try:
|
||||
guardD()
|
||||
except IOError:
|
||||
recoverE()
|
||||
return opF()
|
||||
""",
|
||||
"x > 0",
|
||||
"IOError",
|
||||
),
|
||||
"javascript": (
|
||||
"t.js",
|
||||
"""function f(x) {
|
||||
guardA();
|
||||
if (x > 0) {
|
||||
guardB();
|
||||
}
|
||||
for (let i = 0; i < x; i++) {
|
||||
guardC();
|
||||
}
|
||||
try {
|
||||
guardD();
|
||||
} catch (e) {
|
||||
recoverE();
|
||||
}
|
||||
return opF();
|
||||
}
|
||||
""",
|
||||
"x > 0",
|
||||
"any",
|
||||
),
|
||||
"typescript": (
|
||||
"t.ts",
|
||||
"""function f(x: number): unknown {
|
||||
guardA();
|
||||
if (x > 0) {
|
||||
guardB();
|
||||
}
|
||||
for (let i = 0; i < x; i++) {
|
||||
guardC();
|
||||
}
|
||||
try {
|
||||
guardD();
|
||||
} catch (e) {
|
||||
recoverE();
|
||||
}
|
||||
return opF();
|
||||
}
|
||||
""",
|
||||
"x > 0",
|
||||
"any",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _require(language):
|
||||
if grammar_for(language) is None:
|
||||
pytest.skip(f"no grammar installed for {language}")
|
||||
|
||||
|
||||
def graph(language, path, text):
|
||||
_require(language)
|
||||
assert len(text.encode()) > 0
|
||||
nodes = parse_function_nodes(path, text, language)
|
||||
assert nodes, "the parser read no function"
|
||||
return FunctionCfg.from_cst(nodes[0], language)
|
||||
|
||||
|
||||
def line_of(text, marker):
|
||||
return next(number for number, line in enumerate(text.splitlines(), 1) if marker in line)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language", sorted(SOURCES))
|
||||
def test_dominance_and_context_per_language(language):
|
||||
path, text, condition, caught = SOURCES[language]
|
||||
cfg = graph(language, path, text)
|
||||
assert cfg.available, cfg.reason
|
||||
at = {marker: line_of(text, marker) for marker in ("guardA", "guardB", "guardC", "guardD", "recoverE", "opF")}
|
||||
# A guard before the unconditional return dominates it.
|
||||
assert cfg.dominates(at["guardA"], at["opF"])
|
||||
assert cfg.relation(at["guardA"], at["opF"]) == "all_paths"
|
||||
# A guard inside an if-branch does not dominate a statement after the branch.
|
||||
assert not cfg.dominates(at["guardB"], at["opF"])
|
||||
assert cfg.relation(at["guardB"], at["opF"]) == "some_paths"
|
||||
# A guard inside a loop body does not dominate a statement after the loop.
|
||||
assert not cfg.dominates(at["guardC"], at["opF"])
|
||||
# A guard in a try whose catch falls through does not dominate a statement after the try.
|
||||
assert not cfg.dominates(at["guardD"], at["opF"])
|
||||
assert cfg.relation(at["opF"], at["guardA"]) == "after"
|
||||
assert cfg.context(at["guardB"]).conditions == (condition,)
|
||||
assert cfg.context(at["guardC"]).in_loop and not cfg.context(at["opF"]).in_loop
|
||||
try_context = cfg.context(at["guardD"])
|
||||
assert try_context.in_try and try_context.catches == (caught,) and not try_context.on_exception_path
|
||||
assert cfg.context(at["recoverE"]).on_exception_path
|
||||
assert any(node.kind == "catch" for node in cfg.nodes)
|
||||
assert cfg.context(at["opF"]) == type(try_context)()
|
||||
|
||||
|
||||
def python_graph(body):
|
||||
text = "def f(x):\n" + "".join(f" {line}\n" for line in body)
|
||||
return graph("python", "t.py", text), text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"body,effect",
|
||||
[
|
||||
(["if not can(x):", " raise Denied()", "op(x)"], "throws"),
|
||||
(["if not can(x):", " return None", "op(x)"], "returns"),
|
||||
(["if can(x):", " op(x)"], "branches"),
|
||||
(["if can(x):", " op(x)", "else:", " return None"], "returns"),
|
||||
(["for item in x:", " if not can(item):", " continue", " op(item)"], "exits"),
|
||||
(["if not can(x):", " sys.exit(1)", "op(x)"], "exits"),
|
||||
(["allowed = can(x)", "if not allowed:", " return None", "op(x)"], "returns"),
|
||||
(["allowed = can(x)", "op(x)"], "value_only"),
|
||||
(["can(x)", "op(x)"], "value_only"),
|
||||
(["assert can(x)", "op(x)"], "throws"),
|
||||
],
|
||||
)
|
||||
def test_failure_effect(body, effect):
|
||||
cfg, text = python_graph(body)
|
||||
assert cfg.available, cfg.reason
|
||||
assert cfg.effect(line_of(text, "can("), line_of(text, "op("), throwing=False) == effect
|
||||
|
||||
|
||||
def test_throwing_call_and_both_arms_reaching():
|
||||
cfg, text = python_graph(["check_permission(x)", "op(x)"])
|
||||
assert cfg.effect(line_of(text, "check_permission"), line_of(text, "op("), throwing=True) == "throws"
|
||||
cfg, text = python_graph(["if can(x):", " log()", "op(x)"])
|
||||
assert cfg.effect(line_of(text, "can("), line_of(text, "op("), throwing=False) == "value_only"
|
||||
|
||||
|
||||
def test_switch_arms_and_back_edges():
|
||||
text = """class T {
|
||||
void f(int k) {
|
||||
switch (k) {
|
||||
case 1:
|
||||
guardA();
|
||||
break;
|
||||
default:
|
||||
other();
|
||||
}
|
||||
do {
|
||||
guardB();
|
||||
} while (k > 0);
|
||||
opC();
|
||||
}
|
||||
}
|
||||
"""
|
||||
cfg = graph("java", "T.java", text)
|
||||
assert cfg.available
|
||||
assert not cfg.dominates(line_of(text, "guardA"), line_of(text, "opC"))
|
||||
assert cfg.context(line_of(text, "guardA")).conditions == ("case 1 of k",)
|
||||
# A do body runs at least once, so its first statement dominates what follows the loop.
|
||||
assert cfg.dominates(line_of(text, "guardB"), line_of(text, "opC"))
|
||||
assert cfg.context(line_of(text, "guardB")).in_loop
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"language,path,text",
|
||||
[
|
||||
("java", "T.java", "class T { void f() { outer: for (;;) { for (;;) { break outer; } } } }"),
|
||||
("javascript", "t.js", "function f() { x = cond ? 1 : 2; outer: for (;;) { continue outer; } }"),
|
||||
("php", "t.php", "<?php function f() { while (true) { while (true) { break 2; } } }"),
|
||||
("php", "t.php", "<?php function f($x) { $y = $x ?? throw new Exception(); }"),
|
||||
],
|
||||
)
|
||||
def test_unmodelled_construct_leaves_no_graph(language, path, text):
|
||||
cfg = graph(language, path, text)
|
||||
assert not cfg.available and cfg.reason
|
||||
assert cfg.node_at(1) is None and not cfg.dominates(1, 1)
|
||||
|
||||
|
||||
def test_language_without_builder_is_unavailable():
|
||||
_require("c")
|
||||
nodes = parse_function_nodes("t.c", "int f(void) { return 1; }\n", "c")
|
||||
assert nodes
|
||||
assert FunctionCfg.from_cst(nodes[0], "c").reason == "no_control_flow_builder"
|
||||
|
||||
|
||||
def test_line_after_function_names_exit():
|
||||
cfg, text = python_graph(["guard()", "if x:", " return 1", "return 2"])
|
||||
assert cfg.node_at(cfg.end_line + 1) == FunctionCfg.EXIT
|
||||
assert cfg.dominates(line_of(text, "guard"), cfg.end_line + 1)
|
||||
assert not cfg.dominates(line_of(text, "return 2"), cfg.end_line + 1)
|
||||
Reference in New Issue
Block a user