Repository navigation
Description
Plan (optional)
Spec: codellm-devkit/.github → docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md (decision D30, tiers). Epic: codellm-devkit/.github#62. Builds on #232 / PR #233 — branch from that branch, not main.
Problem
#232 shipped the literal tier: a config read resolves when its key argument is written as a string literal at the call site. Everything else lands in config_reads_unresolved with reason="non-literal", which on a real codebase is the common shape:
String key = "spring.datasource.url";
env.getProperty(key); // non-literal today
String read(String name) { return System.getenv(name); } // non-literal todaycodeanalyzer-python closes both over its dataflow graph (its #162 Task 3), so a Java graph reports strictly fewer readers than a Python one for the same code shape.
Scope boundary
In scope: the two dataflow tiers over the existing L3/L4 overlays, and the prov bookkeeping that goes with them.
Out of scope: any new dataflow machinery — both tiers read the DDG and call graph the analyzer already emits. Also out: the entrypoint report (#234), python-sdk accessors (epic child 4, deferred), CRUD (#187).
Goals
- L3 intra tier. A key argument that is a bare name closes when every DDG-reaching definition of that name at the call site is the same single string literal. Filter
prov.contains("ssa"); require the DDG use node's span to contain the call node's span; accept only a single-target<name> = "literal"definition (VariableDeclarationExprwith one declarator, orAssignExpronto aNameExpr). - L4 interproc tier. A key that names a parameter of its enclosing callable closes when the parameter is never locally redefined and every call site targeting that callable supplies the same literal — directly, or through one hop of caller-side intra closure.
-
prov: ["dataflow"]on tier-produced edges; unresolved records carry every tier attempted, so["literal", "dataflow"]at L3+. - Tier gating: intra at
-a 3+, interproc at-a 4+. A read resolved at a lower tier is never recomputed. -
ConfigUsestakes the analysis level, module source, and the L2 call graph it now needs.
Caveats and known risks
- The
ssafilter is what keeps-a 3 ⊆ -a 4true, and it is not optional. At-a 4,L4WalaOverlays.java:201addspoints-toto the prov of existing edges and mints alias-widened ones. An alias edge can connect this variable's use to an unrelated write that is not aname = "literal"shape; the shape check then refuses it, and — since any non-closing reaching def kills resolution — that refusal would remove an edge the ssa-only L3 set resolved cleanly. Monotonicity broken by a widening, which is the exact failure the additive contract forbids. codeanalyzer-python documents hitting this. - Any non-closing reaching def must kill the resolution, not be skipped. Two paths assigning different literals means the read is genuinely ambiguous, and picking one would be a confident wrong answer.
- The span-containment check is load-bearing. The CFG/DDG is statement-level while a
callbody node is keyed by its own narrower span, so a def's recorded use site is the enclosing statement. Matching by containment covers both the bare-expression-statement case and the commonreturn env.getProperty(key);nesting without special-casing either. SdgPortEdgesalso emitsprov:["ssa"]ddg edges, whose endpoints are synthetic@formal_in/@actual_outvertices with no span. The span guard rejects them; do not relax it.- The interproc tier needs completeness of the call-site set. A callable reachable through a call edge the analyzer did not resolve must not close, or a caller supplying a different key is silently ignored.
- A read the intra tier closed to a literal that matches no declared key must not be re-derived from callers by the interproc tier: the local rebinding is what the callee actually reads, and the caller's argument would misattribute.
Definition of done
- On a fixture carrying
String k = "server.port"; env.getProperty(k);,-a 1reports itnon-literaland-a 3resolves it withprov: ["dataflow"]. - On a fixture whose key is a parameter with two callers passing the same literal,
-a 4resolves it; with two callers passing different literals, it stays unresolved at every level. - A local reassigned to two different literals on two paths stays unresolved — no arbitrary pick.
- Monotonicity on a fixture that carries a literal read, an intra-closable read and an interproc-closable read:
config_uses(-a 1) ⊆ config_uses(-a 3) ⊆ config_uses(-a 4), checked as set containment of(src, dst)pairs, and the literal-tier edges keepprov: ["literal"]at every level. - Running
-a 4with--l3-engine wala(which addspoints-toprov) resolves the same set as-a 3plus interproc — never fewer. analysis.v2.schema.jsonstill validates a-a 4payload; the L1/L3/L4 conformance gates stay green.
Activity
feat(config): dataflow tiers — close non-literal keys over the L3 DDG…