Cache `Type` instances in `TypeFactory.getType(...)` to avoid redundant work
Context
TypeFactory.getType(TypeMirror) and getType(TypeElement) (processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java:209-238) allocate a new Type each call and re-run the full type analysis: Types#isSubtypeErased against Iterable/Collection/Map/Stream, declared-kind introspection, component-type computation, etc.
There is no interning. Callers that ask for the same TypeMirror repeatedly pay the full cost every time.
Why this matters now
While reviewing #4033 (JSpecify nullness), two places were flagged that call getType(...) per property and then walk the enclosing-element chain for @NullMarked/@NullUnmarked:
PropertyMapping.java—getSourceJSpecifyNullability()/targetDeclaringTypeIsNullMarked()per property mappingPresenceCheckMethodResolver.java:98-108— rebuildsTypefor the mapper type on every presence-check resolution
Type#isNullMarked memoizes the walk per Type instance, but since each getType(...) returns a fresh instance, the memoization does not survive across calls. The effect is O(mappings × enclosingDepth) element-chain walks on large mappers, and it is not unique to JSpecify — any code path that asks for the same type twice pays similar overhead today.
Proposal
Intern Type by a stable key:
- For
TypeElement-keyed lookups:IdentityHashMap<TypeElement, Type>is safe within a single processing round. - For
TypeMirror-keyed lookups:TypeMirrordoes not have useful equality, so intern via the resolvedTypeElement(whenDeclaredType) or a fingerprint for primitive/array/wildcard types. Fall back to no-caching where a stable key is not available.
Edges to think about:
alwaysImport/isLiteralflags are currently passed through — the cache key needs to include these, or the memoizedTypeneeds to be immutable w.r.t. them.- Incremental builds (javac + Gradle/Eclipse):
TypeElementidentity is stable within a round, but not across rounds. Scope the cache to theTypeFactorylifetime (one per processing round today) and this is fine. TypeHierarchyErroneousExceptionmust still be thrown for unresolvable mirrors — don't cache the erroneous state in a way that swallows it later.
Acceptance
getType(someElement)called N times returns the same instance (or an equivalent instance from a cache), and measurable reduction inisSubtypeErasedcalls on a representative mapper (e.g. something inintegrationtestwith many properties).- No behavior change for
alwaysImport/ literal variants. Type#isNullMarked(and any future per-Typememoization) becomes effective across repeated lookups.
Related
- PR #4033 (JSpecify nullness support) added
Type#isNullMarkedwith per-instance caching that the currentgetTypecontract makes ineffective; the Javadoc there was corrected in a follow-up but the underlying perf concern remains.
Source: mapstruct/mapstruct