Skip to content

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

Description

@rahlk

Plan (optional)

Spec: codellm-devkit/.githubdocs/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 today

codeanalyzer-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 (VariableDeclarationExpr with one declarator, or AssignExpr onto a NameExpr).
  • 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.
  • ConfigUses takes the analysis level, module source, and the L2 call graph it now needs.

Caveats and known risks

  • The ssa filter is what keeps -a 3 ⊆ -a 4 true, and it is not optional. At -a 4, L4WalaOverlays.java:201 adds points-to to 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 a name = "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 call body 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 common return env.getProperty(key); nesting without special-casing either.
  • SdgPortEdges also emits prov:["ssa"] ddg edges, whose endpoints are synthetic @formal_in/@actual_out vertices 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 1 reports it non-literal and -a 3 resolves it with prov: ["dataflow"].
  • On a fixture whose key is a parameter with two callers passing the same literal, -a 4 resolves 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 keep prov: ["literal"] at every level.
  • Running -a 4 with --l3-engine wala (which adds points-to prov) resolves the same set as -a 3 plus interproc — never fewer.
  • analysis.v2.schema.json still validates a -a 4 payload; the L1/L3/L4 conformance gates stay green.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions