Skip to content

Latest commit

 

History

History
80 lines (55 loc) · 4.9 KB

File metadata and controls

80 lines (55 loc) · 4.9 KB

ADR-004: @CopilotExperimental annotation processor — pure JSR 269 approach

Context and Problem Statement

The Java SDK needs a compile-time gate that prevents accidental use of experimental APIs (types and methods marked with @CopilotExperimental). The annotation processor must detect consumer-side references to experimental elements and emit a compilation error unless the consumer explicitly opts in with -Acopilot.experimental.allowed=true.

The fundamental question is: should the processor use the Compiler Tree API (com.sun.source.util.Trees, TreePathScanner) for full expression-level coverage, or restrict itself to standard JSR 269 (javax.lang.model.*) for portability at the cost of reduced detection scope?

Considered Options

Option 1: Compiler Tree API (com.sun.source.*)

Uses Trees.instance(processingEnv) and TreePathScanner to walk the full AST of every compilation unit, resolving symbols at expression level.

What it catches additionally:

  • new ExperimentalType() inside method bodies
  • ExperimentalType.staticMethod() inline calls
  • Method references (ExperimentalType::method)
  • Local variable types
  • Casts to experimental types

Drawbacks:

  • Depends on jdk.compiler module — ties the processor to javac specifically.
  • Does not work with ECJ (Eclipse Compiler for Java), which has its own AST.
  • Requires requires static jdk.compiler in module-info.java.
  • Requires --add-modules jdk.compiler --add-exports jdk.compiler/com.sun.source.util=ALL-UNNAMED --add-exports jdk.compiler/com.sun.source.tree=ALL-UNNAMED in surefire test configuration.
  • The com.sun.source.* package, while more stable than com.sun.tools.javac.*, is still not part of the Java SE specification. It is a JDK-specific API.

Option 2: Pure JSR 269 (javax.lang.model.*) — declaration-level only

Uses only standard annotation processing APIs to walk declared elements (types, methods, fields) and inspect their type mirrors.

What it catches:

  • Field types referencing experimental classes
  • Method parameter types
  • Method return types
  • Superclass / implemented interfaces
  • Thrown exception types
  • Generic type arguments and bounds

What it cannot catch:

  • new ExperimentalType() purely inside a method body with no declaration footprint
  • Inline static method calls with no stored result
  • Method references to experimental methods
  • Local variable types (not visible to processors)

Advantages:

  • Works with any compliant Java compiler (javac, ECJ, IntelliJ's compiler, etc.)
  • No dependency on JDK-internal modules
  • No --add-exports hacks in build configuration
  • Simpler module-info (no requires static jdk.compiler)
  • Easier to maintain and less fragile across JDK versions

Decision Outcome

Chosen: Option 2 — Pure JSR 269.

Rationale

  1. The SDK's experimental APIs are predominantly types (records, classes). Table apiNote from the codegen analysis shows 316 experimental types vs. 159 experimental methods. Any meaningful use of an experimental record (params, results, events) requires declaring it somewhere — a field, a method parameter, a return type, or a superclass. Pure body-level usage with zero declaration footprint is a degenerate edge case for this SDK.

  2. Portability matters for a published library. The SDK is distributed on Maven Central. Consumers may use Eclipse (ECJ), IntelliJ's compiler, or other toolchains where com.sun.source.* is unavailable. A processor that silently does nothing on non-javac compilers provides false confidence.

  3. Build simplicity. Avoiding jdk.compiler eliminates module-system friction: no requires static jdk.compiler, no --add-exports in surefire, no risk of IllegalAccessError on future JDK versions that further restrict internal APIs.

  4. The gap is well-documented and acceptable. The README explicitly lists what the processor does and does not catch, with suggested workarounds. This transparency is preferable to a fragile implementation with full coverage.

  5. Error Prone or similar tools can fill the gap later. If full expression-level enforcement becomes necessary in the future, it can be implemented as a separate Error Prone check (which is already designed for AST-level analysis) without changing the annotation or the processor's declaration-level behavior.

Consequences

  • Consumers who use experimental APIs only in fully-inline expressions (no field, no parameter, no return type) will not receive a compile error. This is expected and documented.
  • The processor works identically across javac, ECJ, and any JSR 269-compliant compiler.
  • No JDK-internal API dependency in the module descriptor or test infrastructure.
  • Future enhancement path is clear: add an optional Error Prone check for body-level coverage without changing the existing processor.

Related work items