Skip to content

Config-read literal tier: J_USES_CONFIG and J_READS_CONFIG_UNRESOLVED #232

Description

@rahlk

Plan (optional)

Spec: codellm-devkit/.githubdocs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md. Decisions D30/D31 in .claude/SCHEMA_DECISIONS.md.

Problem

The analyzer projects 9,688 :ConfigKey nodes and no relationship saying which code reads one, so DEFINES_CONFIG is the only config relationship in a Java graph. The keys are known; the readers are not. artifacts/ConfigKeys.java only flattens config-bearing artifacts into declarations — there is no config-use analysis in the analyzer at all.

codeanalyzer-python has had this since its #162: a detection-and-resolution pass feeding app.config_uses / app.config_reads_unresolved, projected as PY_USES_CONFIG / PY_READS_CONFIG_UNRESOLVED.

Scope boundary

In scope: the literal tier, both new analysis.json lists, both new Neo4j relationship types, and the conformance-schema and catalog updates that go with them.

Out of scope: the L3/L4 dataflow tier (child 2 of the epic), the entrypoint report (child 3), python-sdk accessors (child 4, deferred), comment projection and CRUD (see the epic's scope guard).

Goals

  • JConfigUseEdge{src, dst, prov} and JConfigRead{site, callee, key, reason, prov} on the schema model, and application.config_uses[] / application.config_reads_unresolved[] in analysis.json
  • Call-site detectors: System.getenv, Environment.getProperty, Properties.getProperty, keyed on receiver_type + method_name, reading the literal from argument_expr
  • Annotation detectors: @Value("${x}") on a field or parameter, @ConfigurationProperties on a class
  • Resolver closing a decoded literal against the declared :ConfigKey set by namespace preference order, emitting one edge per matching key — a key legitimately appears in both application.properties and application-dev.properties, and a @ConfigurationProperties prefix claims every key beneath it. Only a zero-match read becomes a JConfigRead, with reason of undefined-key (a literal nobody declares) or non-literal (never closed on a literal)
  • J_USES_CONFIG in V2SchemaCatalog, from: [JBodyNode, JCallable, JField, JType] → to: [ConfigKey], props {prov}
  • J_READS_CONFIG_UNRESOLVED, JApplication → JExternal, props {key, reason, prov, _k}, with the callee ghost minted at detection time
  • analysis.v2.schema.json admits both new application-level members
  • Tier runs at L1

Caveats and known risks

  • src is a union endpoint set, wider than python's single PyBodyNode. This is the locked divergence (spec §4 D1): Java's dominant config-read idiom is an annotation with no call site. A consumer must not assume the source of a config-read edge is a body node.
  • The _k discriminant on J_READS_CONFIG_UNRESOLVED is load-bearing. One callee (System.getenv) legitimately reads many undeclared keys across a codebase; without _k = "<key>|<reason>" a plain endpoint-pair MERGE collapses them onto one relationship and keeps only the last SET. Python hit this exact hazard.
  • The ghost must be minted, not looked up. :JExternal rows come from the L2 external-symbol set, so a lookup would make unresolved reads invisible at L1 and absent whenever --external-calls is off — precisely the runs where the literal tier is the only tier there is.
  • The graph contract version does not move. V2SchemaCatalog.SCHEMA_VERSION stays 2.0.0; a re-baseline is Epic: scope every destructive Neo4j statement on the can:// id prefix; retire _module .github#50. So this relationship is undetectable from schema_version alone — by design, and recorded in the spec.
  • @Value placeholders can carry defaults (${x:fallback}) and nesting. Decode the key before matching, and treat a placeholder that does not close on a single key as a non-literal read rather than dropping it.

Definition of done

  • On a daytrader8 projection, J_USES_CONFIG is non-empty; every dst resolves to a projected :ConfigKey and every src to a projected :JBodyNode/:JCallable/:JField/:JType. Zero dangling endpoints, checked by query, not by eye.
  • A fixture with a @Value("${a.b}") field, a System.getenv("PATH") call, a getenv of an undeclared key, and a getenv(someVariable) yields, respectively: one J_USES_CONFIG from :JField, one from :JBodyNode, one J_READS_CONFIG_UNRESOLVED with reason="undefined-key", and one with reason="non-literal".
  • Two unresolved reads through the same callee with different keys survive as two distinct relationships (the _k check).
  • Runs and produces edges at -a 1, and with --external-calls off.
  • The L1 conformance gate stays green; analysis.v2.schema.json validates an -a 1 payload carrying both new lists.
  • --emit schema output includes both new relationship types with the endpoint sets above.

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