Skip to content

feat(config): dataflow tiers — close non-literal keys over the L3 DDG and L4 call graph - #237

Merged
rahlk merged 1 commit into
mainfrom
feat/issue-236-config-dataflow-tiers
Sep 7, 2026
Merged

feat(config): dataflow tiers — close non-literal keys over the L3 DDG and L4 call graph#237
rahlk merged 1 commit into
mainfrom
feat/issue-236-config-dataflow-tiers

Conversation

@rahlk

@rahlk rahlk commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #236. Stacked on #233 (base is feat/issue-232-config-read-literal-tier, not main). Spec: codellm-devkit/.github docs/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:

String key = "spring.datasource.url";
env.getProperty(key);                                       // L3 closes this

String read(String name) { return System.getenv(name); }    // L4 closes this

Two tiers, gated on the overlays each needs, 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.
  • 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.

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:

  1. Any non-closing reaching def kills the resolution, rather than being skipped. Two paths assigning different literals means the read is genuinely ambiguous, and picking one would be a confident wrong answer.
  2. The ssa filter keeps -a 3 ⊆ -a 4 true, and is not optional. At -a 4, L4WalaOverlays.java:201 adds points-to prov and mints alias-widened edges. An alias edge can connect this variable's use to an unrelated write that is not a name = "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.
  3. An incomplete call-site set does not close. 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 python's tier makes.

Two details worth a reviewer's eye

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 without special-casing either.

prov means 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 1 vs -a 4 --no-build --no-rta:

edge set -a 4 is a superset of -a 1 — nothing lost
literal edges 15, still prov: ["literal"] at level 4
unresolved 12, moved to prov: ["literal", "dataflow"]
spurious edges none — the interprocedural tier added nothing across the corpus
schema -a 4 payload validates

ConfigDataflowTierTest (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 — CodeAnalyzerIntegrationTest cannot reach a Docker daemon here and fails identically on a clean main.

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 wala was 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.

… 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
rahlk force-pushed the feat/issue-236-config-dataflow-tiers branch from 5bdc470 to 288fecd Compare September 7, 2026 22:27
@rahlk
rahlk changed the base branch from feat/issue-232-config-read-literal-tier to main September 7, 2026 22:27
@rahlk
rahlk merged commit b010151 into main Sep 7, 2026
@rahlk rahlk mentioned this pull request Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Config-read dataflow tiers: close non-literal keys over the L3 DDG and the L4 call graph

1 participant