diff --git a/CLAUDE.md b/CLAUDE.md index 00d9062..20c7127 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ - `hierarchical-codemodel` — type hierarchy (ancestors, descendants, assignability) - `imperative-codemodel` — statement AST nodes (Block, If, While, Return) - `objectoriented-codemodel` — OOP traits (fields, methods, constructors, access modifiers) -- `jdk-codemodel` — JDK-backed impl via reflection (`JDKCodeModel`) or javac (`JdkInitializer`); symbol resolution, method resolution to `MethodDescriptor`, JPMS module-info parsing, `referencesTo()` API, source-fidelity traits (`LocationTrait`, `ImportDeclaration`, `InitializerBlockDescriptor`, `MemberTypeDescriptor`, `Varargs`) +- `jdk-codemodel` — JDK-backed impl via reflection (`JDKCodeModel`) or javac (`JdkInitializer`); shared `TypeMirrorResolver` (also used by `jdk-annotation-processor`); symbol resolution, method resolution to `MethodDescriptor`, JPMS module-info parsing, `referencesTo()` API, incremental `rescan()`, source-fidelity traits (`SourceLocation`, `ImportDeclaration`, `InitializerBlockDescriptor`, `MemberTypeDescriptor`, `Varargs`) - `dependency-injection` — custom JSR-330 DI built on `jdk-codemodel` - `codemodel-framework` — pipeline interfaces (Enricher, TypeChecker, Compiler, Completer) - `codemodel-framework-builder` — concrete `FrameworkBuilder` + `InternalFramework` diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index dce82a6..65945cc 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -1,12 +1,12 @@ --- -last_mapped: 2026-05-15T10:14:37Z -total_files: 438 +last_mapped: 2026-07-02T18:05:03Z +total_files: 446 total_modules: 11 --- # Codebase Map — `codemodel.build` -> Auto-generated by Cartographer. Last mapped: 2026-05-15 +> Auto-generated by Cartographer. Last mapped: 2026-07-02 ## System Overview @@ -88,7 +88,7 @@ codemodel.build/ | File | Purpose | |------|---------| | `CodeModel` | Root registry: creates/queries `TypeDescriptor`s, `ModuleDescriptor`s, `NamespaceDescriptor`s; `createTypeDescriptor(name, supplier, populate)` runs `populate` atomically inside `computeIfAbsent` | -| `AbstractCodeModel` | `ConcurrentHashMap`-backed impl with `HeapBasedCompositeIndex` for `Queryable.match()`; constructor now calls `this.index.index(this)` to ensure `@Indexable` fields are indexed consistently for both fresh and unmarshalled models | +| `AbstractCodeModel` | `ConcurrentHashMap`-backed impl with `HeapBasedCompositeIndex` for `Queryable.match()`; constructor now calls `this.index.index(this)` to ensure `@Indexable` fields are indexed consistently for both fresh and unmarshalled models; `createTypeDescriptor`/`createModuleDescriptor`/`createNamespaceDescriptor` run `populate` **inside** `computeIfAbsent` (atomic, no partial-population visible to other threads); subclass-facing `removeTypeDescriptor`/`removeModuleDescriptor` helpers call `index.unindex` | | `ConceptualCodeModel` | Default concrete impl; `@Inject`-annotated ctor; self-registers with `Marshalling` | | `CodeModelTraitable` | Internal trait bag: enforces `@Singular`/`@NonSingular` cardinality, indexes into heap; calls `reindexDynamic` after every `addTrait`/`removeTrait` mutation | | `TypeDescriptor` | Named-type definition node; `Traitable` + `Dependent` | @@ -137,9 +137,24 @@ Both delegate to `AbstractTypeUsage.render(nameRenderer, usageRenderer)` — a t **Trait/Traitable system:** - `@Singular` — at most one instance of this trait class per `Traitable` (enforced at `addTrait`) -- `@NonSingular` — many instances allowed (default) -- Registration class is determined by walking the class hierarchy for either annotation; cached JVM-globally +- `@NonSingular` — many instances allowed (default; stored in a `ConcurrentHashMap.newKeySet()`) +- Registration class is determined by walking the superclass+interface hierarchy (via `ArrayDeque`) for either annotation; cached JVM-globally in `CodeModelTraitable.registrationTraitClassByTraitClass`; throws `IllegalStateException` if a class carries both `@Singular` and `@NonSingular` - `TraitAware` — lifecycle callbacks `onAddedTrait`/`onRemovedTrait` +- Every `addTrait`/`removeTrait`/`computeIfAbsent`/`computeIfPresent` mutation is atomic (`ConcurrentHashMap.compute`) and triggers `codeModel.index().index/unindex/reindexDynamic` + +**`AnnotationValue.Value` sealed interface — new in this release:** +`AnnotationValue.value()` used to return a raw `Object`. It now returns a typed, exhaustively-switchable sealed interface: +```java +sealed interface Value permits Value.Literal, Value.ClassRef, Value.EnumConstant, Value.Nested, Value.Array { + record Literal(Object value) implements Value // primitive/String literal + record ClassRef(TypeName typeName) implements Value // Foo.class + record EnumConstant(TypeName typeName, String constantName) implements Value + record Nested(AnnotationTypeUsage annotation) implements Value // nested annotation + record Array(List elements) implements Value // array of any of the above +} +``` +Consumers (`jdk-codemodel`'s `TypeMirrorResolver`/`JDKCodeModel`, `dependency-injection`'s `AbstractBindingBuilder`) now build these via exhaustive `switch` instead of instanceof-chains on `Object` — adding/removing a variant is a compiler-enforced break to every consumer. +- **Gotcha:** the wire format is still a raw `Object` for backward compatibility (`toValue`/`fromValue` bridge at marshal/unmarshal time). `ClassRef` and `EnumConstant` are **lossy through a marshal → unmarshal round trip** — `toValue` has no branch that reconstructs them, so they come back as `Value.Literal` wrapping a `TypeName` or a synthesized `"Type.CONST"` string, not as `ClassRef`/`EnumConstant`. **Naming types:** `IrreducibleName` (leaf), `Namespace` (dot-hierarchy), `ModuleName` (flat dot-list), `TypeName` (composite of all). All equality is string-based on `toString()`. @@ -303,7 +318,7 @@ Previous bugs: return type was silently dropped for non-`NamedTypeUsage` types; ### `jdk-codemodel` -**Purpose:** The JDK-backed implementation layer. Populates a `CodeModel` from either (a) loaded `Class` objects via `java.lang.reflect` or (b) `.java` source files via an embedded `javac` run. Both paths produce the same `JDKTypeDescriptor`-based model and are additive. +**Purpose:** The JDK-backed implementation layer. Populates a `CodeModel` from either (a) loaded `Class` objects via `java.lang.reflect` or (b) `.java` source files via an embedded `javac` run. Both paths produce the same `JDKTypeDescriptor`-based model and are additive. The two paths use **independent, non-interoperating resolution stacks** — reflection-based logic lives in `JDKCodeModel` itself; the `javax.lang.model`-based (source/annotation-processing) logic was extracted into a shared `TypeMirrorResolver` used by both `JdkInitializer` and `jdk-annotation-processor`'s `AnnotationProcessor`. **Entry points:** `build.codemodel.jdk.JDKCodeModel` (reflection), `build.codemodel.jdk.JdkInitializer` (source), `build.codemodel.jdk.descriptor.JDKModuleDescriptor` (JPMS module model) @@ -311,10 +326,11 @@ Previous bugs: return type was silently dropped for non-`NamedTypeUsage` types; | File | Purpose | |------|---------| -| `JDKCodeModel` | `ObjectOrientedCodeModel` subclass; BFS type discovery via reflection; `getTypeUsage(Type)` handles all `java.lang.reflect` type variants; `referencesTo(TypeName)` and `referencesTo(TypeName, ReferenceKind)` APIs | -| `JdkInitializer` | `Initializer`; runs javac on source files; populates the code model with full AST fidelity including method bodies, field initializers, JPMS directives, source locations, imports, nested type relationships, initializer blocks, and varargs markers; uses unified `processMembers()` pass | +| `JDKCodeModel` | `ObjectOrientedCodeModel` subclass; BFS type discovery via reflection; `getTypeUsage(Type)` handles all `java.lang.reflect` type variants; `referencesTo(TypeName)` and `referencesTo(TypeName, ReferenceKind)` APIs; `rescan(...)` incremental re-analysis API (new — see below) | +| `JdkInitializer` | `Initializer`; runs javac on source files; populates the code model with full AST fidelity including method bodies, field initializers, JPMS directives, source locations, imports, nested type relationships, initializer blocks, and varargs markers; uses unified `processMembers()` pass; now a thin (~480-line) tree-walker that delegates all `TypeMirror`-resolution work to `TypeMirrorResolver`; switched from `TreeScanner` to `TreePathScanner`; gained `withOptions(...)`, `withEnablePreview()`, and a settable `DiagnosticListener` | +| `TypeMirrorResolver` | **New.** Shared `TypeMirror` → `TypeUsage` resolution engine (707 lines) used by both `JdkInitializer` and the `jdk-annotation-processor` module's `AnnotationProcessor` — "both need this algorithm; this class owns it once." Depth-first lazy-queue algorithm; also builds type descriptors, formal parameters, annotations, and modifiers for the source-based path. Made threadsafe (all mutable resolution state is local to each `resolve()` call; the only shared field, `typeNameCache`, is a threadsafe `Memoizer`) | | `JDKModuleDescriptor` | Rich `ModuleDescriptor` impl for JPMS modules; three creation paths (see below); exposes `requiresClauses()`, `exportsClauses()`, `opensClauses()`, `providesClauses()`, `usesClauses()`, `annotationClauses()`, `include(other)` | -| `JdkExpressionConverter` | `SimpleTreeVisitor` converting `ExpressionTree` → model `Expression`; resolves symbols and method invocations via javac's `Trees` oracle | +| `JdkExpressionConverter` | `SimpleTreeVisitor` converting `ExpressionTree` → model `Expression`; resolves symbols and method invocations via javac's `Trees` oracle; `tagExpressionType()`/`resolveMethod()` are best-effort — swallow exceptions and omit the trait on failure | | `JdkStatementConverter` | `SimpleTreeVisitor` converting `StatementTree` → model `Statement` | | `ReferenceKind` | Enum: `EXTENDS`, `IMPLEMENTS`, `FIELD_TYPE`, `RETURN_TYPE`, `PARAMETER_TYPE`, `METHOD_BODY` — classifies the structural role in which one type references another | | `TypeReference` | Record `(TypeDescriptor owner, ReferenceKind kind, Optional member)` — return value of `referencesTo()`; `member` is `Optional.empty()` for type-level refs and initializer blocks | @@ -350,7 +366,7 @@ All directive-adding helpers (`addRequires`, `addExports`, `addOpens`, `addProvi | `FieldInitializerDescriptor` | Captured `Expression` for a field initializer (source path only) | | `MethodBodyDescriptor` | Captured `Block` body for a method/constructor (source path only) | | `InitializerBlockDescriptor` | Captured `Block` for a static or instance initializer block (source path only); `isStatic()` distinguishes the two; implements `Composite` for mereology traversal | -| `LocationTrait` | Source position: `URI uri`, `long startPosition`, `long endPosition` (character offsets, not line/col); attached to every type/field/method/constructor in the source path | +| `SourceLocation` | **New — replaces the old `LocationTrait`.** Sealed `Location` + `Trait` interface with four record variants: `FilePosition(uri, startPosition, endPosition)` (character offsets; used by `JdkInitializer`, and matched against by `rescan()`), `ElementRef(Element)` (annotation-processing path, no full source tree), `AnnotationRef(Element, AnnotationMirror)`, `AnnotationValueRef(Element, AnnotationMirror, AnnotationValue)`. The latter three replace `jdk-annotation-processor`'s deleted `ElementLocation`/`AnnotationMirrorLocation`/`AnnotationValueLocation` classes, unifying source-provenance tracking for both the source-tree path and the live-`Element` (annotation processor) path in one place | | `ImportDeclaration` | One import per trait: `qualifiedName`, `isStatic`, `isOnDemand`, `order`; attached only to outermost (non-nested) type descriptors in the source path | | `Varargs` | Singleton enum `Trait` on `FormalParameterDescriptor`; marks the varargs (`...`) parameter; only applied to the last parameter when `methodElement.isVarArgs()` is true | | `MethodImplementationDescriptor` | Marks a default interface method | @@ -400,7 +416,7 @@ JdkInitializer.initialize(CodeModel) → new JdkExpressionConverter + JdkStatementConverter (mutual wiring) → for each CompilationUnitTree: processTypeElement (unified processMembers() pass): - → addTrait(LocationTrait) per type/field/method/constructor + → addTrait(SourceLocation.FilePosition) per type/field/method/constructor → addTrait(ImportDeclaration) per import (top-level types only) → addTrait(MemberTypeDescriptor) per declared nested type → addTrait(EnclosingTypeDescriptor) on nested type @@ -429,19 +445,30 @@ JDKCodeModel.referencesTo(targetTypeName) *Method resolution to `MethodDescriptor`:* `resolveMethod` gets the `ExecutableElement` via javac, looks up the declaring `TypeElement`'s FQN in the `CodeModel`, then matches by name + arity + parameter type names → attaches `ResolvedMethod` trait. Gracefully degrades to no trait if the declaring type is not in the model. +**Incremental rescan API — new in this release:** +```java +codeModel.rescan(updatedFile, contextFiles..., classpath, modulePath) +``` +Evicts every `TypeDescriptor`/`ModuleDescriptor` whose `SourceLocation.FilePosition.uri()` matches `updatedFile`, then runs a fresh `JdkInitializer` scoped via `withRegistrationFilter(uri::equals)` — sibling "context files" (e.g. `module-info.java`) are compiled alongside for symbol resolution but not evicted/re-registered themselves. Use this to re-analyze a single file after an edit without rebuilding the whole `CodeModel`. +- Embedded `TypeUsage` references inside *other*, non-evicted descriptors that pointed at the rescanned file's types become **stale and are not fixed up** — this is a documented, deliberate boundary, not a bug. +- Classpath/modulePath must be **repeated** on every `rescan()` call; omitting them (even if they were passed to the original `initialize()`) silently degrades previously-resolved classpath types to `UnknownTypeUsage` rather than erroring. +- "Filtered-out" context compilation units are still fully parsed/analyzed by javac (they just produce no descriptors) — a performance consideration for large rescan contexts. + **Gotchas:** -- `JdkInitializer` is **single-use**; throws `IllegalStateException` on second `initialize()` call -- Javac diagnostics are suppressed (no-op listener); unresolvable types degrade to `UnknownTypeUsage` +- `JdkInitializer` is **single-use**; throws `IllegalStateException` on second `initialize()` call. `rescan()` always constructs a brand-new `JdkInitializer` internally +- Javac diagnostics are suppressed by default (no-op listener); unresolvable types degrade to `UnknownTypeUsage`. A real `DiagnosticListener` can now be attached to `JdkInitializer` - `FieldType`/`MethodType`/`ConstructorType` are reflection-path only; source-path descriptors lack raw reflection handles -- `java.lang.Object` superclass is **not** modelled as `ExtendsTypeDescriptor` unless explicitly extended +- `java.lang.Object` superclass is **not** modelled as `ExtendsTypeDescriptor` unless explicitly extended — elided as a deliberate modeling invariant in both the reflection path and `TypeMirrorResolver.addSuperclass()` - `ResolvedMethod` only resolves methods in types already present in the `CodeModel`; JDK stdlib methods (e.g. `String.valueOf`) are never resolved - Unbounded wildcard `?` in reflection: `WildcardType.getUpperBounds()` returns `[Object.class]` for an unbounded `?` — `JDKCodeModel.getTypeUsage()` suppresses this and correctly produces a `WildcardTypeUsage` with no upper bound - `JDKModuleDescriptor.extract` and `extractFresh` differ in registration: `extract` calls `codeModel.createModuleDescriptor` (shared entry keyed by module name); `extractFresh` creates an unregistered descriptor — use `extractFresh` when multiple JARs share the same JPMS module name but carry different versions - `annotationClauses()` is only populated for source-parsed descriptors; bytecode-extracted descriptors always return empty - `RequiresVersionTrait` is only present on bytecode-extracted descriptors -- `LocationTrait` positions are **character offsets from start of file**, not line/column numbers +- `SourceLocation.FilePosition` positions are **character offsets from start of file**, not line/column numbers. There is no class named `LocationTrait` any more — it was superseded by `SourceLocation` - `ImportDeclaration` traits are attached only to the outermost type in a compilation unit — not to nested types - `initializerRefs` in `referencesTo()` always emits a `TypeReference` with `Optional.empty()` member, since initializer blocks have no named member trait +- Descriptor member ordering is **source-order preserved**: `JdkInitializer.processMembers()` sorts by source start position, not declaration-kind grouping +- Two independent, non-interoperating resolution stacks exist here (reflection-based in `JDKCodeModel`, `javax.lang.model`-based in `TypeMirrorResolver`) — no shared caches/state; the shared `CodeModel`'s dedup-by-`TypeName` semantics prevents duplicate descriptors if both paths touch the same type **Dependencies:** `objectoriented-codemodel`, `codemodel-framework`, `base-foundation`, `base-marshalling`, `base-telemetry-foundation`, `jakarta.inject-api`, JDK modules `java.compiler` + `jdk.compiler` **Depended on by:** `dependency-injection`, `codemodel-framework-builder`, `jdk-annotation-processor` @@ -518,8 +545,17 @@ context.close() // invoke @PreDestroy in reverse topological - `Context.snapshot(Path)` is a no-op unless `InjectionFramework.enableBindingGraph()` or `setBindingGraphContributor` has been called first; no error is thrown — it silently does nothing with the default NOOP contributor - Multibindings (`bindSet`) accumulate across modules naturally — calling `bindSet` from multiple modules returns the same underlying set; no conflict suppression needed - `@PreDestroy` is only invoked on `@Singleton` objects that were actually instantiated during the context's lifetime; never-created singletons are silently skipped +- `resolveMultiBinding` dereferences `multiBindings.get(elementClass)` without a null-check — injecting `Set`/`List` when `bindSet(Foo.class)` was never called throws `NullPointerException` rather than resolving to empty +- `validate()`, `initializeEagerSingletons()`, and `close()` each build their own ad hoc dependency graph on every call — fine at startup, potentially expensive if called repeatedly + +**Concurrency fixes (PR #88):** `InjectionContext` had two check-then-act races over its `ConcurrentHashMap`s, both fixed by making the check-and-insert atomic: +1. **`bindSet` race** — first-time `bindSet(type)` for the same type from concurrent threads could all observe "not present" before any inserted, causing `BindingGraphContributor.contributeBinding` to fire more than once for one logical multibinding. Fixed with a single `ConcurrentHashMap.compute` guarded by an `AtomicBoolean created` flag so the contributor fires exactly once. +2. **Auto-singleton registration race** — concurrent first-time resolution of an unbound `@Singleton` class each tried `bind(concreteClass).to(concreteClass)`; only the first succeeded, and the rest previously let `BindingAlreadyExistsException` propagate and crash the thread. Fixed by catching and swallowing that exception on the losing threads, which then re-resolve through the now-installed binding. +Guidance: any new "register-if-absent" logic here must use one atomic map operation, and any retry path racing `addBinding` must explicitly swallow `BindingAlreadyExistsException` — it signals "someone else won," not a bug. -**Dependencies:** `codemodel-foundation`, `objectoriented-codemodel`, `jdk-codemodel`, `base-foundation`, `base-graph` (for `Graphs.parallelizableGroups` in `initializeEagerSingletons`), `jakarta.inject-api` +**Custom-scope fix (PR #89):** Custom scopes registered via `InjectionFramework.bindScope` were previously honored only by the primary `bind(Class).to(Class)` path. Three other paths skipped the scope check and silently downgraded to prototype/singleton semantics: `toOverriding(Class)`, `bind(value).to(Class)`, and `Context#close()`'s instantiated-binding collection (which only ever looked at `LazySingletonClassBinding`, so custom-scoped instances never got `@PreDestroy`). Fix: replicated the `findScopeEntry` check in all binding-construction paths, gave `CustomScopedClassBinding` an identity-tracked `instances` set mirroring the singleton binding's tracking, and rewrote `close()` to merge singleton + custom-scoped instantiated bindings into one combined reverse-topological teardown order. Any future code path that constructs a `ClassBinding` from a concrete class must replicate the `findScopeEntry` check or custom scopes will silently regress. + +**Dependencies:** `codemodel-foundation`, `objectoriented-codemodel`, `jdk-codemodel`, `base-foundation`, `base-graph` (for `Graphs.parallelizableGroups`/`Graphs.findCycle`/`Graphs.topologicalSort` in `validate`/`initializeEagerSingletons`/`close`), `jakarta.inject-api` **Depended on by:** `codemodel-framework-builder` --- @@ -606,13 +642,18 @@ context.close() // invoke @PreDestroy in reverse topological **Entry point:** `build.codemodel.annotation.processing.AnnotationProcessor` +**Architecture change this release:** `AnnotationProcessor` shrank from ~1000 to ~250 lines. The `TypeMirror`-to-`TypeUsage` resolution logic (generics, wildcards, arrays, type-use annotations — the most bug-prone part of the old processor) was extracted into `jdk-codemodel`'s new shared `TypeMirrorResolver`, since both this processor and `JdkInitializer` need the same algorithm. `AnnotationProcessor` now just orchestrates discovery/round-queueing and delegates all `TypeMirror`-walking to `resolver().resolve(...)`/`createFieldDescriptor(...)`/`getFormalParameters(...)`/`buildTypeDescriptor(...)`, etc. + +The three location classes previously local to this module — `ElementLocation`, `AnnotationMirrorLocation`, `AnnotationValueLocation` — were **deleted**. That responsibility moved to `jdk-codemodel`'s new sealed `SourceLocation` type (`ElementRef`/`AnnotationRef`/`AnnotationValueRef` variants), which is constructed via `SourceLocation.elementRef(element)` → `.withAnnotation(mirror)` → `.withValue(value)`. This ties `jdk-codemodel` more tightly to `javax.lang.model.element` types (`AnnotationMirror`/`AnnotationValue`/`Element`) than before, since it now owns source-provenance tracking for both the source-tree path and the live-`Element` (annotation-processing) path. + **Key files:** | File | Purpose | |------|---------| -| `AnnotationProcessor` | `AbstractProcessor`; registered via `module-info.java provides Processor`; staged processing across annotation rounds | -| `ElementLocation` | `Location` + `Trait` wrapping a `javax.lang.model.element.Element`; rich telemetry error reporting | -| `AnnotationMirrorLocation` / `AnnotationValueLocation` | Fine-grained location types for annotation-level diagnostics | +| `AnnotationProcessor` | `AbstractProcessor`; registered via `module-info.java provides Processor`; staged processing across annotation rounds; delegates `TypeMirror` resolution to `jdk-codemodel`'s `TypeMirrorResolver` | +| `AnnotationProcessorTests` (test) | New shared abstract test base: `compile()` (asserts `Compilation.Status.SUCCESS`) / `run()` (no assertion, for error-path tests) built on `base-compile-testing`'s `Compiler`/`JavaFileObjects` | + +**Test coverage pattern — mirrored suites:** `ConstructorDiscoveryTests`, `TypeAnnotationDiscoveryTests`, `DiscoveryTests`, `FieldDiscoveryTests`, `MethodDiscoveryTests`, and `RecursivelyDefinedTypeDiscoveryTests` exist in near-identical form in both this module (exercising the live `Processor` through `google-compile-testing`-style compilation) and `jdk-codemodel` (exercising `JdkInitializer` directly on source strings) — deliberate, since both funnel through the same `TypeMirrorResolver`/equivalent and the parallel suites guard against the two invocation paths diverging. `TypeAnnotationDiscoveryTests` specifically covers regressions (issues #69–#77) around type-use annotation preservation and graceful degradation to `UnknownTypeUsage` instead of throwing. **Processing stages:** @@ -722,14 +763,15 @@ Non-obvious behaviours that are working as designed but will surprise you if you - `TypeVariableUsage.equals()` compares bounds via `canonicalName()` string equality, not deep structural equality. Two type variables with structurally equivalent but differently-named bounds may compare unequal. - `TypeName.toString()` uses binary name format (dollar-separated nested types with optional module prefix). `canonicalName()` uses dot-separated source form. Don't use `toString()` output as a Java source string. - `AbstractTraitable` lazily creates its `CodeModelTraitable` delegate — `hasTraits()` returns `false` (and `traits()` returns empty) until the first trait is added. Don't rely on `hasTraits()` as a proxy for "has this object been fully initialized." +- `AnnotationValue.value()` returns the sealed `AnnotationValue.Value` (`Literal`/`ClassRef`/`EnumConstant`/`Nested`/`Array`), not a raw `Object`, as of this release. `ClassRef` and `EnumConstant` are **lossy through a marshal → unmarshal round trip** — the wire format is still a raw `Object` for backward compatibility, and the unmarshal-side converter has no branch to reconstruct those two variants, so they come back as `Value.Literal` wrapping a `TypeName` or a synthesized `"Type.CONST"` string. **`jdk-codemodel`** -- `JdkInitializer` is single-use — call `initialize()` a second time and it throws. -- Javac diagnostics are suppressed — unresolvable types silently become `UnknownTypeUsage`. This is intentional; the source path is designed to be permissive. -- Types that implicitly extend `Object` have no `ExtendsTypeDescriptor` trait. `Object` is the implicit root and is not modelled as an explicit parent. +- `JdkInitializer` is single-use — call `initialize()` a second time and it throws. `rescan()` always builds a fresh `JdkInitializer` internally. +- Javac diagnostics are suppressed by default — unresolvable types silently become `UnknownTypeUsage`. This is intentional; the source path is designed to be permissive. A real `DiagnosticListener` can now be attached if you need to observe them. +- Types that implicitly extend `Object` have no `ExtendsTypeDescriptor` trait. `Object` is the implicit root and is not modelled as an explicit parent — true on both the reflection path and the new shared `TypeMirrorResolver`. - `ResolvedMethod` is only attached when the declaring type is already in the `CodeModel`. Calls into JDK stdlib (e.g. `String.valueOf`) produce no trait — expected, since stdlib types aren't reverse-engineered unless explicitly loaded. -- `FieldType`/`MethodType`/`ConstructorType` traits only exist on reflection-path descriptors. `MethodBodyDescriptor`/`FieldInitializerDescriptor`/`LocationTrait`/`ImportDeclaration`/`InitializerBlockDescriptor`/`Varargs` only exist on source-path descriptors. -- `LocationTrait` positions are **character offsets from start of file**, not line/column pairs. Use `SourcePositions` to convert to line/column if needed. +- `FieldType`/`MethodType`/`ConstructorType` traits only exist on reflection-path descriptors. `MethodBodyDescriptor`/`FieldInitializerDescriptor`/`SourceLocation`/`ImportDeclaration`/`InitializerBlockDescriptor`/`Varargs` only exist on source-path descriptors. +- There is no class named `LocationTrait` any more — it was superseded by the sealed `SourceLocation` interface (`FilePosition`/`ElementRef`/`AnnotationRef`/`AnnotationValueRef`). `SourceLocation.FilePosition` positions are **character offsets from start of file**, not line/column pairs. Use `SourcePositions` to convert to line/column if needed. - `ImportDeclaration` is only on the **outermost** (non-nested) type in a compilation unit. Nested types in the same file share that import context — look it up on the enclosing type's descriptor. - `MemberTypeDescriptor` (on enclosing type) and `EnclosingTypeDescriptor` (on nested type) are the two ends of the nesting relationship. Neither stores the descriptor itself — only `TypeName`s; resolve via `codeModel.getTypeDescriptor(name)`. - `referencesTo()` scans all registered descriptors — it is not indexed. For large codebases, call it sparingly or with a `ReferenceKind` filter. @@ -737,6 +779,9 @@ Non-obvious behaviours that are working as designed but will surprise you if you - `JDKModuleDescriptor.extract` vs `extractFresh`: `extract` registers in the `CodeModel` (one descriptor per module name shared globally); `extractFresh` creates an independent descriptor not registered in the model. Use `extractFresh` when scanning a classpath where the same JPMS module name may appear at different versions. - Directive-adding helpers on `JDKModuleDescriptor` are idempotent — duplicate `requires`/`exports`/`opens`/`provides`/`uses` entries are silently skipped. - `annotationClauses()` (annotations declared on `module-info`) is only populated via the source path; bytecode extraction does not capture module annotations. +- `JDKCodeModel.rescan(...)` evicts descriptors by `SourceLocation.FilePosition` URI match only — it does **not** fix up stale `TypeUsage` references embedded in other, non-evicted descriptors that pointed at the rescanned types. It also silently degrades classpath-resolved types to `UnknownTypeUsage` if you forget to re-supply classpath/modulePath on the `rescan()` call (they aren't remembered from the original `initialize()`). +- `TypeMirrorResolver` (new) is threadsafe — its resolution state is local per `resolve()` call — but `JdkInitializer` still creates a fresh instance per `initialize()`, so don't assume sharing one across `JdkInitializer` instances is supported without checking. +- Two independent, non-interoperating resolution stacks coexist in this module: reflection-based (`JDKCodeModel`) and `javax.lang.model`-based (`TypeMirrorResolver`, shared with `jdk-annotation-processor`). They share no caches; only the `CodeModel`'s dedup-by-`TypeName` semantics keeps them from producing duplicate descriptors for the same type. **`objectoriented-codemodel`** - `MethodDescriptor.signature()` now uses `canonicalName()` — output is module-free. If you need module-qualified names in a diagnostic, call `returnType().toString()` directly. @@ -747,8 +792,10 @@ Non-obvious behaviours that are working as designed but will surprise you if you - `@Provides` methods with parameters are silently skipped by `ProvidesResolver` (factory pattern requires no-arg methods). - `Context.snapshot(Path)` is a no-op unless a real `BindingGraphContributor` has been installed via `InjectionFramework.enableBindingGraph()` or `setBindingGraphContributor`. - `Modules.override(base, overrides)` installs the override module first, then installs the base through a `SuppressingBinder` that swallows `BindingAlreadyExistsException`; the override module therefore runs `configure` once (with a real `Binder`), and the base module also runs `configure` once (with a suppressing one). -- Custom scopes require both: (1) a scope annotation meta-annotated with `@ScopeAnnotation`, and (2) a `Scope` impl registered via `InjectionFramework.bindScope`. Missing either silently produces prototype behaviour. -- `@PreDestroy` methods are called only for singletons that were *actually created* in the context's lifetime; never-created singletons are silently skipped. +- Custom scopes require both: (1) a scope annotation meta-annotated with `@ScopeAnnotation`, and (2) a `Scope` impl registered via `InjectionFramework.bindScope`. Missing either silently produces prototype behaviour — **as of PR #89 this is now honored consistently across all four `ClassBinding`-construction paths** (`bind(Class).to`, `.toOverriding`, `bind(value).to(Class)`, and eager-singleton auto-registration); previously three of the four silently ignored custom scopes. +- `@PreDestroy` methods are called only for singletons/custom-scoped instances that were *actually created* in the context's lifetime; never-created bindings are silently skipped. `close()` now tears down singleton and custom-scoped instances together in one combined reverse-topological order (previously only singletons were considered). +- `InjectionContext`'s `bindSet`-first-registration and `@Singleton` auto-registration are now race-safe as of PR #88 (single atomic `compute`, and losing threads swallow `BindingAlreadyExistsException` and retry) — but any *new* register-if-absent logic added to this class must follow the same pattern or reintroduce the race. +- `resolveMultiBinding` will `NullPointerException` if you inject `Set`/`List`/etc. for a type that was never registered via `bindSet(Foo.class)` in any module — there's no null-check fallback to "empty." **`base-parsing` migration** - The internal Pratt-parser classes (`ExpressionParser`, `Tokenizer`, `NodeResolver`, `TokenParser`, etc.) were deleted from `expression-codemodel` in PR #24. Parsing is now in `build.base:base-parsing`. References to those classes in old branches are dead. @@ -775,7 +822,7 @@ Non-obvious behaviours that are working as designed but will surprise you if you **To find all usages of a type across the code model:** Call `jdkCodeModel.referencesTo(typeName)`. Filter by role with `referencesTo(typeName, ReferenceKind.FIELD_TYPE)` etc. Each `TypeReference` has the owning descriptor, the kind, and optionally the specific member. -**To get the source location of a type/field/method (source path only):** Call `descriptor.getTrait(LocationTrait.class)`. Positions are character offsets from file start; the `uri` identifies the source file. +**To get the source location of a type/field/method (source path only):** Call `descriptor.getTrait(SourceLocation.class)` and pattern-match on the sealed `SourceLocation.FilePosition` variant. Positions are character offsets from file start; the `uri` identifies the source file. **To get the import declarations of a type (source path only):** Call `descriptor.traits(ImportDeclaration.class)` on the outermost type descriptor. Sorted by `order()` for source-order fidelity. @@ -796,3 +843,7 @@ Non-obvious behaviours that are working as designed but will surprise you if you **To parse a `module-info.java`:** Call `JDKModuleDescriptor.parse(codeModel, source)` (string or `Reader`). For bytecode extraction from a JAR: `JDKModuleDescriptor.extract(codeModel, jarPath)`. For a version-aware non-registering extraction: `JDKModuleDescriptor.extractFresh(codeModel, jarPath)`. **To merge JPMS directives from one module into another:** Call `targetDescriptor.include(sourceDescriptor)` — deduplicates by module name / package name / service type. + +**To re-analyze a single file after an edit without rebuilding the whole `CodeModel`:** Call `jdkCodeModel.rescan(updatedFile, contextFiles..., classpath, modulePath)`. Re-supply classpath/modulePath every call — they aren't remembered from the original `initialize()`. Remember that `TypeUsage` references embedded in other, non-evicted descriptors are not automatically fixed up. + +**To resolve a `TypeMirror` (from `javax.lang.model`) into a `TypeUsage` outside of `JdkInitializer` or the annotation processor (e.g. building a custom `Processor`):** Reuse `build.codemodel.jdk.TypeMirrorResolver` rather than writing a new visitor — it's shared specifically so this logic isn't duplicated.