feat(config): dataflow tiers — close non-literal keys over the L3 DDG and L4 call graph - #237
Merged
Merged
Conversation
… and L4 call graph
The literal tier resolves a config read only when the key is written as a string
literal at the call site. Everything else landed in `config_reads_unresolved`
with `reason="non-literal"`, which on real code is the common shape:
String key = "spring.datasource.url";
env.getProperty(key);
String read(String name) { return System.getenv(name); }
codeanalyzer-python closes both over its dataflow graph, so a Java graph reported
strictly fewer readers than a Python one for the same code.
Adds two tiers, gated on the overlays each needs and reading only what the
analyzer already emits — no new dataflow machinery:
- **L3 intra.** A bare-name key closes when every DDG-reaching definition at the
call site is the same single string literal. Only `prov` containing `ssa` is
consulted, and only a single-target `<name> = "literal"` shape closes.
- **L4 interprocedural.** A key naming a parameter closes when the callable never
rebinds it and every call site targeting it supplies the same literal —
directly, or through one caller-side hop of the intra tier.
Three refusals are as load-bearing as the closures, since a tier that closes a
read it should not have produces an edge claiming code reads a key it never
reads:
- Any non-closing reaching def kills the resolution rather than being skipped.
Two paths assigning different literals means the read is genuinely ambiguous.
- The `ssa` filter is what keeps `-a 3 ⊆ -a 4` true. At `-a 4` the ddg also
carries `points-to` edges that may-alias this variable's use to an unrelated
write, which is not a `name = "literal"` shape; since a non-closing def kills
resolution, letting an alias edge in would REMOVE an edge the ssa-only L3 set
resolved cleanly — a widening that breaks the additive contract.
- An incomplete call-site set does not close. A callee whose callers the analyzer
could not fully see may be handed a different key elsewhere. Known ceiling,
stated rather than papered over: a `public` method can be called from outside
the analyzed project, which no in-project call graph rules out — the same
whole-application assumption codeanalyzer-python's tier makes.
The DDG use site is matched by span CONTAINMENT, not id equality: the CFG/DDG is
statement-level while a `call` body node is keyed by its own narrower span, so a
def's recorded use site is the enclosing statement. Containment covers both the
bare-expression-statement case and the common `return env.getProperty(key);`
nesting.
An edge's `prov` is the tier that closed it, so a literal-tier edge still reads
`["literal"]` at `-a 4`. An unresolved record's `prov` is every tier attempted,
so it says how hard the analyzer tried before giving up.
Verified on daytrader8 at `-a 1` and `-a 4`: the edge set is a strict superset
(nothing lost), the 15 literal edges keep `prov: ["literal"]`, the 12 unresolved
reads move to `prov: ["literal", "dataflow"]`, and the payload validates. The
interprocedural tier produced no spurious edges across that corpus.
Closes #236
rahlk
force-pushed
the
feat/issue-236-config-dataflow-tiers
branch
from
September 7, 2026 22:27
5bdc470 to
288fecd
Compare
rahlk
changed the base branch from
feat/issue-232-config-read-literal-tier
to
main
September 7, 2026 22:27
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #236. Stacked on #233 (base is
feat/issue-232-config-read-literal-tier, notmain). Spec:codellm-devkit/.githubdocs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md(D30, tiers), epic codellm-devkit/.github#62.What
The literal tier resolves a read only when the key is a string literal at the call site. Everything else was
reason="non-literal", which on real code is the common shape:Two tiers, gated on the overlays each needs, reading only what the analyzer already emits — no new dataflow machinery:
The refusals are the interesting part
A tier that closes a read it should not have emits an edge claiming code reads a key it never reads — strictly worse than the unresolved record it replaced. Three guards, each with a paired test:
ssafilter keeps-a 3 ⊆ -a 4true, and is not optional. At-a 4,L4WalaOverlays.java:201addspoints-toprov and mints alias-widened edges. An alias edge can connect this variable's use to an unrelated write that is not aname = "literal"shape — and because a non-closing def kills resolution, letting one in would remove an edge the ssa-only L3 set resolved cleanly. A widening that breaks monotonicity. codeanalyzer-python documents hitting this first.publicmethod can be called from outside the analyzed project, which no in-project call graph rules out — the same whole-application assumption python's tier makes.Two details worth a reviewer's eye
Span containment, not id equality. 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. Containment covers both the bare-expression-statement case and the commonreturn env.getProperty(key);nesting without special-casing either.provmeans two different things by design. On an edge it is the tier that closed it, so a literal-tier edge still reads["literal"]at-a 4. On an unresolved record it is every tier attempted, so a failure says how hard the analyzer tried.Verification
daytrader8,
-a 1vs-a 4 --no-build --no-rta:-a 4is a superset of-a 1— nothing lostprov: ["literal"]at level 4prov: ["literal", "dataflow"]-a 4payload validatesConfigDataflowTierTest(11 tests) covers each closure paired with the ambiguous shape that must stay unresolved, the level chain as set containment, and provenance at every level.Full suite: 539 tests, 1 failure —
CodeAnalyzerIntegrationTestcannot reach a Docker daemon here and fails identically on a cleanmain.Not verified here
The tiers add nothing on the available corpora. daytrader8's config reads are all either literals or undeclared keys — it contains no non-literal read to widen — and the other checked-in fixtures (
plantsbywebsphere,cargotracker,commons-lang,spring-petclinic) have no config reads or no sources at all. So the corpus runs prove monotonicity and no false positives; the closures themselves are proven by fixtures only.--l3-engine walawas not exercised, since it needs a built project and these runs are--no-build. That is the configuration where guard 2 above actually matters, so it is worth running once against a real build before this merges.