Skip to content

Add TypeNameMatcher to RecipeClassLoader allowlist; align with moderne-recipe-loading-commons#7584

Merged
jkschneider merged 2 commits intomainfrom
dt-fix-typenamematcher-allowlist
May 7, 2026
Merged

Add TypeNameMatcher to RecipeClassLoader allowlist; align with moderne-recipe-loading-commons#7584
jkschneider merged 2 commits intomainfrom
dt-fix-typenamematcher-allowlist

Conversation

@pdelagrave
Copy link
Copy Markdown
Contributor

@pdelagrave pdelagrave commented May 6, 2026

Problem

RecipeClassLoader.PARENT_DELEGATED_PREFIXES and the parallel loadFromParent list in moderne-recipe-loading-commons (which this class was forked from in #6437) have drifted. The most acute miss: org.openrewrite.java.TypeNameMatcher, recently added as a parameter type to TypesInUse.hasTypeMatching(TypeNameMatcher, boolean).

Because TypesInUse is in the allowlist (parent-loaded) but TypeNameMatcher was not, the recipe artifact's classloader loaded its own copy of TypeNameMatcher, while TypesInUse (parent-loaded) was compiled against the parent's copy. When a recipe-loaded UsesType.visit() invokes TypesInUse.hasTypeMatching(TypeNameMatcher, …), the JVM enforces a loader constraint and throws LinkageError because the two classloaders disagree on TypeNameMatcher's identity.

In production this fires once per (recipe instance × source file) pair, with a multi-KB stack trace recorded in SourcesFileErrors each time. On Spring-heavy customer portfolios it produces hundreds of MB to tens of GB per repo and filled worker disks today (Moderne ops/issues#867).

Solution

Add org.openrewrite.java.TypeNameMatcher to PARENT_DELEGATED_PREFIXES so both the recipe classloader and the parent classloader resolve to the same TypeNameMatcher Class object.

While here, bring in the other entries from moderne-recipe-loading-commons's allowlist that reference OSS classes and have drifted out of this one over time. They're all engine API surface that recipe code touches and would produce the same kind of failure if the parent and recipe ever held different versions:

  • org.openrewrite.Singleton (added on the commons side in moderneinc/moderne-recipe-loading-commons#69, never ported here)
  • org.openrewrite.gradle.attributes.Category / ProjectAttribute
  • org.openrewrite.java.Java17Parser
  • org.openrewrite.maven.attributes.Attributed
  • org.openrewrite.polyglot
  • org.openrewrite.protobuf.ProtoVisitor
  • org.openrewrite.rpc.{Reference, RpcCodec, RpcObjectData, RpcReceiveQueue, RpcRecipe, RpcSendQueue}

Entries from the commons allowlist that reference proprietary Moderne artifacts are intentionally not included here, since they don't exist in OSS rewrite-core's classpath:

  • org.openrewrite.cobol.CobolPreprocessor[Iso]Visitor lives in moderneinc/rewrite-cobol (proprietary)
  • org.openrewrite.nodejs.NpmExecutor lives in moderneinc/rewrite-nodejs (proprietary)
  • io.moderne.devcenter.* (proprietary)

Two further entries that exist in the commons allowlist but appear to refer to nonexistent classes (org.openrewrite.java.InvocationMatcher; org.openrewrite.maven.table.MavenDownloadEvents) are also not included here, since they would be no-op string matches. They will be cleaned up in a separate PR on the commons side.

Validation

Locally: with this fix applied (and the equivalent fix already merged in moderneinc/moderne-recipe-loading-commons#70 for the parallel class used by moderne-worker v1), running io.moderne.java.spring.boot4.SpringBoot4BestPractices on a small (104 Java file) repo via mod CLI drops SourcesFileErrors from 111,767 rows to 4 rows. The remaining 4 are an unrelated K.Return.getPrefix() NPE in rewrite-kotlin.

Going forward

The two allowlists having drifted is the underlying anti-pattern. Worth tracking a long-term fix (single source of truth, generated allowlist from public method signatures, or CI lint) but out of scope for this PR.

`RecipeClassLoader.PARENT_DELEGATED_PREFIXES` includes `org.openrewrite.java.internal.TypesInUse`, ensuring the parent and child classloaders share one `TypesInUse` Class object. The signature of `TypesInUse.hasTypeMatching` was extended with a new parameter type, `org.openrewrite.java.TypeNameMatcher`, but `TypeNameMatcher` was not added to the allowlist.

The result: when a recipe-loaded `UsesType.visit()` invokes `TypesInUse.hasTypeMatching(TypeNameMatcher, boolean)`, the recipe classloader resolves `TypeNameMatcher` to its own copy (from the recipe artifact's bundled rewrite-core), while the parent-loaded `TypesInUse` was compiled against the parent's `TypeNameMatcher`. The JVM enforces a loader constraint and throws `LinkageError` because the two classloaders disagree on `TypeNameMatcher`'s identity.

Each error fires once per `(recipe instance, source file)` pair and writes a multi-KB stack trace to the `SourcesFileErrors` data table. On Spring-heavy multi-recipe runs this produced hundreds of MB to tens of GB per repo.

Adding `TypeNameMatcher` to the allowlist forces both classloaders to resolve to the same `Class` object, satisfying the loader constraint.

Validated locally with `mod` CLI: running `io.moderne.java.spring.boot4.SpringBoot4BestPractices` on a Spring-using repo dropped `SourcesFileErrors` from 111,767 rows to 4, eliminating all LinkageError occurrences.
@pdelagrave pdelagrave self-assigned this May 6, 2026
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite May 6, 2026
…e-recipe-loading-commons

`RecipeClassLoader.PARENT_DELEGATED_PREFIXES` and the parallel `loadFromParent` list in `moderne-recipe-loading-commons` (which this class was forked from in #6437) have drifted. The most acute miss: `org.openrewrite.java.TypeNameMatcher`, recently added as a parameter type to `TypesInUse.hasTypeMatching(TypeNameMatcher, boolean)`.

Because `TypesInUse` is in the allowlist (parent-loaded) but `TypeNameMatcher` was not, the recipe artifact's classloader loaded its own copy of `TypeNameMatcher` while `TypesInUse` (parent-loaded) was compiled against the parent's copy. When a recipe-loaded `UsesType.visit()` invokes `TypesInUse.hasTypeMatching(TypeNameMatcher, ...)`, the JVM enforces a loader constraint and throws `LinkageError` because the two classloaders disagree on `TypeNameMatcher`'s identity. The error fires once per (recipe instance, source file) pair, with a multi-KB stack trace recorded in `SourcesFileErrors` each time.

Locally validated: with this fix applied, running `io.moderne.java.spring.boot4.SpringBoot4BestPractices` on a small (104 Java file) repo via `mod` CLI dropped `SourcesFileErrors` from 111,767 rows to 4 rows.

While here, also bring in the other entries from moderne-recipe-loading-commons that are missing here and reference OSS classes:
- org.openrewrite.Singleton
- org.openrewrite.gradle.attributes.Category / ProjectAttribute
- org.openrewrite.java.Java17Parser
- org.openrewrite.maven.attributes.Attributed
- org.openrewrite.polyglot
- org.openrewrite.protobuf.ProtoVisitor
- org.openrewrite.rpc.{Reference, RpcCodec, RpcObjectData, RpcReceiveQueue, RpcRecipe, RpcSendQueue}

Entries from the moderne-recipe-loading-commons allowlist that reference proprietary Moderne classes (org.openrewrite.cobol.CobolPreprocessor[Iso]Visitor lives in moderneinc/rewrite-cobol; org.openrewrite.nodejs.NpmExecutor lives in moderneinc/rewrite-nodejs) are intentionally not added here since they don't exist in OSS rewrite-core's classpath. Same for io.moderne.devcenter.* entries.
@pdelagrave pdelagrave force-pushed the dt-fix-typenamematcher-allowlist branch from 2af7adb to 40dd096 Compare May 6, 2026 22:00
@jkschneider jkschneider merged commit 1bba33d into main May 7, 2026
1 check passed
@jkschneider jkschneider deleted the dt-fix-typenamematcher-allowlist branch May 7, 2026 13:57
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants