csharp: implement TreeVisitor.Adapt() on the .NET side#7591
Merged
jkschneider merged 1 commit intomainfrom May 7, 2026
Merged
Conversation
…isitors can visit Cs / J nodes Mirrors the Python fix from c89d246 (#7590), but for the parallel .NET implementation in `rewrite-csharp/csharp/OpenRewrite/`. Two distinct failure modes existed before this change: 1. A bare `TreeVisitor<J, P>` (or any subclass that overrides only `PreVisit` / `PostVisit` — a common pattern for cross-language collectors) traversing a parsed C# source would silently no-op past the root: the default `Accept` is identity, so traversal never descended into children. Quiet, but every per-node hook missed every node. 2. A plain `JavaVisitor<P>` traversing a Cs source would throw `InvalidOperationException("Unknown J tree type: ...")` on the first `Cs.UsingDirective` / `Cs.PropertyDeclaration` / etc. its switch doesn't recognize. Unlike the JVM side (which inherits a real `TreeVisitor.adapt()` from `rewrite-core` backed by Quarkus Gizmo bytecode generation), and unlike the TypeScript side (which sidesteps the problem with a kind-keyed registry consulted only inside `JavaVisitor.accept`), the C# port had no adaptation mechanism at all. This change introduces one: * `TreeVisitorAdapterRegistry` — a non-generic, lock-protected registry of `(treeType, openLangVisitorType, openAdapterType)` triples kept sorted with the most specific tree type first, so a Cs node hits the `CSharpVisitor` adapter before falling through to the `JavaVisitor` one. * `TreeVisitor<T, P>.Adapt(tree)` — looks up the matching language adapter, closes the open generic with `P`, and instantiates via `Activator`. Result is cached per visitor instance keyed by the closed language-visitor type, so the wrapper allocation happens at most once per language per visitor lifetime. * `Visit()` now dispatches via `Adapt(t).Accept(t, p)` instead of `Accept(t, p)`. `Cursor` is made virtual so the adapter can forward it to the wrapped visitor. * `TreeVisitorAsJavaVisitor<P>` and `TreeVisitorAsCSharpVisitor<P>` — thin adapter classes that IS-A `JavaVisitor` / `CSharpVisitor` (so the language switch and child-traversal defaults are inherited), but override `Visit` to forward to the wrapped visitor (so its lifecycle — `_visitCount`, `_afterVisit`, `_stopAfterPreVisit`, `PreVisit`, `PostVisit`, `DefaultValue` — runs unchanged) and override `Cursor` to forward to wrapped state. Each language registers its triple via a `[ModuleInitializer]` that fires on assembly load, so registration is guaranteed before any `TreeVisitor<J, P>.Visit()` call regardless of which generic instantiations the user code happens to reference. Tests: `TreeVisitorAdaptTest` covers both regression modes — a `CountingPreVisitor : TreeVisitor<J, ExecutionContext>` overriding only `PreVisit` must descend into both Cs and J nodes of a parsed file, and a `MethodCountingJavaVisitor : JavaVisitor<ExecutionContext>` must traverse the same source without throwing. Full suite stays green (1917/1917 with `RPC_TEST_SERVER_CLASSPATH` wired via `./gradlew :rewrite-csharp:rpcTestClasspath`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
rewrite-csharp/csharp/OpenRewrite/. Two distinct failure modes existed before this change:Silent no-op: A bare
TreeVisitor<J, P>(or any subclass that overrides onlyPreVisit/PostVisit— a common pattern for cross-language collectors) traversing a parsed C# source would silently no-op past the root. The defaultAcceptis identity, so traversal never descended into children. Quiet, but every per-node hook missed every node.Hard throw: A plain
JavaVisitor<P>traversing a Cs source would throwInvalidOperationException("Unknown J tree type: ...")on the firstCs.UsingDirective/Cs.PropertyDeclaration/ etc. its switch doesn't recognize.Unlike the JVM side (which inherits a real
TreeVisitor.adapt()fromrewrite-corebacked by Quarkus Gizmo bytecode generation) and unlike the TypeScript side (which sidesteps the problem with a kind-keyed registry consulted only insideJavaVisitor.accept), the C# port had no adaptation mechanism at all.Approach
TreeVisitorAdapterRegistry— a non-generic, lock-protected registry of(treeType, openLangVisitorType, openAdapterType)triples kept sorted with the most specific tree type first, so a Cs node hits theCSharpVisitoradapter before falling through to theJavaVisitorone.TreeVisitor<T, P>.Adapt(tree)— looks up the matching language adapter, closes the open generic withP, and instantiates viaActivator. Result is cached per visitor instance keyed by the closed language-visitor type, so the wrapper allocation happens at most once per language per visitor lifetime.Visit()now dispatches viaAdapt(t).Accept(t, p)instead ofAccept(t, p).Cursoris made virtual so the adapter can forward it to the wrapped visitor.TreeVisitorAsJavaVisitor<P>/TreeVisitorAsCSharpVisitor<P>— thin adapter classes that IS-AJavaVisitor/CSharpVisitor(so the language switch and child-traversal defaults are inherited), but overrideVisitto forward to the wrapped visitor (so its lifecycle —_visitCount,_afterVisit,_stopAfterPreVisit,PreVisit,PostVisit,DefaultValue— runs unchanged) and overrideCursorto forward to wrapped state. Each language registers its triple via a[ModuleInitializer]that fires on assembly load, so registration is guaranteed before anyTreeVisitor<J, P>.Visit()call regardless of which generic instantiations the user code happens to reference.Test plan
TreeVisitorAdaptTestcovers both regression modes:BareTreeVisitorTraversesCSharpSource: aTreeVisitor<J, ExecutionContext>overriding onlyPreVisitdescends into both Cs- and J-typed nodes of a parsed file.JavaVisitorTraversesCSharpSourceWithoutThrowing: aJavaVisitor<ExecutionContext>traverses the same source without throwing on Cs nodes.RPC_TEST_SERVER_CLASSPATHwired via./gradlew :rewrite-csharp:rpcTestClasspath(covers JVM↔.NET RPC integration tests too).Cursorvirtual and addingAdapt(protected virtual). ExistingJavaVisitor/CSharpVisitor/XmlVisitoroverrides ofAcceptcontinue to compile unchanged.