python: implement TreeVisitor.adapt() so generic visitors can visit Py/J nodes#7590
Merged
jkschneider merged 1 commit intomainfrom May 6, 2026
Merged
python: implement TreeVisitor.adapt() so generic visitors can visit Py/J nodes#7590jkschneider merged 1 commit intomainfrom
jkschneider merged 1 commit intomainfrom
Conversation
… Py / J nodes
`TreeVisitor.adapt()` was stubbed (`# FIXME implement the visitor adapting`) —
it just returned `self`. That's load-bearing for correctness because each
language's LST root routes its `accept(v, p)` through the adapter:
```python
class J:
def accept(self, v, p):
return self.accept_java(v.adapt(J, JavaVisitor), p)
class Py(J):
def accept(self, v, p):
return self.accept_python(v.adapt(Py, PythonVisitor), p)
```
When `v` is a bare `TreeVisitor` (or a direct subclass that overrides only
`pre_visit` / `post_visit` — a common pattern for cross-language collectors),
the stubbed `adapt` flowed `v` straight into `accept_java` / `accept_python`,
which immediately calls language-specific dispatch:
```python
class CompilationUnit:
def accept_python(self, v, p):
return v.visit_compilation_unit(self, p)
```
The bare `TreeVisitor` doesn't implement `visit_compilation_unit` →
`AttributeError`, wrapped in `RecipeRunException` by the visitor framework's
outer try/except, propagating up. A reproducible failure case:
```python
class _Collector(TreeVisitor):
def pre_visit(self, tree, p):
return tree
_Collector().visit(some_py_compilation_unit, None)
# RecipeRunException: AttributeError: '_Collector' object has no attribute 'visit_compilation_unit'
```
This blocks any correct implementation of `_collect_search_result_ids` (and
similar cross-language tree walks) from extending the bare `TreeVisitor`. PR
#7589 worked around the symptom by replacing the visitor-based collector
with a hand-rolled iterative walker; that's the wrong layer to fix it.
This change implements `adapt()` properly:
* A class-level registry on `TreeVisitor` maps each language visitor base
class to a small adapter class. Each language module
(`rewrite.java.visitor`, `rewrite.python.visitor`) registers its adapter
at import time via `TreeVisitor.register_adapter`.
* `adapt(tree_type, visitor_type)` returns:
- `self` if the visitor is already an instance of `visitor_type`
- otherwise an adapter instance that *is-a* `visitor_type` (so the
language-specific `visit_*` defaults in JavaVisitor / PythonVisitor are
available for child traversal) and forwards `pre_visit` / `post_visit`
/ `default_value` / `is_acceptable` plus `_cursor` / `_visit_count` /
`_after_visit` to the wrapped visitor — so user-defined logic on the
original generic visitor still runs against the right cursor and sees
every traversed node.
* When `adapt` is called on an existing adapter (e.g. cross-language
traversal: a Py node visited via a JavaVisitor adapter chain), the
wrapped visitor is unwrapped and re-wrapped in the correct adapter
rather than nesting adapters.
`is_adaptable_to` is updated to match the new semantics: a visitor is
adaptable to a target type if it's already an instance, or if it's an
existing adapter, or if a registered adapter exists for the target type.
Tests: a new `tests/test_visitor_adapt.py` covers the regression (a bare
`TreeVisitor` subclass visiting a `Py.CompilationUnit`), the end-to-end
search-result collection through `_collect_search_result_ids`, and the
unwrap-then-readapt behaviour for cross-language traversal.
3 tasks
jkschneider
added a commit
that referenced
this pull request
May 7, 2026
…isitors can visit Cs / J nodes (#7591) 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
TreeVisitor.adapt()was stubbed inrewrite-python(# FIXME implement the visitor adapting) — it just returnedself. That's load-bearing for correctness because each language's LST root routes itsaccept(v, p)through the adapter:When
vis a bareTreeVisitor(or a direct subclass that overrides onlypre_visit/post_visit— a common pattern for cross-language collectors), the stubbedadaptflowedvstraight intoaccept_java/accept_python, which immediately calls language-specific dispatch:The bare
TreeVisitordoesn't implementvisit_compilation_unit→AttributeError, wrapped inRecipeRunExceptionby the visitor framework's outertry/exceptand propagated up. Reproducible failure:_collect_search_result_ids(and similar cross-language tree walks) from extending the bareTreeVisitor. python: replace _Collector visitor with iterative tree walker in _collect_search_result_ids #7589 worked around the symptom by replacing the visitor-based collector with a hand-rolled iterative walker; closing that PR was the right call — this is the right layer to fix it.Implementation
A class-level registry on
TreeVisitormaps each language visitor base class to a small adapter class. Each language module (rewrite.java.visitor,rewrite.python.visitor) registers its adapter at import time viaTreeVisitor.register_adapter.adapt(tree_type, visitor_type)returns:selfif the visitor is already an instance ofvisitor_typevisitor_type(so the language-specificvisit_*defaults inJavaVisitor/PythonVisitorare available for child traversal) and forwardspre_visit/post_visit/default_value/is_acceptableplus_cursor/_visit_count/_after_visitto the wrapped visitor — so user-defined logic on the original generic visitor still runs against the right cursor and sees every traversed node.When
adaptis called on an existing adapter (e.g. cross-language traversal: a Py node visited via aJavaVisitoradapter chain), the wrapped visitor is unwrapped and re-wrapped in the correct adapter rather than nesting adapters.is_adaptable_tois updated to match the new semantics: a visitor is adaptable to a target type if it's already an instance, or if it's an existing adapter, or if a registered adapter exists for the target type.Test plan
tests/test_visitor_adapt.pycovers:TreeVisitorsubclass visiting aPy.CompilationUnitno longer raisespre_visitactually fires on the visited node_collect_search_result_ids(which uses aTreeVisitorsubclass) finds aSearchResultmarker on aPyrootadaptis a no-op when the visitor already is-a target typeadaptunwraps an existing adapter rather than stacking adapters~/.moderne/cli/python/openrewrite/8.81.6/) — same minimal repro that crashed before this fix now succeeds andpre_visitis invoked.