Skip to content

codeanalyzer-java: L1 v2 tree emission #180

Description

@sinha108

Plan (optional)

Design spec: https://github.com/codellm-devkit/codeanalyzer-java/blob/main/docs/design/specs/schema-v2-l3-l4-design.md

Problem

The analyzer emits the legacy v1 tree (combinedJsonObject.add("symbol_table", …), CodeAnalyzer.java) with JTypeis_* booleans, per-callable code, and flat start_line/end_line. Canonical schema v2 requires an additive CPG tree: an envelope, per-module source blob, byte-offset spans, can:// ids, structured decorators, nested metrics/refs, and bodycall nodes.

Scope boundary

L1 tree emission only — no call_graph edges (that is the L2 issue), no dataflow. Keep the JavaParser compute guts; rewrite only the emission layer. Both emitters remain selectable during the transition so each can serve as an oracle for the other. Pure structural emission — no analysis-engine changes.

Goals

  • Envelope {schema_version:"2.0.0", language:"java", max_level, analyzer{name,version}, application{id,kind,symbol_table}}
  • module{id, kind:"module", span, source, package, imports, types, content_hash}; byte offsets on every span — functions is omitted: Java has no module-level callables, and absence encodes "no fact"
  • type.kind ∈ {class,interface,enum,record,annotation}; base_types/interfaces; structured decorators:[{name,args,span}] (D2) — the nesting{parent?,is_local?} object was dropped: nesting is encoded by containment plus the id path (D4, refined)
  • callable{id, kind, signature, span, parameters, return_type, error_channel, modifiers, decorators, metrics{cyclomatic}, refs{types,fields}, body{}} (D1, D3); thrown_exceptions → error_channel
  • bodycall nodes {kind:"call", span, arguments:[local-id]}, keyed by bare local id — callee is absent at L1 rather than null, per the no-nulls convention, and appears when L2 resolves the site
  • can://java/<app>/<file>/<type>/<sig> id construction from signatureOf() (D8)
  • Both emitters selectable: --schema v2 opts into the canonical shape; v1 remains the default until the rest of the migration lands, so existing consumers are unaffected

Added during implementation (beyond the original goals)

  • Library type resolution: dependency jars are put on the symbol solver's path so third-party types resolve to qualified names (v2 was strictly worse than v1 without this); reflection restricted to the JRE so the analyzer's own dependencies cannot be resolved as the project's
  • Anonymous inner classes modelled as type nodes, both in callable bodies and in field initializers
  • callable.body_span so source[body_span.bytes] reproduces v1's per-callable code byte for byte
  • v1 parity for comments, local variables, enum constants, record components, initializer blocks (as kind:"initializer" callables), field initializers, parameter modifiers, declaration, code_start_line, is_implicit, entrypoint flags
  • Rich call-site facts on call nodes (receiver expression/type, argument types/expressions, callee signature, static/constructor flags)
  • Real-world fixture applications added as git submodules

Caveats and known risks

  • Span byte offsets depend on JavaParser token positions; degrade gracefully where absent.
  • symbol_table keys must be relative (never absolute or ..-prefixed).
  • Callable kinds: method, constructor, initializer (static/instance init blocks); lambda bodies stay part of their enclosing callable. is_variadic for varargs.

Definition of done

  • L1 gate: symbol_table non-empty with relative keys; a known module has source and the get_method_body slice matches; call nodes carry no resolved callee; can:// ids stable across two runs.
  • Output validates against a strict in-repo JSON Schema (src/test/resources/schema/analysis.v2.schema.json) rather than the SDK models, which do not exist for v2 yet — see the oracle decision in this issue's comments. Gate runs over in-repo fixtures on every change and over four real-world applications via ./gradlew realWorldConformanceTest.
  • Output is byte-identical across two runs on unchanged source.
  • Cache reuse on re-run-c/--cache-dir writes analysis_cache.json; modules whose files still hash to the same content_hash are reused, skipping the parse as well as the build (commons-lang: 130s cold, 4s warm). --eager forces a rebuild. Caching is opt-in.
  • Monotonicity L1 ⊆ L2 — blocked: cannot be exercised until L2 exists (codeanalyzer-java: L2 v2 call_graph (identity edges + external_symbols) #181); the check is vacuous with only L1 emitted. Not work remaining in this issue.
  • -j N byte-identical to -j 1 — not applicable yet: no parallel fan-out is implemented for v2, so the flag has nothing to vary. Ids are assigned from sorted source positions, and file/type/callable ordering is sorted throughout, specifically so parallelising later stays deterministic. Not work remaining in this issue.

Metadata

Metadata

Assignees

Labels

breakingBreaking ChangeenhancementNew feature or requestjavaPull requests that update java code

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions