Skip to content

Commit 7218621

Browse files
authored
Merge branch 'main' into maven-anonymous-first-auth
2 parents 62a101e + ce2b0e2 commit 7218621

164 files changed

Lines changed: 15147 additions & 1583 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/npm-publish.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ jobs:
6060

6161
# Trusted Publisher auto-detection in `npm publish` requires npm >= 11.5.1.
6262
# GitHub-hosted ubuntu runners currently ship Node 22 / npm 10.x, and the
63-
# global install needs sudo to write under /usr/local.
63+
# global install needs sudo to write under /usr/local. Pinned off `@latest`
64+
# because npm 12 breaks provenance publish with `Cannot find module 'sigstore'`
65+
# (https://github.com/npm/cli/issues/9722); revisit once that ships fixed.
6466
- shell: bash
65-
run: sudo npm install -g npm@latest
67+
run: sudo npm install -g npm@11.18.0
6668

6769
# The `ci` run records the exact version string baked into its rewrite-javascript jar
6870
# (META-INF/rewrite-javascript-version.txt) and uploads it as the
@@ -73,7 +75,7 @@ jobs:
7375
# falls back to Gradle's own snapshot-version derivation.
7476
- if: github.event.workflow_run.name == 'ci'
7577
continue-on-error: true
76-
uses: actions/download-artifact@v4
78+
uses: actions/download-artifact@v8
7779
with:
7880
name: published-snapshot-version
7981
path: .published-version

rewrite-core/src/main/java/org/openrewrite/internal/TreeVisitorAdapter.java

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,23 @@
1818
import io.micrometer.core.instrument.Metrics;
1919
import io.micrometer.core.instrument.Timer;
2020
import io.quarkus.gizmo.*;
21+
import org.jspecify.annotations.Nullable;
2122
import org.openrewrite.Cursor;
2223
import org.openrewrite.Tree;
2324
import org.openrewrite.TreeVisitor;
2425

26+
import java.io.BufferedReader;
27+
import java.io.IOException;
28+
import java.io.InputStreamReader;
2529
import java.lang.reflect.*;
30+
import java.net.URL;
31+
import java.nio.charset.StandardCharsets;
32+
import java.util.Arrays;
33+
import java.util.Enumeration;
2634
import java.util.IdentityHashMap;
35+
import java.util.LinkedHashMap;
2736
import java.util.Map;
37+
import java.util.Optional;
2838

2939
public class TreeVisitorAdapter {
3040
private static final Integer classCreationLock = 1;
@@ -106,28 +116,30 @@ public static <T extends Tree, Adapted> Adapted adapt(TreeVisitor<T, ?> delegate
106116
);
107117
setCursor.returnValue(null);
108118

109-
// Walk delegate's class hierarchy (down to but excluding TreeVisitor)
110-
// and collect every visit-like method declared at each level — typed
111-
// overrides AND synthetic bridges. JVM virtual dispatch picks based on
112-
// erased descriptor; e.g., J.Annotation.acceptJava emits
113-
// invokevirtual JavaVisitor.visitAnnotation(J.Annotation, Object) → J,
114-
// and the proxy must define a method with that exact descriptor
115-
// (not just the typed RemoveAnnotationVisitor.visitAnnotation(..., ExecutionContext))
116-
// for the forwarding to actually be the dispatch target.
117-
java.util.LinkedHashMap<MethodKey, Method> methodsByKey = new java.util.LinkedHashMap<>();
118-
for (Method m : delegate.getClass().getDeclaredMethods()) {
119-
if (!isVisitLike(m) || Modifier.isStatic(m.getModifiers()) || Modifier.isPrivate(m.getModifiers())) {
120-
continue;
119+
// Collect the user's visit-like overrides from the leaf up to (but not into) the
120+
// iso-visitor, so overrides declared on a shared superclass are forwarded too.
121+
// Leaf-first, so the most-derived override wins.
122+
Map<MethodKey, Method> methodsByKey = new LinkedHashMap<>();
123+
Class<?> leafClass = delegate.getClass();
124+
for (Class<?> c = leafClass;
125+
c != null && c != Object.class && !TreeVisitor.class.equals(c);
126+
c = c.getSuperclass()) {
127+
if (c == delegateType && c != leafClass) {
128+
break;
129+
}
130+
for (Method m : c.getDeclaredMethods()) {
131+
if (!isVisitLike(m) || Modifier.isStatic(m.getModifiers()) || Modifier.isPrivate(m.getModifiers())) {
132+
continue;
133+
}
134+
methodsByKey.putIfAbsent(new MethodKey(m.getName(), m.getParameterTypes(), m.getReturnType()), m);
135+
}
136+
if (c == delegateType) {
137+
break;
121138
}
122-
MethodKey key = new MethodKey(m.getName(), m.getParameterTypes(), m.getReturnType());
123-
methodsByKey.putIfAbsent(key, m);
124139
}
125140

126-
// For visit*(SpecificNode) override-skip: name+arity match against any
127-
// user-declared (non-synthetic) mixin method. If the mixin overrides
128-
// visitClassDeclaration at one signature, skip generating proxy methods
129-
// for ALL same-name same-arity delegate methods (typed + bridges), so
130-
// dispatch falls through to the mixin via inheritance.
141+
// If the mixin declares its own visitX override, don't proxy the delegate's
142+
// same-name/arity methods (typed or bridge) — let dispatch fall through to it.
131143
for (Method method : methodsByKey.values()) {
132144
boolean isPreOrPost = "preVisit".equals(method.getName()) || "postVisit".equals(method.getName());
133145
boolean mixinOverridesByName = mixinClass != null && !isPreOrPost &&
@@ -206,11 +218,8 @@ public static <T extends Tree, Adapted> Adapted adapt(TreeVisitor<T, ?> delegate
206218
}
207219

208220
private static boolean declaresUserOverrideByNameAndArity(Class<?> mixinClass, Class<?> adaptTo, Method baseMethod) {
209-
// Only consider methods declared on the immediate mixin class —
210-
// that is, what the user wrote. Methods inherited from the language
211-
// iso-visitor (e.g., JavaIsoVisitor declares visitIdentifier) or from
212-
// adaptTo are ambient, not real overrides, and would otherwise cause
213-
// us to skip generating forwarding for nearly every visit method.
221+
// Only methods the user actually declared on the mixin count; inherited iso-visitor/adaptTo
222+
// methods are ambient, not real overrides.
214223
for (Method m : mixinClass.getDeclaredMethods()) {
215224
if (m.isSynthetic() || m.isBridge()) {
216225
continue;
@@ -242,12 +251,12 @@ public boolean equals(Object o) {
242251
MethodKey other = (MethodKey) o;
243252
return name.equals(other.name) &&
244253
returnType.equals(other.returnType) &&
245-
java.util.Arrays.equals(paramTypes, other.paramTypes);
254+
Arrays.equals(paramTypes, other.paramTypes);
246255
}
247256

248257
@Override
249258
public int hashCode() {
250-
return name.hashCode() * 31 + returnType.hashCode() + java.util.Arrays.hashCode(paramTypes);
259+
return name.hashCode() * 31 + returnType.hashCode() + Arrays.hashCode(paramTypes);
251260
}
252261
}
253262

@@ -324,13 +333,11 @@ private static void copyMixinInstanceFields(TreeVisitor<?, ?> mixin, Object prox
324333
}
325334
}
326335

327-
private static @org.jspecify.annotations.Nullable TreeVisitor<?, ?> discoverRegisteredMixin(
336+
private static @Nullable TreeVisitor<?, ?> discoverRegisteredMixin(
328337
TreeVisitorAdapterClassLoader cache, TreeVisitor<?, ?> delegate, Class<?> adaptTo) {
329-
// adapt() is invoked once per node of a foreign-language subtree, so the
330-
// classpath scan in discoverRegisteredMixinClass would otherwise run per node.
331-
// The cache resolves the mixin class (or "none") keyed by delegate + adaptTo,
332-
// then we instantiate a fresh mixin per call (the proxy copies the mixin's fields).
333-
java.util.Optional<Class<?>> mixinClass = cache.mixinClass(delegate.getClass(), adaptTo);
338+
// Cache the per-(delegate, adaptTo) classpath scan — adapt() runs once per node — and
339+
// instantiate a fresh mixin each call (the proxy copies its fields).
340+
Optional<Class<?>> mixinClass = cache.mixinClass(delegate.getClass(), adaptTo);
334341
if (!mixinClass.isPresent()) {
335342
return null;
336343
}
@@ -342,34 +349,25 @@ private static void copyMixinInstanceFields(TreeVisitor<?, ?> mixin, Object prox
342349
}
343350
}
344351

345-
static java.util.Optional<Class<?>> discoverRegisteredMixinClass(Class<?> delegateClass, Class<?> adaptTo) {
346-
// Each language module ships per-base-visitor registry files at
347-
// META-INF/rewrite/mixins/<base-visitor-fqn>
348-
// listing mixin classes that should compose with that base. Same
349-
// namespace already used by rewrite-YAML resources, not the JDK
350-
// ServiceLoader namespace — we don't use {@link ServiceLoader} because
351-
// its subtype check rejects mixins (a mixin extends the language
352-
// module's iso-visitor, e.g. KotlinIsoVisitor, not the base visitor
353-
// it composes with, e.g. RemoveAnnotationVisitor). The visitor role
354-
// is shared structurally, not by inheritance.
355-
//
356-
// Anchor resource lookup to adaptTo's classloader so the language
357-
// module's own registry files (shipped in the language module's JAR)
358-
// are visible.
352+
static Optional<Class<?>> discoverRegisteredMixinClass(Class<?> delegateClass, Class<?> adaptTo) {
353+
// Mixins are registered in META-INF/rewrite/mixins/<base-visitor-fqn>. Not ServiceLoader:
354+
// its subtype check rejects mixins, which extend the language iso-visitor (e.g.
355+
// KotlinIsoVisitor) rather than the base visitor they compose with. Anchor lookup to
356+
// adaptTo's classloader so the language module's own registry files are visible.
359357
ClassLoader cl = adaptTo.getClassLoader();
360358
if (cl == null) {
361359
cl = Thread.currentThread().getContextClassLoader();
362360
}
363361
if (cl == null) {
364-
return java.util.Optional.empty();
362+
return Optional.empty();
365363
}
366364
String resource = "META-INF/rewrite/mixins/" + delegateClass.getName();
367365
try {
368-
java.util.Enumeration<java.net.URL> urls = cl.getResources(resource);
366+
Enumeration<URL> urls = cl.getResources(resource);
369367
while (urls.hasMoreElements()) {
370-
java.net.URL url = urls.nextElement();
371-
try (java.io.BufferedReader reader = new java.io.BufferedReader(
372-
new java.io.InputStreamReader(url.openStream(), java.nio.charset.StandardCharsets.UTF_8))) {
368+
URL url = urls.nextElement();
369+
try (BufferedReader reader = new BufferedReader(
370+
new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
373371
String line;
374372
while ((line = reader.readLine()) != null) {
375373
int hash = line.indexOf('#');
@@ -389,18 +387,18 @@ static java.util.Optional<Class<?>> discoverRegisteredMixinClass(Class<?> delega
389387
if (!adaptTo.isAssignableFrom(mixinClass)) {
390388
continue;
391389
}
392-
return java.util.Optional.of(mixinClass);
390+
return Optional.of(mixinClass);
393391
} catch (ClassNotFoundException e) {
394392
throw new IllegalStateException(
395393
"Failed to load registered mixin " + line + " (from " + url + ").", e);
396394
}
397395
}
398396
}
399397
}
400-
} catch (java.io.IOException e) {
398+
} catch (IOException e) {
401399
// Couldn't enumerate mixin registry resources; treat as no registration.
402400
}
403-
return java.util.Optional.empty();
401+
return Optional.empty();
404402
}
405403

406404
@SuppressWarnings("rawtypes")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.trait;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.openrewrite.Cursor;
20+
import org.openrewrite.SourceFile;
21+
import org.openrewrite.text.PlainTextParser;
22+
23+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
24+
25+
class CommentsTest {
26+
27+
@Test
28+
void errorsOnSourceFileWithoutCommentService() {
29+
SourceFile text = new PlainTextParser().parse("hello").findFirst().orElseThrow();
30+
Comments comments = Comments.of(new Cursor(new Cursor(null, Cursor.ROOT_VALUE), text));
31+
assertThatThrownBy(comments::getComments)
32+
.isInstanceOf(UnsupportedOperationException.class)
33+
.hasMessageContaining("CommentService");
34+
assertThatThrownBy(() -> comments.comment(" not supported here"))
35+
.isInstanceOf(UnsupportedOperationException.class)
36+
.hasMessageContaining("CommentService");
37+
}
38+
39+
@Test
40+
void requiresCursorRootedAtSourceFile() {
41+
Comments comments = Comments.of(new Cursor(null, Cursor.ROOT_VALUE));
42+
assertThatThrownBy(() -> comments.comment(" x"))
43+
.isInstanceOf(IllegalStateException.class)
44+
.hasMessageContaining("SourceFile");
45+
}
46+
}

rewrite-csharp/src/main/java/org/openrewrite/csharp/marketplace/NuGetRecipeBundleResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public RecipeBundleReader resolve(RecipeBundle bundle) {
4444
// directory may differ from ours, so it needs the absolute form to load the assembly —
4545
// and aligning the bundle's packageName with it keeps the install origin the server
4646
// records and the marketplace filter key (the bundle's packageName) the same value.
47-
String absolutePath = pkgPath.toAbsolutePath().toString();
47+
String absolutePath = pkgPath.toAbsolutePath().normalize().toString();
4848
bundle.setPackageName(absolutePath);
4949
response = rpc.installRecipes(absolutePath, bundle.getVersion());
5050
} else {

0 commit comments

Comments
 (0)