Add @CopilotExperimental compile-time gate for experimental APIs#1601
Add @CopilotExperimental compile-time gate for experimental APIs#1601edburns wants to merge 7 commits into
@CopilotExperimental compile-time gate for experimental APIs#1601Conversation
## Summary
Introduces a compile-time enforcement mechanism that prevents accidental use of
experimental APIs in the Java SDK. Consumer code that references experimental
types or methods now fails to compile by default, with explicit opt-in via
`-Acopilot.experimental.allowed=true`.
## Motivation
Generated SDK APIs marked as `stability: "experimental"` in the schema were
previously only documented via `@apiNote` in Javadoc — invisible to the compiler
and easy to miss. This change makes the experimental contract enforceable at
build time, giving consumers a clear signal and an explicit opt-in path.
## What changed
### New files
| File | Purpose |
|------|---------|
| `java/src/main/java/com/github/copilot/CopilotExperimental.java` | Annotation: `@Documented`, `@Retention(CLASS)`, `@Target({TYPE, METHOD})` |
| `java/src/main/java/com/github/copilot/CopilotExperimentalProcessor.java` | Annotation processor using Trees API + `TreePathScanner` |
| `java/src/main/resources/META-INF/services/javax.annotation.processing.Processor` | Service registration for classpath-based discovery |
| `java/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java` | 3 tests proving fail-by-default and allow-when-opted-in |
### Modified files
| File | Change |
|------|--------|
| `java/src/main/java/module-info.java` | Added `requires static jdk.compiler;` and `provides` directive |
| `java/pom.xml` | Compiler plugin: `<proc>none</proc>` + `-Acopilot.experimental.allowed=true`; surefire args for `jdk.compiler` exports |
| `java/scripts/codegen/java.ts` | Emits `@CopilotExperimental` + import at 3 codegen sites |
| `java/src/generated/java/**` | Regenerated — experimental types/methods now annotated |
| `java/README.md` | New "Using experimental APIs" section with examples |
## How it works
### Processor behavior
1. Runs with `@SupportedAnnotationTypes("*")` — processes all compilation units.
2. Checks option `copilot.experimental.allowed`:
- If `"true"`: processor is a no-op (all experimental use permitted).
- Otherwise: scans source trees for forbidden references.
3. Uses `com.sun.source.util.Trees` + `TreePathScanner` to visit:
- `IdentifierTree` — direct type/variable references
- `MemberSelectTree` — qualified access (`obj.method`)
- `MemberReferenceTree` — method references (`Type::method`)
- `NewClassTree` — constructor calls (`new ExperimentalType()`)
4. For each resolved element, checks if it or its enclosing type carries
`@CopilotExperimental`. If so, emits `Diagnostic.Kind.ERROR`.
### Diagnostic message
```
error: Use of experimental API 'symbolName' is not allowed.
Add compiler option -Acopilot.experimental.allowed=true to opt in.
```
### Bootstrap handling
The processor lives in the same module it protects. To avoid the bootstrap
problem (javac trying to load the processor before it's compiled):
- Main compile uses `<proc>none</proc>` — annotation processing is disabled
during this module's own compilation.
- Consumer builds discover the processor via `META-INF/services` (classpath) or
`provides` directive (module path).
### Codegen integration
The TypeScript codegen (`java/scripts/codegen/java.ts`) was updated at three
sites where `stability === "experimental"` is detected:
1. **Session event classes** — `@CopilotExperimental` on the class
2. **RPC params/result records** — `@CopilotExperimental` on the class
3. **API wrapper methods** — `@CopilotExperimental` on individual methods
Import (`com.github.copilot.CopilotExperimental`) is added to the generated
file's import set only when needed. Existing `@apiNote` Javadoc is preserved.
## Key review points for a Java SME
1. **Trees API usage** — The processor uses only `com.sun.source.*` (exported
from `jdk.compiler`), not `com.sun.tools.javac.*` internals. This is the
standard supported API for source-level annotation processors.
2. **Module wiring** — `requires static jdk.compiler` means the dependency is
compile-time only; the module works at runtime without `jdk.compiler` on the
module graph. The `provides` directive enables modular service discovery.
3. **Surefire JVM args** — Tests that instantiate the processor directly need
`--add-modules jdk.compiler --add-exports jdk.compiler/com.sun.source.util=ALL-UNNAMED`
because surefire runs on the classpath (unnamed module) while `Trees` is in
`jdk.compiler`.
4. **proc=none trade-off** — The SDK module itself never runs its own processor.
Enforcement is consumer-facing only. This is intentional: the SDK must
compile its own generated experimental code without error.
5. **Retention=CLASS** — The annotation is not available via reflection at
runtime (`RUNTIME` is unnecessary), but is preserved in `.class` files so the
processor can read it from compiled dependencies on the classpath.
6. **Enclosing-type propagation** — When a type is annotated, all member
accesses through that type are treated as experimental, even if the member
itself is not annotated. This covers field access, method calls, and
constructor invocations on experimental types.
## Test coverage
| Test | Asserts |
|------|---------|
| `failsByDefault_whenReferencingExperimentalType` | ERROR diagnostic with "experimental API" for type usage |
| `failsByDefault_whenInvokingExperimentalMethod` | ERROR diagnostic for method-level annotation |
| `passes_whenOptInFlagIsProvided` | Zero errors when `-Acopilot.experimental.allowed=true` is set |
Tests use `javax.tools.JavaCompiler` with in-memory sources and explicit
`setProcessors()` to avoid environment sensitivity.
## How to verify
```bash
cd java
mvn generate-sources -Pcodegen # regenerate with annotations
mvn test -Dtest=CopilotExperimentalProcessorTest # run gate tests
```
622004b to
b7378ae
Compare
This comment has been minimized.
This comment has been minimized.
…m.sun APIs) Replace Trees/TreePathScanner-based implementation with standard javax.lang.model.* declaration-level checking. This removes the dependency on jdk.compiler and makes the processor portable across all Java compilers (javac, ECJ, etc.). - Remove requires static jdk.compiler from module-info - Remove --add-exports surefire hacks from pom.xml - Update tests to validate declaration-level detection - Document known limitations and workarounds in README - Add ADR-004 explaining the design decision and trade-offs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
modified: src/main/java/com/github/copilot/CopilotExperimentalProcessor.java - Address comment from John Oliver.
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-SDK Consistency ReviewThis PR adds Experimental API marking across SDKs
AssessmentThis PR brings the Java SDK to parity with .NET — both now offer compile-time enforcement in addition to the documentation markers that all SDKs already have. This is a consistency improvement, not a divergence. The differences in opt-in mechanism (Java: The codegen script (
|
Summary
This change adds
@CopilotExperimentaland a processor that rejectsunapproved references to experimental SDK APIs at compile time unless the
consumer opts in with
-Acopilot.experimental.allowed=true.What changed
New files
java/src/main/java/com/github/copilot/CopilotExperimental.javajava/src/main/java/com/github/copilot/CopilotExperimentalProcessor.javajava/src/main/resources/META-INF/services/javax.annotation.processing.Processorjava/docs/adr/adr-004-copilotexperimental.mdjava/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.javaUpdated files
java/scripts/codegen/java.ts@CopilotExperimentalalongside existing experimental@apiNoteJavadocsjava/src/generated/java/**java/pom.xmljava/src/main/java/module-info.javajava/README.mdHow it works
The processor is intentionally declaration-level only:
javax.lang.model.*.thrown types, and generic type usage.
@CopilotExperimental,compilation fails unless
-Acopilot.experimental.allowed=trueis set.This keeps the implementation portable across Java compilers while still
covering the main ways the SDK’s generated experimental APIs are used.
Important review points
@apiNoteJavadocs; the annotation is additive.Test coverage
-Acopilot.experimental.allowed=trueis presentOutcome
Experimental APIs are now opt-in at compile time, with a portable implementation and explicit documentation of the remaining limitations.