Skip to content

Commit d90273d

Browse files
aschemanclaude
andcommitted
Fix #13068: make PluginDependenciesResolver methods default
resolveCoreExtensionAndFlatten and resolvePluginAndFlatten were added as abstract methods in 4.0.0-rc-6, forward-porting the Maven 3.10.0 changes (#12335). The interface is documented as internal, but it is the only hook Maven offers for influencing plugin resolution, and every major Java IDE overrides it: IntelliJ IDEA, Eclipse m2e and NetBeans. Those implementations live out of tree and are compiled against one Maven while running on another, so adding abstract methods turns plugin resolution into an AbstractMethodError. Both methods now default to the pre-existing resolvePlugin, which DefaultPluginDependenciesResolver already treats as an alias -- there, resolvePlugin delegates to resolvePluginAndFlatten. resolvePlugin itself deliberately stays abstract: a default there would let an implementation overriding neither method recurse infinitely. The default for resolveCoreExtensionAndFlatten is best effort. A dedicated implementation additionally reads the extension's artifact descriptor to apply relocations and run MavenPluginDependenciesValidator. Verified against the class shipped in IntelliJ IDEA 2026.2.2, which implements resolvePluginAndFlatten but not resolveCoreExtensionAndFlatten and therefore still fails on stock rc-6. Adds a unit test pinning the compatibility contract and a core IT that builds an extension against maven-core 4.0.0-rc-5 -- the last release before the methods existed -- and runs a build through it. Verified by Christofer Dutz (@chrisdutz) on Apache PLC4X: a build carrying this change imports cleanly in IntelliJ IDEA, where stock rc-6 fails. Closes #13068 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2HnhKt7MJWYsSSVUmPrvU
1 parent 321a9c8 commit d90273d

8 files changed

Lines changed: 473 additions & 4 deletions

File tree

impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PluginDependenciesResolver.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ DependencyNode resolve(
7474

7575
/**
7676
* Resolves the runtime dependencies of the specified core extension (as {@link Plugin} as GAV carrier).
77+
* <p>
78+
* The default implementation delegates to
79+
* {@link #resolvePlugin(Plugin, Artifact, DependencyFilter, List, RepositorySystemSession)} so that
80+
* implementations written against Maven 4.0.0-rc-5 and earlier keep working. Implementations should override
81+
* this method: unlike the default, a dedicated implementation is expected to read the extension's artifact
82+
* descriptor first, applying relocations and dependency validation.
7783
*
7884
* @param plugin The plugin for which to resolve the dependencies, must not be {@code null}.
7985
* @param dependencyFilter A filter to exclude artifacts from resolution (but not collection), may be {@code null}.
@@ -83,15 +89,21 @@ DependencyNode resolve(
8389
* @throws PluginResolutionException If any dependency could not be resolved.
8490
* @since 3.10.0
8591
*/
86-
DependencyResult resolveCoreExtensionAndFlatten(
92+
default DependencyResult resolveCoreExtensionAndFlatten(
8793
Plugin plugin,
8894
DependencyFilter dependencyFilter,
8995
List<RemoteRepository> repositories,
9096
RepositorySystemSession session)
91-
throws PluginResolutionException;
97+
throws PluginResolutionException {
98+
return resolvePlugin(plugin, null, dependencyFilter, repositories, session);
99+
}
92100

93101
/**
94102
* Resolves the runtime dependencies of the specified plugin.
103+
* <p>
104+
* Implementations must not delegate to
105+
* {@link #resolvePluginAndFlatten(Plugin, Artifact, DependencyFilter, List, RepositorySystemSession)} without
106+
* also overriding it: the default implementation of that method delegates back here.
95107
*
96108
* @param plugin The plugin for which to resolve the dependencies, must not be {@code null}.
97109
* @param artifact The plugin's main artifact, may be {@code null}.
@@ -114,6 +126,10 @@ DependencyResult resolvePlugin(
114126

115127
/**
116128
* Resolves the runtime dependencies of the specified plugin.
129+
* <p>
130+
* The default implementation delegates to
131+
* {@link #resolvePlugin(Plugin, Artifact, DependencyFilter, List, RepositorySystemSession)}, which this method
132+
* supersedes, so that implementations written against Maven 4.0.0-rc-5 and earlier keep working.
117133
*
118134
* @param plugin The plugin for which to resolve the dependencies, must not be {@code null}.
119135
* @param pluginArtifact The plugin's main artifact, may be {@code null}.
@@ -124,11 +140,13 @@ DependencyResult resolvePlugin(
124140
* @throws PluginResolutionException If any dependency could not be resolved.
125141
* @since 3.10.0
126142
*/
127-
DependencyResult resolvePluginAndFlatten(
143+
default DependencyResult resolvePluginAndFlatten(
128144
Plugin plugin,
129145
Artifact pluginArtifact,
130146
DependencyFilter dependencyFilter,
131147
List<RemoteRepository> repositories,
132148
RepositorySystemSession session)
133-
throws PluginResolutionException;
149+
throws PluginResolutionException {
150+
return resolvePlugin(plugin, pluginArtifact, dependencyFilter, repositories, session);
151+
}
134152
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.plugin.internal;
20+
21+
import java.lang.reflect.Method;
22+
import java.util.List;
23+
24+
import org.apache.maven.model.Plugin;
25+
import org.apache.maven.plugin.PluginResolutionException;
26+
import org.eclipse.aether.RepositorySystemSession;
27+
import org.eclipse.aether.artifact.Artifact;
28+
import org.eclipse.aether.artifact.DefaultArtifact;
29+
import org.eclipse.aether.graph.DependencyFilter;
30+
import org.eclipse.aether.repository.RemoteRepository;
31+
import org.eclipse.aether.resolution.DependencyRequest;
32+
import org.eclipse.aether.resolution.DependencyResult;
33+
import org.junit.jupiter.api.Test;
34+
35+
import static org.junit.jupiter.api.Assertions.assertNull;
36+
import static org.junit.jupiter.api.Assertions.assertSame;
37+
import static org.junit.jupiter.api.Assertions.assertTrue;
38+
39+
/**
40+
* {@link PluginDependenciesResolver} is documented as internal, but tools that embed Maven — most visibly
41+
* IntelliJ IDEA's {@code Maven40PluginDependenciesResolver} — implement it out of tree and are compiled against
42+
* one Maven version while running against another. When {@code resolveCoreExtensionAndFlatten} and
43+
* {@code resolvePluginAndFlatten} were forward-ported from Maven 3.10.0 as <em>abstract</em> methods, every such
44+
* implementation started failing with {@code AbstractMethodError} as soon as core invoked them.
45+
*
46+
* <p>These tests pin the compatibility contract: an implementation providing only the methods that existed before
47+
* the forward port must remain a legal implementation, and the two newer methods must stay {@code default}.
48+
*/
49+
class PluginDependenciesResolverDefaultMethodsTest {
50+
51+
private static final DependencyResult RESULT = new DependencyResult(new DependencyRequest());
52+
53+
/**
54+
* Implements exactly the method set of the pre-forward-port interface — nothing more. This class failing to
55+
* compile <em>is</em> the regression: it means the two newer methods went back to being abstract.
56+
*/
57+
private static final class LegacyResolver implements PluginDependenciesResolver {
58+
59+
private Plugin plugin;
60+
private Artifact pluginArtifact;
61+
private DependencyFilter dependencyFilter;
62+
private List<RemoteRepository> repositories;
63+
private RepositorySystemSession session;
64+
65+
@Override
66+
public Artifact resolve(Plugin plugin, List<RemoteRepository> repositories, RepositorySystemSession session) {
67+
throw new UnsupportedOperationException();
68+
}
69+
70+
@Override
71+
public org.eclipse.aether.graph.DependencyNode resolve(
72+
Plugin plugin,
73+
Artifact pluginArtifact,
74+
DependencyFilter dependencyFilter,
75+
List<RemoteRepository> repositories,
76+
RepositorySystemSession session) {
77+
throw new UnsupportedOperationException();
78+
}
79+
80+
@Override
81+
public DependencyResult resolvePlugin(
82+
Plugin plugin,
83+
Artifact pluginArtifact,
84+
DependencyFilter dependencyFilter,
85+
List<RemoteRepository> repositories,
86+
RepositorySystemSession session) {
87+
this.plugin = plugin;
88+
this.pluginArtifact = pluginArtifact;
89+
this.dependencyFilter = dependencyFilter;
90+
this.repositories = repositories;
91+
this.session = session;
92+
return RESULT;
93+
}
94+
}
95+
96+
@Test
97+
void resolvePluginAndFlattenDelegatesToResolvePlugin() throws PluginResolutionException {
98+
LegacyResolver resolver = new LegacyResolver();
99+
Plugin plugin = new Plugin();
100+
Artifact artifact = new DefaultArtifact("g:a:1.0");
101+
DependencyFilter filter = (node, parents) -> true;
102+
List<RemoteRepository> repositories = List.of();
103+
104+
assertSame(RESULT, resolver.resolvePluginAndFlatten(plugin, artifact, filter, repositories, null));
105+
106+
assertSame(plugin, resolver.plugin);
107+
assertSame(artifact, resolver.pluginArtifact);
108+
assertSame(filter, resolver.dependencyFilter);
109+
assertSame(repositories, resolver.repositories);
110+
assertNull(resolver.session);
111+
}
112+
113+
@Test
114+
void resolveCoreExtensionAndFlattenDelegatesToResolvePlugin() throws PluginResolutionException {
115+
LegacyResolver resolver = new LegacyResolver();
116+
Plugin plugin = new Plugin();
117+
DependencyFilter filter = (node, parents) -> true;
118+
List<RemoteRepository> repositories = List.of();
119+
120+
assertSame(RESULT, resolver.resolveCoreExtensionAndFlatten(plugin, filter, repositories, null));
121+
122+
assertSame(plugin, resolver.plugin);
123+
assertNull(resolver.pluginArtifact, "the extension's main artifact is resolved from the plugin GAV");
124+
assertSame(filter, resolver.dependencyFilter);
125+
assertSame(repositories, resolver.repositories);
126+
}
127+
128+
/**
129+
* Guards the property that actually broke IntelliJ IDEA: these methods are invoked on implementations compiled
130+
* against an older Maven, so they must carry an implementation in the interface itself. Turning either back into
131+
* an abstract method reintroduces {@code AbstractMethodError} for every out-of-tree implementation.
132+
*/
133+
@Test
134+
void newerMethodsAreDefaultMethods() throws NoSuchMethodException {
135+
Method resolveCoreExtensionAndFlatten = PluginDependenciesResolver.class.getMethod(
136+
"resolveCoreExtensionAndFlatten",
137+
Plugin.class,
138+
DependencyFilter.class,
139+
List.class,
140+
RepositorySystemSession.class);
141+
Method resolvePluginAndFlatten = PluginDependenciesResolver.class.getMethod(
142+
"resolvePluginAndFlatten",
143+
Plugin.class,
144+
Artifact.class,
145+
DependencyFilter.class,
146+
List.class,
147+
RepositorySystemSession.class);
148+
149+
assertTrue(
150+
resolveCoreExtensionAndFlatten.isDefault(),
151+
"resolveCoreExtensionAndFlatten must stay a default method");
152+
assertTrue(resolvePluginAndFlatten.isDefault(), "resolvePluginAndFlatten must stay a default method");
153+
}
154+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.it;
20+
21+
import java.io.File;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
/**
26+
* Regression test for <a href="https://github.com/apache/maven/issues/13068">apache/maven#13068</a>.
27+
* The forward port in <a href="https://github.com/apache/maven/pull/12335">apache/maven#12335</a> added
28+
* {@code resolveCoreExtensionAndFlatten} and {@code resolvePluginAndFlatten} to
29+
* {@code PluginDependenciesResolver} as abstract methods in 4.0.0-rc-6.
30+
* <p>
31+
* That interface is documented as internal, but it is the only hook Maven offers for influencing plugin
32+
* resolution, and every major Java IDE overrides it: IntelliJ IDEA, Eclipse m2e and NetBeans. Because such
33+
* implementations live out of tree and are compiled against one Maven while running on another, adding
34+
* abstract methods turns every plugin resolution into an {@code AbstractMethodError}.
35+
* <p>
36+
* This test builds a core extension against maven-core 4.0.0-rc-5 — the last release before those methods
37+
* existed — installs it, and then runs a build that uses it. If the newer interface methods are abstract,
38+
* the build fails before any goal executes.
39+
*/
40+
class MavenITgh13068LegacyPluginDependenciesResolverTest extends AbstractMavenIntegrationTestCase {
41+
42+
MavenITgh13068LegacyPluginDependenciesResolverTest() {
43+
// resolvePluginAndFlatten / resolveCoreExtensionAndFlatten were introduced in 4.0.0-rc-6
44+
super("[4.0.0-rc-6,)");
45+
}
46+
47+
@Test
48+
void legacyImplementationStillWorks() throws Exception {
49+
File testDir = extractResources("/gh-13068-legacy-plugin-dependencies-resolver");
50+
51+
Verifier extensionVerifier = newVerifier(new File(testDir, "extension").getPath());
52+
extensionVerifier.deleteArtifacts("org.apache.maven.its.gh-13068");
53+
extensionVerifier.addCliArgument("install");
54+
extensionVerifier.execute();
55+
extensionVerifier.verifyErrorFreeLog();
56+
57+
Verifier clientVerifier = newVerifier(new File(testDir, "client").getPath());
58+
clientVerifier.setAutoclean(false);
59+
clientVerifier.addCliArgument("clean");
60+
clientVerifier.execute();
61+
clientVerifier.verifyErrorFreeLog();
62+
63+
// the extension must actually have displaced the default component, otherwise this test
64+
// would pass without ever exercising the interface
65+
clientVerifier.verifyTextInLog("[gh-13068] legacy PluginDependenciesResolver installed");
66+
clientVerifier.verifyTextInLog("[gh-13068] resolvePlugin reached for maven-clean-plugin");
67+
}
68+
}

its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public TestSuiteOrdering() {
103103
* the tests are to finishing. Newer tests are also more likely to fail, so this is
104104
* a fail fast technique as well.
105105
*/
106+
suite.addTestSuite(MavenITgh13068LegacyPluginDependenciesResolverTest.class);
106107
suite.addTestSuite(MavenITmdep0590ClassifiedPomArtifactFromReactorTest.class);
107108
suite.addTestSuite(MavenITgh12660BomVersionFromImportedBomTest.class);
108109
suite.addTestSuite(MavenITgh11346DependencyManagementOverrideTest.class);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
<extensions>
21+
<extension>
22+
<groupId>org.apache.maven.its.gh-13068</groupId>
23+
<artifactId>legacy-plugin-dependencies-resolver</artifactId>
24+
<version>1.0.0-SNAPSHOT</version>
25+
</extension>
26+
</extensions>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
23+
<groupId>org.apache.maven.its.gh-13068</groupId>
24+
<artifactId>client</artifactId>
25+
<version>1.0.0-SNAPSHOT</version>
26+
<packaging>jar</packaging>
27+
28+
<name>Maven Integration Test :: gh-13068 :: client</name>
29+
<description>
30+
Builds with a core extension that overrides PluginDependenciesResolver with a pre-4.0.0-rc-6
31+
implementation. Resolving any plugin routes through that extension.
32+
</description>
33+
</project>

0 commit comments

Comments
 (0)