From 8de2fac83fb2c24136f7340c80e30fe17728d4d8 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 22:07:07 +0200 Subject: [PATCH 1/4] MPLUGIN-421: Add comment-only source reproducer --- .../src/it/mplugin-421/invoker.properties | 18 +++++ .../src/it/mplugin-421/pom.xml | 77 +++++++++++++++++++ .../maven/plugin/coreit/CommentOnly.java | 20 +++++ .../apache/maven/plugin/coreit/HelloMojo.java | 32 ++++++++ .../src/it/mplugin-421/verify.groovy | 32 ++++++++ pom.xml | 2 + 6 files changed, 181 insertions(+) create mode 100644 maven-plugin-plugin/src/it/mplugin-421/invoker.properties create mode 100644 maven-plugin-plugin/src/it/mplugin-421/pom.xml create mode 100644 maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/CommentOnly.java create mode 100644 maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java create mode 100644 maven-plugin-plugin/src/it/mplugin-421/verify.groovy diff --git a/maven-plugin-plugin/src/it/mplugin-421/invoker.properties b/maven-plugin-plugin/src/it/mplugin-421/invoker.properties new file mode 100644 index 00000000..a5ed592b --- /dev/null +++ b/maven-plugin-plugin/src/it/mplugin-421/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +invoker.goals = clean verify diff --git a/maven-plugin-plugin/src/it/mplugin-421/pom.xml b/maven-plugin-plugin/src/it/mplugin-421/pom.xml new file mode 100644 index 00000000..2321a47f --- /dev/null +++ b/maven-plugin-plugin/src/it/mplugin-421/pom.xml @@ -0,0 +1,77 @@ + + + + + + 4.0.0 + + org.apache.maven.its.mplugin-421 + mplugin-421 + 1.0-SNAPSHOT + maven-plugin + + Maven Integration Test :: mplugin-421 + Checks that comment-only Java sources do not prevent plugin descriptor generation. + + + UTF-8 + + + + + org.apache.maven + maven-plugin-api + @maven3Version@ + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + @project.version@ + provided + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @version.maven-compiler-plugin@ + + 1.8 + 1.8 + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + @project.version@ + + mplugin421 + + + + + diff --git a/maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/CommentOnly.java b/maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/CommentOnly.java new file mode 100644 index 00000000..7d50ac21 --- /dev/null +++ b/maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/CommentOnly.java @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// This file intentionally contains no Java declaration. diff --git a/maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java b/maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java new file mode 100644 index 00000000..c2b7a2fa --- /dev/null +++ b/maven-plugin-plugin/src/it/mplugin-421/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.coreit; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Mojo; + +/** Says hello. */ +@Mojo(name = "hello") +public class HelloMojo extends AbstractMojo { + + @Override + public void execute() { + getLog().info("Hello"); + } +} diff --git a/maven-plugin-plugin/src/it/mplugin-421/verify.groovy b/maven-plugin-plugin/src/it/mplugin-421/verify.groovy new file mode 100644 index 00000000..df29f2e1 --- /dev/null +++ b/maven-plugin-plugin/src/it/mplugin-421/verify.groovy @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import groovy.xml.XmlParser + +File descriptorFile = new File( basedir, "target/classes/META-INF/maven/plugin.xml" ) +assert descriptorFile.isFile() + +def pluginDescriptor = new XmlParser().parse( descriptorFile ) +def mojo = pluginDescriptor.mojos.mojo.find { it.goal.text() == "hello" } + +assert mojo != null +assert mojo.implementation.text() == "org.apache.maven.plugin.coreit.HelloMojo" +assert mojo.description.text() == "Says hello." + +return true diff --git a/pom.xml b/pom.xml index ba5dbf80..46641ee6 100644 --- a/pom.xml +++ b/pom.xml @@ -345,6 +345,8 @@ **/it/source-encoding/ + + **/it/mplugin-421/**/CommentOnly.java From 368df4f7f9d78bfa5193e4c1e53a40d49831464f Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 22:09:25 +0200 Subject: [PATCH 2/4] MPLUGIN-421: Add JavaParser source model --- .../base/pom.xml | 32 +++ .../maven/plugin/coreit/PlatformType.java | 65 +++++ .../invoker.properties | 19 ++ .../plugin/pom.xml | 66 +++++ .../apache/maven/plugin/coreit/HelloMojo.java | 32 +++ .../javaparser-platform-classloader/pom.xml | 57 +++++ .../verify.groovy | 35 +++ maven-plugin-tools-annotations/pom.xml | 4 + .../annotations/JavaSourceModel.java | 230 ++++++++++++++++++ .../annotations/JavaSourceModelTest.java | 179 ++++++++++++++ pom.xml | 6 + 11 files changed, 725 insertions(+) create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/base/pom.xml create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/base/src/main/java/org/apache/maven/plugin/coreit/PlatformType.java create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/invoker.properties create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/pom.xml create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/pom.xml create mode 100644 maven-plugin-plugin/src/it/javaparser-platform-classloader/verify.groovy create mode 100644 maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java create mode 100644 maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModelTest.java diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/base/pom.xml b/maven-plugin-plugin/src/it/javaparser-platform-classloader/base/pom.xml new file mode 100644 index 00000000..66714836 --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/base/pom.xml @@ -0,0 +1,32 @@ + + + + + + 4.0.0 + + + org.apache.maven.its.javaparser-platform-classloader + javaparser-platform-classloader + 1.0-SNAPSHOT + + + base + diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/base/src/main/java/org/apache/maven/plugin/coreit/PlatformType.java b/maven-plugin-plugin/src/it/javaparser-platform-classloader/base/src/main/java/org/apache/maven/plugin/coreit/PlatformType.java new file mode 100644 index 00000000..b7551f33 --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/base/src/main/java/org/apache/maven/plugin/coreit/PlatformType.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.coreit; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.util.Properties; +import java.util.logging.Logger; + +/** A reactor class whose interface is loaded by the platform classloader on Java 9 and later. */ +public class PlatformType implements Driver { + + @Override + public Connection connect(String url, Properties info) throws SQLException { + return null; + } + + @Override + public boolean acceptsURL(String url) throws SQLException { + return false; + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return new DriverPropertyInfo[0]; + } + + @Override + public int getMajorVersion() { + return 1; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public boolean jdbcCompliant() { + return false; + } + + @Override + public Logger getParentLogger() { + return Logger.getGlobal(); + } +} diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/invoker.properties b/maven-plugin-plugin/src/it/javaparser-platform-classloader/invoker.properties new file mode 100644 index 00000000..e6ff81c3 --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/invoker.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +invoker.java.version = 9+ +invoker.goals = clean process-classes diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/pom.xml b/maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/pom.xml new file mode 100644 index 00000000..9ddde97f --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/pom.xml @@ -0,0 +1,66 @@ + + + + + + 4.0.0 + + + org.apache.maven.its.javaparser-platform-classloader + javaparser-platform-classloader + 1.0-SNAPSHOT + + + plugin + maven-plugin + + + + ${project.groupId} + base + ${project.version} + + + org.apache.maven + maven-plugin-api + @maven3Version@ + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + @project.version@ + provided + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + @project.version@ + + platform-classloader + + + + + diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java b/maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java new file mode 100644 index 00000000..b516e63c --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/plugin/src/main/java/org/apache/maven/plugin/coreit/HelloMojo.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.coreit; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Mojo; + +/** Links to {@link PlatformType}. */ +@Mojo(name = "hello") +public class HelloMojo extends AbstractMojo { + + @Override + public void execute() { + getLog().info("Hello"); + } +} diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/pom.xml b/maven-plugin-plugin/src/it/javaparser-platform-classloader/pom.xml new file mode 100644 index 00000000..a53ea4db --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/pom.xml @@ -0,0 +1,57 @@ + + + + + + 4.0.0 + + org.apache.maven.its.javaparser-platform-classloader + javaparser-platform-classloader + 1.0-SNAPSHOT + pom + + Maven Integration Test :: JavaParser Platform Classloader + Checks source resolution for reactor classes that use JDK platform modules. + + + base + plugin + + + + UTF-8 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @version.maven-compiler-plugin@ + + 8 + 8 + + + + + + diff --git a/maven-plugin-plugin/src/it/javaparser-platform-classloader/verify.groovy b/maven-plugin-plugin/src/it/javaparser-platform-classloader/verify.groovy new file mode 100644 index 00000000..792cf4f2 --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-platform-classloader/verify.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import groovy.xml.XmlParser + +File descriptorFile = new File( basedir, "plugin/target/classes/META-INF/maven/plugin.xml" ) +assert descriptorFile.isFile() + +def pluginDescriptor = new XmlParser().parse( descriptorFile ) +def mojo = pluginDescriptor.mojos.mojo.find { it.goal.text() == "hello" } + +assert mojo != null +assert mojo.description.text().contains( "PlatformType" ) + +File buildLog = new File( basedir, "build.log" ) +assert buildLog.isFile() +assert !buildLog.text.contains( "Unresolvable link in javadoc tag with value PlatformType" ) + +return true diff --git a/maven-plugin-tools-annotations/pom.xml b/maven-plugin-tools-annotations/pom.xml index 090da590..8753d2c1 100644 --- a/maven-plugin-tools-annotations/pom.xml +++ b/maven-plugin-tools-annotations/pom.xml @@ -100,6 +100,10 @@ com.thoughtworks.qdox qdox + + com.github.javaparser + javaparser-symbol-solver-core + diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java new file mode 100644 index 00000000..1d6562c7 --- /dev/null +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.tools.plugin.extractor.annotations; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.modules.ModuleExportsDirective; +import com.github.javaparser.resolution.TypeSolver; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.model.SymbolReference; +import com.github.javaparser.symbolsolver.JavaSymbolSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import com.github.javaparser.utils.SourceRoot; + +/** Source declarations and type resolution used while extracting Javadocs. */ +final class JavaSourceModel implements Closeable { + + private final Charset encoding; + private final Set sourceDirectories = new LinkedHashSet<>(); + private final Set classPathEntries = new LinkedHashSet<>(); + private final Map> types = new LinkedHashMap<>(); + private final Map modulesByExportedPackage = new LinkedHashMap<>(); + private final Set packages = new LinkedHashSet<>(); + + private ParserConfiguration parserConfiguration; + private CombinedTypeSolver typeSolver; + private URLClassLoader classPathLoader; + private boolean parsed; + + JavaSourceModel(Charset encoding) { + this.encoding = encoding; + } + + void addSourceDirectory(File directory) throws IOException { + if (directory != null && directory.isDirectory()) { + sourceDirectories.add(directory.toPath().toRealPath()); + } + } + + void addClassPathEntry(File entry) throws IOException { + if (entry != null && entry.exists()) { + classPathEntries.add(entry.toPath().toRealPath()); + } + } + + void parse() throws IOException { + if (parsed) { + return; + } + parsed = true; + + parserConfiguration = new ParserConfiguration() + .setCharacterEncoding(encoding) + .setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE); + typeSolver = new CombinedTypeSolver(); + for (Path sourceDirectory : sourceDirectories) { + typeSolver.add(new JavaParserTypeSolver(sourceDirectory, parserConfiguration)); + } + + List classPathUrls = new ArrayList<>(); + for (Path classPathEntry : classPathEntries) { + if (Files.isDirectory(classPathEntry)) { + classPathUrls.add(classPathEntry.toUri().toURL()); + indexClassDirectory(classPathEntry); + } else if (classPathEntry.getFileName().toString().endsWith(".jar")) { + classPathUrls.add(classPathEntry.toUri().toURL()); + JarTypeSolver jarTypeSolver = new JarTypeSolver(classPathEntry); + typeSolver.add(jarTypeSolver); + jarTypeSolver.getKnownClasses().stream() + .map(JavaSourceModel::packageName) + .filter(name -> !name.isEmpty()) + .forEach(packages::add); + } + } + if (!classPathUrls.isEmpty()) { + classPathLoader = new URLClassLoader(classPathUrls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()); + typeSolver.add(new ClassLoaderTypeSolver(classPathLoader)); + } + typeSolver.add(new ReflectionTypeSolver(ReflectionTypeSolver.JCL_ONLY)); + parserConfiguration.setSymbolResolver(new JavaSymbolSolver(typeSolver)); + + for (Path sourceDirectory : sourceDirectories) { + SourceRoot sourceRoot = new SourceRoot(sourceDirectory, parserConfiguration); + for (ParseResult result : sourceRoot.tryToParse()) { + if (!result.isSuccessful()) { + String path = result.getSourcePath().map(Path::toString).orElse(sourceDirectory.toString()); + String problems = result.getProblems().stream() + .map(Object::toString) + .collect(Collectors.joining(System.lineSeparator())); + throw new IOException("Unable to parse " + path + System.lineSeparator() + problems); + } + CompilationUnit unit = result.getResult().orElseThrow( + () -> new IOException("Parser returned no compilation unit for " + result.getSourcePath())); + index(unit); + } + } + } + + private void indexClassDirectory(Path directory) throws IOException { + try (Stream entries = Files.walk(directory)) { + entries.filter(Files::isRegularFile) + .map(directory::relativize) + .filter(path -> path.getFileName().toString().endsWith(".class")) + .map(Path::getParent) + .filter(path -> path != null) + .map(Path::toString) + .map(name -> name.replace(File.separatorChar, '.')) + .filter(name -> !name.isEmpty()) + .forEach(packages::add); + } + } + + private void index(CompilationUnit unit) { + unit.getPackageDeclaration() + .map(declaration -> declaration.getName().asString()) + .ifPresent(packages::add); + for (TypeDeclaration type : unit.findAll(TypeDeclaration.class)) { + type.getFullyQualifiedName().ifPresent(name -> types.putIfAbsent(name, type)); + } + unit.getModule().ifPresent(module -> module.getDirectives().stream() + .filter(ModuleExportsDirective.class::isInstance) + .map(ModuleExportsDirective.class::cast) + .forEach(exports -> modulesByExportedPackage.putIfAbsent( + exports.getName().asString(), module.getName().asString()))); + } + + Collection> getTypes() { + return Collections.unmodifiableCollection(types.values()); + } + + Optional> getType(String fullyQualifiedName) { + return Optional.ofNullable(types.get(fullyQualifiedName)); + } + + Optional> getType(ResolvedReferenceTypeDeclaration declaration) { + return getType(declaration.getQualifiedName()); + } + + Optional getModuleName(String packageName) { + return Optional.ofNullable(modulesByExportedPackage.get(packageName)); + } + + boolean hasPackage(String packageName) { + return packages.contains(packageName) || Package.getPackage(packageName) != null; + } + + boolean isInternal(ResolvedReferenceTypeDeclaration declaration) { + return types.containsKey(declaration.getQualifiedName()); + } + + Optional resolveType(String fullyQualifiedName) { + ensureParsed(); + SymbolReference reference = typeSolver.tryToSolveType(fullyQualifiedName); + return reference.isSolved() ? Optional.of(reference.getCorrespondingDeclaration()) : Optional.empty(); + } + + TypeSolver getTypeSolver() { + ensureParsed(); + return typeSolver; + } + + String getLocation(Node node, int fallbackLine) { + int line = node.getBegin().map(position -> position.line).orElse(fallbackLine); + return node.findCompilationUnit() + .flatMap(CompilationUnit::getStorage) + .map(storage -> storage.getPath().toString() + ":" + line) + .orElse("unknown:" + line); + } + + private void ensureParsed() { + if (!parsed) { + throw new IllegalStateException("Java source model has not been parsed"); + } + } + + private static String packageName(String className) { + int separator = className.lastIndexOf('.'); + return separator > 0 ? className.substring(0, separator) : ""; + } + + @Override + public void close() throws IOException { + if (classPathLoader != null) { + classPathLoader.close(); + } + } +} diff --git a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModelTest.java b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModelTest.java new file mode 100644 index 00000000..be6ab0c8 --- /dev/null +++ b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModelTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.tools.plugin.extractor.annotations; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavaSourceModelTest { + + @TempDir + Path sourceDirectory; + + @Test + void acceptsCommentOnlyAndCurrentSyntax() throws Exception { + write("CommentOnly.java", "// no declaration\n"); + write( + "example/Model.java", + "package example;\n" + + "import java.lang.annotation.ElementType;\n" + + "import java.lang.annotation.Target;\n" + + "import java.util.List;\n" + + "@Target(ElementType.TYPE_USE) @interface Nullable {}\n" + + "class Model { List<@Nullable String> values; }\n"); + + try (JavaSourceModel model = new JavaSourceModel(StandardCharsets.UTF_8)) { + model.addSourceDirectory(sourceDirectory.toFile()); + model.parse(); + + assertTrue(model.getType("example.Model").isPresent()); + assertTrue(model.resolveType("java.lang.String").isPresent()); + } + } + + @Test + void indexesNestedTypesAndExportedModules() throws Exception { + write("module-info.java", "module example.module { exports example; }\n"); + write("example/Outer.java", "package example; public class Outer { public static class Nested {} }\n"); + + try (JavaSourceModel model = new JavaSourceModel(StandardCharsets.UTF_8)) { + model.addSourceDirectory(sourceDirectory.toFile()); + model.parse(); + + assertTrue(model.getType("example.Outer.Nested").isPresent()); + assertEquals("example.module", model.getModuleName("example").orElse(null)); + assertTrue(model.hasPackage("example")); + } + } + + @Test + void reportsSourcePathForInvalidJava() throws Exception { + Path source = write("example/Broken.java", "package example; class Broken {\n"); + + try (JavaSourceModel model = new JavaSourceModel(StandardCharsets.UTF_8)) { + model.addSourceDirectory(sourceDirectory.toFile()); + IOException exception = assertThrows(IOException.class, model::parse); + assertTrue(exception.getMessage().contains(source.toString())); + } + } + + @Test + void resolvesDirectoryClassesAgainstJarEntriesAndIndexesTheirPackages() throws Exception { + Path dependencySource = write( + sourceDirectory.resolve("dependency-sources"), + "dependency/Parent.java", + "package dependency; public class Parent {}\n"); + Path dependencyClasses = sourceDirectory.resolve("dependency-classes"); + compile(dependencySource, dependencyClasses); + Path dependencyJar = jar(dependencyClasses, sourceDirectory.resolve("dependency.jar")); + + Path reactorSource = write( + sourceDirectory.resolve("reactor-sources"), + "reactor/Child.java", + "package reactor; public class Child extends dependency.Parent {}\n"); + Path reactorClasses = sourceDirectory.resolve("reactor-classes"); + compile(reactorSource, reactorClasses, dependencyJar); + + try (JavaSourceModel model = new JavaSourceModel(StandardCharsets.UTF_8)) { + model.addClassPathEntry(reactorClasses.toFile()); + model.addClassPathEntry(dependencyJar.toFile()); + model.parse(); + + ResolvedReferenceTypeDeclaration child = + model.resolveType("reactor.Child").orElseThrow(AssertionError::new); + assertTrue(child.getAncestors().stream() + .anyMatch(ancestor -> "dependency.Parent".equals(ancestor.getQualifiedName()))); + assertTrue(model.hasPackage("reactor")); + } + } + + @Test + void resolvesClassesFromTheCompleteJavaClassLibrary() throws Exception { + try (JavaSourceModel model = new JavaSourceModel(StandardCharsets.UTF_8)) { + model.parse(); + + assertTrue(model.resolveType("org.w3c.dom.Document").isPresent()); + assertTrue(model.resolveType("org.xml.sax.InputSource").isPresent()); + } + } + + private Path write(String relativePath, String source) throws IOException { + return write(sourceDirectory, relativePath, source); + } + + private static Path write(Path directory, String relativePath, String source) throws IOException { + Path path = directory.resolve(relativePath); + Files.createDirectories(path.getParent()); + Files.write(path, source.getBytes(StandardCharsets.UTF_8)); + return path; + } + + private static void compile(Path source, Path outputDirectory, Path... classPath) throws IOException { + Files.createDirectories(outputDirectory); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler); + List arguments = new ArrayList<>(); + arguments.add("-d"); + arguments.add(outputDirectory.toString()); + if (classPath.length > 0) { + arguments.add("-classpath"); + arguments.add(Arrays.stream(classPath).map(Path::toString).collect(Collectors.joining(File.pathSeparator))); + } + arguments.add(source.toString()); + assertEquals(0, compiler.run(null, null, null, arguments.toArray(new String[0]))); + } + + private static Path jar(Path classesDirectory, Path jarFile) throws IOException { + List classFiles; + try (Stream files = Files.walk(classesDirectory)) { + classFiles = files.filter(Files::isRegularFile).collect(Collectors.toList()); + } + try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jarFile))) { + for (Path classFile : classFiles) { + String name = classesDirectory.relativize(classFile).toString().replace(File.separatorChar, '/'); + output.putNextEntry(new JarEntry(name)); + Files.copy(classFile, output); + output.closeEntry(); + } + } + return jarFile; + } +} diff --git a/pom.xml b/pom.xml index 46641ee6..9ca5ab55 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,7 @@ maven-plugin-tools-annotations/src/main/java/o/a/m/tools/plugins/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor#CLASS_VERSION_TO_JAVA_STRING needs to be updated as well --> 9.10.1 + 3.28.2 4.0.3 3.0.1 4.0.0 @@ -239,6 +240,11 @@ qdox 2.2.0 + + com.github.javaparser + javaparser-symbol-solver-core + ${javaParserVersion} + org.jsoup From 527f0ca3f2188bd2c359d9505a89c8982b3cc40f Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 22:22:04 +0200 Subject: [PATCH 3/4] MPLUGIN-421: Migrate Javadoc extraction to JavaParser --- ...avaAnnotationsMojoDescriptorExtractor.java | 460 +++++------- .../annotations/JavaSourceModel.java | 65 +- .../converter/JavaClassConverterContext.java | 674 +++++++++--------- .../JavaClassConverterContextTest.java | 25 +- 4 files changed, 599 insertions(+), 625 deletions(-) diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java index b44f3c37..34357c85 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java @@ -23,9 +23,9 @@ import javax.inject.Singleton; import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -42,14 +42,16 @@ import java.util.TreeSet; import java.util.stream.Collectors; -import com.thoughtworks.qdox.JavaProjectBuilder; -import com.thoughtworks.qdox.library.SortedClassLibraryBuilder; -import com.thoughtworks.qdox.model.DocletTag; -import com.thoughtworks.qdox.model.JavaAnnotatedElement; -import com.thoughtworks.qdox.model.JavaClass; -import com.thoughtworks.qdox.model.JavaField; -import com.thoughtworks.qdox.model.JavaMember; -import com.thoughtworks.qdox.model.JavaMethod; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import com.github.javaparser.resolution.UnsolvedSymbolException; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.versioning.ComparableVersion; import org.apache.maven.plugin.descriptor.InvalidParameterException; @@ -206,9 +208,6 @@ public List execute(PluginToolsRequest request) request.setRequiredJavaVersion(requiredJavaVersion); } } - JavaProjectBuilder builder = scanJavadoc(request, mojoAnnotatedClasses.values()); - Map javaClassesMap = discoverClasses(builder); - final JavadocLinkGenerator linkGenerator; if (request.getInternalJavadocBaseUrl() != null || (request.getExternalJavadocBaseUrls() != null @@ -222,7 +221,12 @@ public List execute(PluginToolsRequest request) linkGenerator = null; } - populateDataFromJavadoc(builder, mojoAnnotatedClasses, javaClassesMap, linkGenerator); + try (JavaSourceModel sourceModel = scanJavadoc(request, mojoAnnotatedClasses.values())) { + Map> javaClassesMap = discoverClasses(sourceModel); + populateDataFromJavadoc(sourceModel, mojoAnnotatedClasses, javaClassesMap, linkGenerator); + } catch (IOException e) { + throw new ExtractionException("Could not parse Java sources: " + e.getMessage(), e); + } return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor()); } @@ -242,9 +246,8 @@ private Map scanAnnotations(PluginToolsRequest reque return result; } - private JavaProjectBuilder scanJavadoc( - PluginToolsRequest request, Collection mojoAnnotatedClasses) - throws ExtractionException { + private JavaSourceModel scanJavadoc(PluginToolsRequest request, Collection mojoAnnotatedClasses) + throws ExtractionException, IOException { // found artifact from reactors to scan sources // we currently only scan sources from reactors List mavenProjects = new ArrayList<>(); @@ -252,9 +255,10 @@ private JavaProjectBuilder scanJavadoc( // if we need to scan sources from external artifacts Set externalArtifacts = new HashSet<>(); - JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder()); - builder.setEncoding(request.getEncoding()); - extendJavaProjectBuilder(request, builder, request.getProject()); + Charset encoding = + request.getEncoding() == null ? StandardCharsets.UTF_8 : Charset.forName(request.getEncoding()); + JavaSourceModel sourceModel = new JavaSourceModel(encoding); + extendJavaSourceModel(request, sourceModel, request.getProject()); for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) { if (Objects.equals( @@ -282,17 +286,18 @@ private JavaProjectBuilder scanJavadoc( for (Artifact artifact : externalArtifacts) { // parameter for test-sources too ?? olamy I need that for it test only if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) { - extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "test-sources"); + extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "test-sources"); } else { - extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "sources"); + extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "sources"); } } for (MavenProject mavenProject : mavenProjects) { - extendJavaProjectBuilder(request, builder, mavenProject); + extendJavaSourceModel(request, sourceModel, mavenProject); } - return builder; + sourceModel.parse(); + return sourceModel; } private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) { @@ -303,281 +308,191 @@ private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedC * from sources scan to get @since and @deprecated and description of classes and fields. */ protected void populateDataFromJavadoc( - JavaProjectBuilder javaProjectBuilder, + JavaSourceModel sourceModel, Map mojoAnnotatedClasses, - Map javaClassesMap, + Map> javaClassesMap, JavadocLinkGenerator linkGenerator) { - for (Map.Entry entry : mojoAnnotatedClasses.entrySet()) { - JavaClass javaClass = javaClassesMap.get(entry.getKey()); + TypeDeclaration javaClass = javaClassesMap.get(entry.getKey()); if (javaClass == null) { continue; } - // populate class-level content - MojoAnnotationContent mojoAnnotationContent = entry.getValue().getMojo(); - if (mojoAnnotationContent != null) { + MojoAnnotationContent mojo = entry.getValue().getMojo(); + if (mojo != null) { JavaClassConverterContext context = new JavaClassConverterContext( - javaClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, javaClass.getLineNumber()); - mojoAnnotationContent.setDescription(getDescriptionFromElement(javaClass, context)); - - DocletTag since = findInClassHierarchy(javaClass, "since"); - if (since != null) { - mojoAnnotationContent.setSince(getRawValueFromTaglet(since, context)); - } - - DocletTag deprecated = findInClassHierarchy(javaClass, "deprecated"); - if (deprecated != null) { - mojoAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context)); - } + javaClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber(javaClass)); + mojo.setDescription(getDescriptionFromElement(javaClass, context)); + findInClassHierarchy(sourceModel, javaClass, "since") + .ifPresent(tag -> mojo.setSince(getRawValueFromTaglet(tag, context))); + findInClassHierarchy(sourceModel, javaClass, "deprecated") + .ifPresent(tag -> mojo.setDeprecated(getRawValueFromTaglet(tag, context))); } - Map fieldsMap = extractFieldsAnnotations(javaClass, javaClassesMap); - Map methodsMap = extractMethodsAnnotations(javaClass, javaClassesMap); - - // populate parameters + Map fields = extractFields(sourceModel, javaClass); + Map methods = extractMethods(sourceModel, javaClass); Map parameters = - getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses); - parameters = new TreeMap<>(parameters); + new TreeMap<>(getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses)); for (Map.Entry parameter : parameters.entrySet()) { - JavaAnnotatedElement element; - if (parameter.getValue().isAnnotationOnMethod()) { - element = methodsMap.get(parameter.getKey()); - } else { - element = fieldsMap.get(parameter.getKey()); - } - - if (element == null) { - continue; - } - - JavaClassConverterContext context = new JavaClassConverterContext( - javaClass, ((JavaMember) element).getDeclaringClass(), - javaProjectBuilder, mojoAnnotatedClasses, - linkGenerator, element.getLineNumber()); - ParameterAnnotationContent parameterAnnotationContent = parameter.getValue(); - parameterAnnotationContent.setDescription(getDescriptionFromElement(element, context)); - - DocletTag deprecated = element.getTagByName("deprecated"); - if (deprecated != null) { - parameterAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context)); - } - - DocletTag since = element.getTagByName("since"); - if (since != null) { - parameterAnnotationContent.setSince(getRawValueFromTaglet(since, context)); + SourceMember member = parameter.getValue().isAnnotationOnMethod() + ? methods.get(parameter.getKey()) + : fields.get(parameter.getKey()); + if (member != null) { + populateMemberJavadoc( + javaClass, member, parameter.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator); } } - // populate components - Map components = - entry.getValue().getComponents(); - for (Map.Entry component : components.entrySet()) { - JavaAnnotatedElement element = fieldsMap.get(component.getKey()); - if (element == null) { - continue; - } - - JavaClassConverterContext context = new JavaClassConverterContext( - javaClass, ((JavaMember) element).getDeclaringClass(), - javaProjectBuilder, mojoAnnotatedClasses, - linkGenerator, javaClass.getLineNumber()); - ComponentAnnotationContent componentAnnotationContent = component.getValue(); - componentAnnotationContent.setDescription(getDescriptionFromElement(element, context)); - - DocletTag deprecated = element.getTagByName("deprecated"); - if (deprecated != null) { - componentAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context)); - } - - DocletTag since = element.getTagByName("since"); - if (since != null) { - componentAnnotationContent.setSince(getRawValueFromTaglet(since, context)); + for (Map.Entry component : + entry.getValue().getComponents().entrySet()) { + SourceMember member = fields.get(component.getKey()); + if (member != null) { + populateMemberJavadoc( + javaClass, member, component.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator); } } } } - /** - * Returns the XHTML description from the given element. - * This may refer to either goal, parameter or component. - * @param element the element for which to generate the description - * @param context the context with which to call the converter - * @return the generated description - */ - String getDescriptionFromElement(JavaAnnotatedElement element, JavaClassConverterContext context) { + private void populateMemberJavadoc( + TypeDeclaration mojoClass, + SourceMember member, + Object annotation, + JavaSourceModel sourceModel, + Map mojoAnnotatedClasses, + JavadocLinkGenerator linkGenerator) { + JavaClassConverterContext context = new JavaClassConverterContext( + mojoClass, + member.declaringClass, + member.element, + sourceModel, + mojoAnnotatedClasses, + linkGenerator, + lineNumber(member.element)); + String description = getDescriptionFromElement(member.javadocElement, context); + Optional deprecated = getTag(member.javadocElement, "deprecated"); + Optional since = getTag(member.javadocElement, "since"); + if (annotation instanceof ParameterAnnotationContent) { + ParameterAnnotationContent parameter = (ParameterAnnotationContent) annotation; + parameter.setDescription(description); + deprecated.ifPresent(tag -> parameter.setDeprecated(getRawValueFromTaglet(tag, context))); + since.ifPresent(tag -> parameter.setSince(getRawValueFromTaglet(tag, context))); + } else { + ComponentAnnotationContent component = (ComponentAnnotationContent) annotation; + component.setDescription(description); + deprecated.ifPresent(tag -> component.setDeprecated(getRawValueFromTaglet(tag, context))); + since.ifPresent(tag -> component.setSince(getRawValueFromTaglet(tag, context))); + } + } - String comment = element.getComment(); - if (comment == null) { + String getDescriptionFromElement(NodeWithJavadoc element, JavaClassConverterContext context) { + Optional javadoc = element.getJavadoc(); + if (!javadoc.isPresent()) { return null; } - StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(comment, context)); - for (DocletTag docletTag : element.getTags()) { - // also consider see block tags - if ("see".equals(docletTag.getName())) { - description.append(javadocBlockTagsToHtmlConverter.convert(docletTag, context)); - } - } + StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert( + javadoc.get().getDescription().toText(), context)); + javadoc.get().getBlockTags().stream() + .filter(tag -> "see".equals(tag.getTagName())) + .forEach(tag -> description.append(javadocBlockTagsToHtmlConverter.convert( + tag.getTagName(), tag.getContent().toText(), context))); return description.toString(); } - String getRawValueFromTaglet(DocletTag docletTag, ConverterContext context) { - // just resolve inline tags and convert to XHTML - return javadocInlineTagsToHtmlConverter.convert(docletTag.getValue(), context); + String getRawValueFromTaglet(JavadocBlockTag tag, ConverterContext context) { + return javadocInlineTagsToHtmlConverter.convert(tag.getContent().toText(), context); } - /** - * @param javaClass not null - * @param tagName not null - * @return docletTag instance - */ - private DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) { - try { - DocletTag tag = javaClass.getTagByName(tagName); - - if (tag == null) { - JavaClass superClass = javaClass.getSuperJavaClass(); - - if (superClass != null) { - tag = findInClassHierarchy(superClass, tagName); - } - } - + private Optional findInClassHierarchy( + JavaSourceModel sourceModel, TypeDeclaration javaClass, String tagName) { + Optional tag = getTag(javaClass, tagName); + if (tag.isPresent()) { return tag; - } catch (NoClassDefFoundError e) { - if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) { - return null; - } - String str; - try { - str = javaClass.getFullyQualifiedName(); - } catch (Throwable t) { - str = javaClass.getValue(); - } - LOGGER.warn("Failed extracting tag '" + tagName + "' from class " + str); - throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e); } + return getSuperSourceClass(sourceModel, javaClass) + .flatMap(parent -> findInClassHierarchy(sourceModel, parent, tagName)); } - /** - * extract fields that are either parameters or components. - * - * @param javaClass not null - * @return map with Mojo parameters names as keys - */ - private Map extractFieldsAnnotations( - JavaClass javaClass, Map javaClassesMap) { - try { - Map rawParams = new TreeMap<>(); - - // we have to add the parent fields first, so that they will be overwritten by the local fields if - // that actually happens... - JavaClass superClass = javaClass.getSuperJavaClass(); - - if (superClass != null) { - if (!superClass.getFields().isEmpty()) { - rawParams = extractFieldsAnnotations(superClass, javaClassesMap); - } - // maybe sources comes from scan of sources artifact - superClass = javaClassesMap.get(superClass.getFullyQualifiedName()); - if (superClass != null && !superClass.getFields().isEmpty()) { - rawParams = extractFieldsAnnotations(superClass, javaClassesMap); - } - } else { - - rawParams = new TreeMap<>(); - } - - for (JavaField field : javaClass.getFields()) { - rawParams.put(field.getName(), field); - } + private static Optional getTag(NodeWithJavadoc element, String tagName) { + return element.getJavadoc() + .flatMap(javadoc -> javadoc.getBlockTags().stream() + .filter(tag -> tagName.equals(tag.getTagName())) + .findFirst()); + } - return rawParams; - } catch (NoClassDefFoundError e) { - LOGGER.warn("Failed extracting parameters from " + javaClass); - throw e; + private Map extractFields(JavaSourceModel sourceModel, TypeDeclaration javaClass) { + Map result = getSuperSourceClass(sourceModel, javaClass) + .map(parent -> extractFields(sourceModel, parent)) + .orElseGet(TreeMap::new); + for (FieldDeclaration field : javaClass.getFields()) { + field.getVariables() + .forEach(variable -> + result.put(variable.getNameAsString(), new SourceMember(javaClass, field, field))); } + return result; } - /** - * extract methods that are parameters. - * - * @param javaClass not null - * @return map with Mojo parameters names as keys - */ - private Map extractMethodsAnnotations( - JavaClass javaClass, Map javaClassesMap) { - try { - Map rawParams = new TreeMap<>(); - - // we have to add the parent methods first, so that they will be overwritten by the local methods if - // that actually happens... - JavaClass superClass = javaClass.getSuperJavaClass(); - - if (superClass != null) { - if (!superClass.getMethods().isEmpty()) { - rawParams = extractMethodsAnnotations(superClass, javaClassesMap); - } - // maybe sources comes from scan of sources artifact - superClass = javaClassesMap.get(superClass.getFullyQualifiedName()); - if (superClass != null && !superClass.getMethods().isEmpty()) { - rawParams = extractMethodsAnnotations(superClass, javaClassesMap); - } - } else { - - rawParams = new TreeMap<>(); - } - - for (JavaMethod method : javaClass.getMethods()) { - if (isPublicSetterMethod(method)) { - rawParams.put( - StringUtils.lowercaseFirstLetter(method.getName().substring(3)), method); - } - } - - return rawParams; - } catch (NoClassDefFoundError e) { - if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) { - return new TreeMap<>(); - } - String str; - try { - str = javaClass.getFullyQualifiedName(); - } catch (Throwable t) { - str = javaClass.getValue(); + private Map extractMethods(JavaSourceModel sourceModel, TypeDeclaration javaClass) { + Map result = getSuperSourceClass(sourceModel, javaClass) + .map(parent -> extractMethods(sourceModel, parent)) + .orElseGet(TreeMap::new); + for (MethodDeclaration method : javaClass.getMethods()) { + if (isPublicSetterMethod(method)) { + result.put( + StringUtils.lowercaseFirstLetter( + method.getNameAsString().substring(3)), + new SourceMember(javaClass, method, method)); } - LOGGER.warn("Failed extracting methods from " + str); - throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e); } + return result; } - private boolean isPublicSetterMethod(JavaMethod method) { + private static boolean isPublicSetterMethod(MethodDeclaration method) { return method.isPublic() && !method.isStatic() - && method.getName().length() > 3 - && (method.getName().startsWith("add") || method.getName().startsWith("set")) - && "void".equals(method.getReturnType().getValue()) + && method.getNameAsString().length() > 3 + && (method.getNameAsString().startsWith("add") + || method.getNameAsString().startsWith("set")) + && method.getType().isVoidType() && method.getParameters().size() == 1; } - protected Map discoverClasses(JavaProjectBuilder builder) { - Collection javaClasses = builder.getClasses(); - - if (javaClasses == null || javaClasses.size() < 1) { - return Collections.emptyMap(); + private Optional> getSuperSourceClass( + JavaSourceModel sourceModel, TypeDeclaration javaClass) { + if (!(javaClass instanceof ClassOrInterfaceDeclaration) + || ((ClassOrInterfaceDeclaration) javaClass).isInterface()) { + return Optional.empty(); } - - Map javaClassMap = new HashMap<>(javaClasses.size()); - - for (JavaClass javaClass : javaClasses) { - javaClassMap.put(javaClass.getFullyQualifiedName(), javaClass); + Optional superClass = ((ClassOrInterfaceDeclaration) javaClass) + .getExtendedTypes().stream().findFirst(); + if (!superClass.isPresent()) { + return Optional.empty(); + } + try { + return superClass + .get() + .resolve() + .asReferenceType() + .getTypeDeclaration() + .flatMap(sourceModel::getType); + } catch (UnsolvedSymbolException e) { + // Sources artifacts need not contain the sources of every superclass. In that case there is no + // source Javadoc to inherit, just as when no sources artifact is available at all. + LOGGER.debug("Could not resolve source superclass {}", superClass.get(), e); + return Optional.empty(); } + } - return javaClassMap; + protected Map> discoverClasses(JavaSourceModel sourceModel) { + Map> result = new HashMap<>(); + for (TypeDeclaration type : sourceModel.getTypes()) { + type.getFullyQualifiedName().ifPresent(name -> result.put(name, type)); + } + return result; } - protected void extendJavaProjectBuilderWithSourcesJar( - JavaProjectBuilder builder, Artifact artifact, PluginToolsRequest request, String classifier) - throws ExtractionException { + protected void extendJavaSourceModelWithSourcesJar( + JavaSourceModel sourceModel, Artifact artifact, PluginToolsRequest request, String classifier) + throws ExtractionException, IOException { try { org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact( artifact.getGroupId(), @@ -621,17 +536,17 @@ protected void extendJavaProjectBuilderWithSourcesJar( unArchiver.setDestDirectory(extractDirectory); unArchiver.extract(); - extendJavaProjectBuilder(builder, Arrays.asList(extractDirectory), request.getDependencies()); + extendJavaSourceModel(sourceModel, Arrays.asList(extractDirectory), request.getDependencies()); } else if (sourcesArtifact.getFile().isDirectory()) { - extendJavaProjectBuilder(builder, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies()); + extendJavaSourceModel(sourceModel, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies()); } } catch (ArchiverException | NoSuchArchiverException e) { throw new ExtractionException(e.getMessage(), e); } } - private void extendJavaProjectBuilder( - PluginToolsRequest request, JavaProjectBuilder builder, final MavenProject project) { + private void extendJavaSourceModel( + PluginToolsRequest request, JavaSourceModel sourceModel, final MavenProject project) throws IOException { List sources = new ArrayList<>(); for (String source : project.getCompileSourceRoots()) { @@ -651,25 +566,32 @@ private void extendJavaProjectBuilder( sources.add(generatedPlugin); } - extendJavaProjectBuilder(builder, sources, project.getArtifacts()); + extendJavaSourceModel(sourceModel, sources, project.getArtifacts()); } - private void extendJavaProjectBuilder( - JavaProjectBuilder builder, List sourceDirectories, Set artifacts) { - - // Build isolated Classloader with only the artifacts of the project (none of this plugin) - List urls = new ArrayList<>(artifacts.size()); + private void extendJavaSourceModel( + JavaSourceModel sourceModel, List sourceDirectories, Set artifacts) throws IOException { for (Artifact artifact : artifacts) { - try { - urls.add(artifact.getFile().toURI().toURL()); - } catch (MalformedURLException e) { - // noop - } + sourceModel.addClassPathEntry(artifact.getFile()); } - builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader())); - for (File source : sourceDirectories) { - builder.addSourceTree(source); + sourceModel.addSourceDirectory(source); + } + } + + private static int lineNumber(Node node) { + return node.getBegin().map(position -> position.line).orElse(0); + } + + private static final class SourceMember { + private final TypeDeclaration declaringClass; + private final Node element; + private final NodeWithJavadoc javadocElement; + + private SourceMember(TypeDeclaration declaringClass, Node element, NodeWithJavadoc javadocElement) { + this.declaringClass = declaringClass; + this.element = element; + this.javadocElement = javadocElement; } } diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java index 1d6562c7..ac1e8a49 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java @@ -56,7 +56,7 @@ import com.github.javaparser.utils.SourceRoot; /** Source declarations and type resolution used while extracting Javadocs. */ -final class JavaSourceModel implements Closeable { +public final class JavaSourceModel implements Closeable { private final Charset encoding; private final Set sourceDirectories = new LinkedHashSet<>(); @@ -64,29 +64,30 @@ final class JavaSourceModel implements Closeable { private final Map> types = new LinkedHashMap<>(); private final Map modulesByExportedPackage = new LinkedHashMap<>(); private final Set packages = new LinkedHashSet<>(); + private final Set internalPackages = new LinkedHashSet<>(); private ParserConfiguration parserConfiguration; private CombinedTypeSolver typeSolver; private URLClassLoader classPathLoader; private boolean parsed; - JavaSourceModel(Charset encoding) { + public JavaSourceModel(Charset encoding) { this.encoding = encoding; } - void addSourceDirectory(File directory) throws IOException { + public void addSourceDirectory(File directory) throws IOException { if (directory != null && directory.isDirectory()) { sourceDirectories.add(directory.toPath().toRealPath()); } } - void addClassPathEntry(File entry) throws IOException { + public void addClassPathEntry(File entry) throws IOException { if (entry != null && entry.exists()) { classPathEntries.add(entry.toPath().toRealPath()); } } - void parse() throws IOException { + public void parse() throws IOException { if (parsed) { return; } @@ -132,8 +133,9 @@ void parse() throws IOException { .collect(Collectors.joining(System.lineSeparator())); throw new IOException("Unable to parse " + path + System.lineSeparator() + problems); } - CompilationUnit unit = result.getResult().orElseThrow( - () -> new IOException("Parser returned no compilation unit for " + result.getSourcePath())); + CompilationUnit unit = result.getResult() + .orElseThrow(() -> + new IOException("Parser returned no compilation unit for " + result.getSourcePath())); index(unit); } } @@ -156,57 +158,72 @@ private void indexClassDirectory(Path directory) throws IOException { private void index(CompilationUnit unit) { unit.getPackageDeclaration() .map(declaration -> declaration.getName().asString()) - .ifPresent(packages::add); + .ifPresent(name -> { + packages.add(name); + internalPackages.add(name); + }); for (TypeDeclaration type : unit.findAll(TypeDeclaration.class)) { type.getFullyQualifiedName().ifPresent(name -> types.putIfAbsent(name, type)); } - unit.getModule().ifPresent(module -> module.getDirectives().stream() - .filter(ModuleExportsDirective.class::isInstance) - .map(ModuleExportsDirective.class::cast) - .forEach(exports -> modulesByExportedPackage.putIfAbsent( - exports.getName().asString(), module.getName().asString()))); + unit.getModule() + .ifPresent(module -> module.getDirectives().stream() + .filter(ModuleExportsDirective.class::isInstance) + .map(ModuleExportsDirective.class::cast) + .forEach(exports -> modulesByExportedPackage.putIfAbsent( + exports.getName().asString(), module.getName().asString()))); } - Collection> getTypes() { + public Collection> getTypes() { return Collections.unmodifiableCollection(types.values()); } - Optional> getType(String fullyQualifiedName) { + public Optional> getType(String fullyQualifiedName) { return Optional.ofNullable(types.get(fullyQualifiedName)); } - Optional> getType(ResolvedReferenceTypeDeclaration declaration) { + public Optional> getType(ResolvedReferenceTypeDeclaration declaration) { return getType(declaration.getQualifiedName()); } - Optional getModuleName(String packageName) { + public Optional getModuleName(String packageName) { return Optional.ofNullable(modulesByExportedPackage.get(packageName)); } - boolean hasPackage(String packageName) { + public boolean hasPackage(String packageName) { return packages.contains(packageName) || Package.getPackage(packageName) != null; } - boolean isInternal(ResolvedReferenceTypeDeclaration declaration) { + public boolean isInternal(ResolvedReferenceTypeDeclaration declaration) { return types.containsKey(declaration.getQualifiedName()); } - Optional resolveType(String fullyQualifiedName) { + public boolean isInternalPackage(String packageName) { + return internalPackages.contains(packageName); + } + + public Optional resolveType(String fullyQualifiedName) { ensureParsed(); SymbolReference reference = typeSolver.tryToSolveType(fullyQualifiedName); return reference.isSolved() ? Optional.of(reference.getCorrespondingDeclaration()) : Optional.empty(); } - TypeSolver getTypeSolver() { + public TypeSolver getTypeSolver() { ensureParsed(); return typeSolver; } - String getLocation(Node node, int fallbackLine) { - int line = node.getBegin().map(position -> position.line).orElse(fallbackLine); + public String getLocation(Node node, int fallbackLine) { + int line = fallbackLine > 0 + ? fallbackLine + : node.getBegin().map(position -> position.line).orElse(0); return node.findCompilationUnit() .flatMap(CompilationUnit::getStorage) - .map(storage -> storage.getPath().toString() + ":" + line) + .map(storage -> java.nio.file.Paths.get("") + .toAbsolutePath() + .toUri() + .relativize(storage.getPath().toUri()) + .toString() + + ":" + line) .orElse("unknown:" + line); } diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java index b039d5b4..bc6bf04f 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java @@ -20,8 +20,6 @@ import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -30,121 +28,101 @@ import java.util.Optional; import java.util.stream.Collectors; -import com.thoughtworks.qdox.JavaProjectBuilder; -import com.thoughtworks.qdox.builder.TypeAssembler; -import com.thoughtworks.qdox.library.ClassNameLibrary; -import com.thoughtworks.qdox.model.JavaClass; -import com.thoughtworks.qdox.model.JavaField; -import com.thoughtworks.qdox.model.JavaModule; -import com.thoughtworks.qdox.model.JavaPackage; -import com.thoughtworks.qdox.model.JavaType; -import com.thoughtworks.qdox.parser.structs.TypeDef; -import com.thoughtworks.qdox.type.TypeResolver; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.types.ResolvedReferenceType; +import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel; import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass; import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference; import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType; import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; import org.apache.maven.tools.plugin.javadoc.JavadocReference; -/** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */ +/** {@link ConverterContext} backed by JavaParser source declarations and symbol resolution. */ public class JavaClassConverterContext implements ConverterContext { - final JavaClass mojoClass; // this is the mojo's class + /** The Mojo class whose documentation is being generated. */ + private final TypeDeclaration mojoClass; - final JavaClass declaringClass; // this may be a super class of the mojo's class + /** The class declaring the converted Javadoc, possibly a superclass of the Mojo class. */ + private final TypeDeclaration declaringClass; - final JavaProjectBuilder javaProjectBuilder; + private final Node locationNode; + private final JavaSourceModel sourceModel; + private final Map mojoAnnotatedClasses; - final Map mojoAnnotatedClasses; + /** The link generator, or {@code null} when no Javadoc site is configured. */ + private final JavadocLinkGenerator linkGenerator; - final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured - - final int lineNumber; - - final Optional javaModule; - - final Map attributes; + private final int lineNumber; + private final Map attributes = new HashMap<>(); public JavaClassConverterContext( - JavaClass mojoClass, - JavaProjectBuilder javaProjectBuilder, + TypeDeclaration mojoClass, + JavaSourceModel sourceModel, Map mojoAnnotatedClasses, JavadocLinkGenerator linkGenerator, int lineNumber) { - this(mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber); + this(mojoClass, mojoClass, mojoClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber); } public JavaClassConverterContext( - JavaClass mojoClass, - JavaClass declaringClass, - JavaProjectBuilder javaProjectBuilder, + TypeDeclaration mojoClass, + TypeDeclaration declaringClass, + Node locationNode, + JavaSourceModel sourceModel, Map mojoAnnotatedClasses, JavadocLinkGenerator linkGenerator, int lineNumber) { this.mojoClass = mojoClass; this.declaringClass = declaringClass; - this.javaProjectBuilder = javaProjectBuilder; + this.locationNode = locationNode; + this.sourceModel = sourceModel; this.mojoAnnotatedClasses = mojoAnnotatedClasses; this.linkGenerator = linkGenerator; this.lineNumber = lineNumber; - this.attributes = new HashMap<>(); - - javaModule = mojoClass.getJavaClassLibrary().getJavaModules().stream() - .filter(m -> m.getDescriptor().getExports().stream() - .anyMatch(e -> e.getSource().getName().equals(getPackageName()))) - .findFirst(); } @Override public Optional getModuleName() { - // https://github.com/paul-hammant/qdox/issues/113, module name is not exposed - return javaModule.map(JavaModule::getName); + return sourceModel.getModuleName(getPackageName()); } @Override public String getPackageName() { - return mojoClass.getPackageName(); + return resolve(mojoClass).getPackageName(); } @Override public String getLocation() { - try { - URL url = declaringClass.getSource().getURL(); - if (url == null) // url is not always available, just emit FQCN in that case - { - return declaringClass.getPackageName() + declaringClass.getSimpleName() + ":" + lineNumber; - } - return Paths.get("").toUri().relativize(url.toURI()) + ":" + lineNumber; - } catch (URISyntaxException e) { - return declaringClass.getSource().getURL() + ":" + lineNumber; - } + return sourceModel.getLocation(locationNode, lineNumber); } - /** - * @param reference - * @return true in case either the current context class or any of its super classes are referenced - */ + /** Returns whether {@code reference} identifies the Mojo class or one of its ancestors. */ @Override public boolean isReferencedBy(FullyQualifiedJavadocReference reference) { - JavaClass javaClassInHierarchy = this.mojoClass; - while (javaClassInHierarchy != null) { - if (isClassReferencedByReference(javaClassInHierarchy, reference)) { - return true; - } - // check implemented interfaces - for (JavaClass implementedInterfaces : javaClassInHierarchy.getInterfaces()) { - if (isClassReferencedByReference(implementedInterfaces, reference)) { - return true; - } - } - javaClassInHierarchy = javaClassInHierarchy.getSuperJavaClass(); + ResolvedReferenceTypeDeclaration declaration = resolve(mojoClass); + if (isClassReferencedByReference(declaration, reference)) { + return true; } - return false; + return declaration.getAllAncestors().stream() + .map(ResolvedReferenceType::getTypeDeclaration) + .filter(Optional::isPresent) + .map(Optional::get) + .anyMatch(type -> isClassReferencedByReference(type, reference)); } - private static boolean isClassReferencedByReference(JavaClass javaClass, FullyQualifiedJavadocReference reference) { - return javaClass.getPackageName().equals(reference.getPackageName().orElse("")) - && javaClass.getSimpleName().equals(reference.getClassName().orElse("")); + private static boolean isClassReferencedByReference( + ResolvedReferenceTypeDeclaration declaration, FullyQualifiedJavadocReference reference) { + return declaration.getPackageName().equals(reference.getPackageName().orElse("")) + && declaration.getClassName().equals(reference.getClassName().orElse("")); } @Override @@ -157,8 +135,8 @@ public URI getUrl(FullyQualifiedJavadocReference reference) { try { if (isReferencedBy(reference) && MemberType.FIELD == reference.getMemberType().orElse(null)) { - // link to current goal's parameters - return new URI(null, null, reference.getMember().orElse(null)); // just an anchor if same context + // Fields in the current Mojo link to parameter anchors on the same page. + return new URI(null, null, reference.getMember().orElse(null)); } Optional fqClassName = reference.getFullyQualifiedClassName(); if (fqClassName.isPresent()) { @@ -167,7 +145,7 @@ public URI getUrl(FullyQualifiedJavadocReference reference) { && mojoAnnotatedClass.getMojo() != null && (!reference.getLabel().isPresent() || MemberType.FIELD == reference.getMemberType().orElse(null))) { - // link to other mojo (only for fields = parameters or without member) + // Fields and whole-class references to another Mojo link to that Mojo's page. return new URI( null, "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html", @@ -175,7 +153,7 @@ public URI getUrl(FullyQualifiedJavadocReference reference) { } } } catch (URISyntaxException e) { - throw new IllegalStateException("Error constructing a valid URL", e); // should not happen + throw new IllegalStateException("Error constructing a valid URL", e); } if (linkGenerator == null) { throw new IllegalStateException("No Javadoc Sites given to create URLs to"); @@ -185,296 +163,352 @@ public URI getUrl(FullyQualifiedJavadocReference reference) { @Override public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) { - Optional resolvedName; - // is it already fully qualified? + Optional resolved; + // First try the reference exactly as written, which handles fully qualified names. if (reference.getPackageNameClassName().isPresent()) { - resolvedName = resolveMember( - reference.getPackageNameClassName().get(), reference.getMember(), reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); + String name = reference.getPackageNameClassName().get(); + resolved = resolveNamedReference(name, reference.getMember(), reference.getLabel()); + if (resolved.isPresent()) { + return resolved.get(); } } - // is it a member only? + + // Search member-only references in the current class or interface first, then its ancestors. if (reference.getMember().isPresent() && !reference.getPackageNameClassName().isPresent()) { - // search order for not fully qualified names: - // 1. The current class or interface (only for members) - resolvedName = resolveMember(declaringClass, reference.getMember(), reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } - // 2. Any enclosing classes and interfaces searching the closest first (only members) - for (JavaClass nestedClass : declaringClass.getNestedClasses()) { - resolvedName = resolveMember(nestedClass, reference.getMember(), reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } + resolved = resolveMember(resolve(declaringClass), reference.getMember(), reference.getLabel(), true); + if (resolved.isPresent()) { + return resolved.get(); } - // 3. Any superclasses and superinterfaces, searching the closest first. (only members) - JavaClass superClass = declaringClass.getSuperJavaClass(); - while (superClass != null) { - resolvedName = resolveMember(superClass, reference.getMember(), reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } - superClass = superClass.getSuperJavaClass(); - } - } else { - String packageNameClassName = reference.getPackageNameClassName().get(); - // 4. The current package - resolvedName = resolveMember( - declaringClass.getPackageName() + "." + packageNameClassName, - reference.getMember(), - reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } - // 5. Any imported packages, classes, and interfaces, searching in the order of the import statement. - List importNames = new ArrayList<>(); - importNames.add("java.lang.*"); // default import - importNames.addAll(declaringClass.getSource().getImports()); - for (String importName : importNames) { - if (importName.endsWith(".*")) { - resolvedName = resolveMember( - importName.replace("*", packageNameClassName), reference.getMember(), reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } - } else { - if (importName.endsWith(packageNameClassName)) { - resolvedName = resolveMember(importName, reference.getMember(), reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } - } else { - // ends with prefix of reference (nested class name) - int firstDotIndex = packageNameClassName.indexOf("."); - if (firstDotIndex > 0 - && importName.endsWith(packageNameClassName.substring(0, firstDotIndex))) { - resolvedName = resolveMember( - importName, - packageNameClassName.substring(firstDotIndex + 1), - reference.getMember(), - reference.getLabel()); - if (resolvedName.isPresent()) { - return resolvedName.get(); - } - } - } + } else if (reference.getPackageNameClassName().isPresent()) { + // Resolve simple and nested type names using Javadoc's lookup order. + String name = reference.getPackageNameClassName().get(); + for (String candidate : classNameCandidates(name)) { + resolved = resolveNamedReference(candidate, reference.getMember(), reference.getLabel()); + if (resolved.isPresent()) { + return resolved.get(); } } } throw new IllegalArgumentException("Could not resolve javadoc reference " + reference); } - @Override - public String getStaticFieldValue(FullyQualifiedJavadocReference reference) { - String fqcn = reference - .getFullyQualifiedClassName() - .orElseThrow(() -> - new IllegalArgumentException("Given reference does not specify a fully qualified class name!")); - String fieldName = reference - .getMember() - .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!")); - JavaClass javaClass = javaProjectBuilder.getClassByName(fqcn); - JavaField javaField = javaClass.getFieldByName(fieldName); - if (javaField == null) { - throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn); + private Optional resolveNamedReference( + String name, Optional member, Optional label) { + Optional type = sourceModel.resolveType(name); + if (type.isPresent()) { + return resolveMember(type.get(), member, label, true); } - if (!javaField.isStatic()) { - throw new IllegalArgumentException("Field with name " + fieldName + " in class " + fqcn + " is not static"); + // Package references cannot contain members. + if (!member.isPresent() && sourceModel.hasPackage(name)) { + return Optional.of(new FullyQualifiedJavadocReference(name, label, !sourceModel.isInternalPackage(name))); } - return javaField.getInitializationExpression(); + return Optional.empty(); } - @Override - public URI getInternalJavadocSiteBaseUrl() { - return linkGenerator.getInternalJavadocSiteBaseUrl(); - } - - private Optional resolveMember( - String fullyQualifiedPackageNameClassName, Optional member, Optional label) { - return resolveMember(fullyQualifiedPackageNameClassName, "", member, label); + private List classNameCandidates(String name) { + List candidates = new ArrayList<>(); + ResolvedReferenceTypeDeclaration declaration = resolve(declaringClass); + // Search order: current package, implicit java.lang import, then explicit imports in declaration order. + candidates.add(declaration.getPackageName() + "." + name); + candidates.add("java.lang." + name); + List imports = declaringClass + .findCompilationUnit() + .>map(unit -> new ArrayList<>(unit.getImports())) + .orElseGet(Collections::emptyList); + for (ImportDeclaration importDeclaration : imports) { + if (importDeclaration.isStatic()) { + continue; + } + String importName = importDeclaration.getNameAsString(); + if (importDeclaration.isAsterisk()) { + candidates.add(importName + "." + name); + } else if (name.equals(simpleName(importName))) { + candidates.add(importName); + } else if (name.startsWith(simpleName(importName) + ".")) { + // An imported outer type may prefix a nested-type reference. + candidates.add( + importName + name.substring(simpleName(importName).length())); + } + } + return candidates; } private Optional resolveMember( - String fullyQualifiedPackageNameClassName, - String nestedClassName, + ResolvedReferenceTypeDeclaration type, Optional member, - Optional label) { - JavaClass javaClass = javaProjectBuilder.getClassByName(fullyQualifiedPackageNameClassName); - if (!isClassFound(javaClass)) { - JavaPackage javaPackage = javaProjectBuilder.getPackageByName(fullyQualifiedPackageNameClassName); - if (javaPackage == null || !nestedClassName.isEmpty()) { - // is it a nested class? - int lastIndexOfDot = fullyQualifiedPackageNameClassName.lastIndexOf('.'); - if (lastIndexOfDot > 0) { - String newNestedClassName = nestedClassName; - if (!newNestedClassName.isEmpty()) { - newNestedClassName += '.'; - } - newNestedClassName += fullyQualifiedPackageNameClassName.substring(lastIndexOfDot + 1); - return resolveMember( - fullyQualifiedPackageNameClassName.substring(0, lastIndexOfDot), - newNestedClassName, - member, - label); - } - return Optional.empty(); - } else { - // reference to java package never has a member - return Optional.of( - new FullyQualifiedJavadocReference(javaPackage.getName(), label, isExternal(javaPackage))); - } - } else { - if (!nestedClassName.isEmpty()) { - javaClass = javaClass.getNestedClassByName(nestedClassName); - if (javaClass == null) { - return Optional.empty(); - } - } + Optional label, + boolean includeAncestors) { + if (!member.isPresent()) { + return Optional.of(toReference(type, Optional.empty(), Optional.empty(), label)); + } + + String memberText = member.get(); + // Resolve ambiguous member text as a field, method, then constructor. + Optional field = findField(type, memberText, includeAncestors); + if (field.isPresent()) { + return Optional.of(toReference( + field.get().declaringType().asReferenceType(), member, Optional.of(MemberType.FIELD), label)); + } - return resolveMember(javaClass, member, label); + String methodName = methodName(memberText); + Optional> parameterTypes = parameterTypes(memberText); + Optional method = findMethod(type, methodName, parameterTypes, includeAncestors); + if (method.isPresent()) { + return Optional.of(toReference( + method.get().declaringType(), + Optional.of(canonicalMember(method.get())), + Optional.of(MemberType.METHOD), + label)); + } + + if (methodName.equals(type.getName())) { + Optional constructor = findConstructor(type, parameterTypes); + if (constructor.isPresent()) { + return Optional.of(toReference( + type, + Optional.of(canonicalMember(constructor.get())), + Optional.of(MemberType.CONSTRUCTOR), + label)); + } } + return Optional.empty(); } - private boolean isExternal(JavaClass javaClass) { - return isExternal(javaClass.getPackage()); + private Optional findField( + ResolvedReferenceTypeDeclaration type, String name, boolean includeAncestors) { + List fields = includeAncestors ? type.getAllFields() : type.getDeclaredFields(); + return fields.stream().filter(field -> field.getName().equals(name)).findFirst(); } - private boolean isExternal(JavaPackage javaPackage) { - return !javaPackage.getJavaClassLibrary().equals(mojoClass.getJavaClassLibrary()); + private Optional findMethod( + ResolvedReferenceTypeDeclaration type, + String name, + Optional> parameterTypes, + boolean includeAncestors) { + List hierarchy = new ArrayList<>(); + hierarchy.add(type); + if (includeAncestors) { + type.getAllAncestors().stream() + .map(ResolvedReferenceType::getTypeDeclaration) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(hierarchy::add); + } + for (ResolvedReferenceTypeDeclaration declaration : hierarchy) { + Optional method = declaration.getDeclaredMethods().stream() + .filter(candidate -> candidate.getName().equals(name)) + .filter(candidate -> matches(candidate, parameterTypes)) + .findFirst(); + if (method.isPresent()) { + return method; + } + } + return Optional.empty(); } - private Optional resolveMember( - JavaClass javaClass, Optional member, Optional label) { - final Optional memberType; - Optional resolvedMember = member; - if (member.isPresent()) { - // member is either field... - if (javaClass.getFieldByName(member.get()) == null) { - // ...is method... - List parameterTypes = getParameterTypes(member.get()); - String methodName = getMethodName(member.get()); - if (javaClass.getMethodBySignature(methodName, parameterTypes) == null) { - // ...or is constructor - if ((!methodName.equals(javaClass.getSimpleName())) - || (javaClass.getConstructor(parameterTypes) == null)) { - return Optional.empty(); - } else { - memberType = Optional.of(MemberType.CONSTRUCTOR); - } - } else { - memberType = Optional.of(MemberType.METHOD); - } - // reconstruct member with fully qualified names but leaving out the argument names - StringBuilder memberBuilder = new StringBuilder(methodName); - memberBuilder.append("("); - memberBuilder.append(parameterTypes.stream() - .map(JavaType::getFullyQualifiedName) - .collect(Collectors.joining(","))); - memberBuilder.append(")"); - resolvedMember = Optional.of(memberBuilder.toString()); - } else { - memberType = Optional.of(MemberType.FIELD); + private Optional findConstructor( + ResolvedReferenceTypeDeclaration type, Optional> parameterTypes) { + return type.getConstructors().stream() + .filter(candidate -> matches(candidate, parameterTypes)) + .findFirst(); + } + + private boolean matches(ResolvedMethodLikeDeclaration declaration, Optional> parameterTypes) { + if (!parameterTypes.isPresent()) { + return true; + } + if (declaration.getNumberOfParams() != parameterTypes.get().size()) { + return false; + } + for (int index = 0; index < declaration.getNumberOfParams(); index++) { + String actual = declaration.getParam(index).getType().erasure().describe(); + if (!actual.equals(parameterTypes.get().get(index))) { + return false; } - } else { - memberType = Optional.empty(); } - String className = javaClass - .getCanonicalName() - .substring(javaClass.getPackageName().length() + 1); - return Optional.of(new FullyQualifiedJavadocReference( - javaClass.getPackageName(), - Optional.of(className), - resolvedMember, - memberType, - label, - isExternal(javaClass))); - } - - private static boolean isClassFound(JavaClass javaClass) { - // this is never null due to using the ClassNameLibrary in the builder - // but every instance of ClassNameLibrary basically means that the class was not found - return !(javaClass.getJavaClassLibrary() instanceof ClassNameLibrary); - } - - // https://github.com/paul-hammant/qdox/issues/104 - private List getParameterTypes(String member) { - List parameterTypes = new ArrayList<>(); - // TypeResolver.byClassName() always resolves types as non existing inner class - TypeResolver typeResolver = TypeResolver.byClassName( - declaringClass.getPackageName(), - declaringClass.getJavaClassLibrary(), - declaringClass.getSource().getImports()); - - // method parameters are optionally enclosed by parentheses - int indexOfOpeningParenthesis = member.indexOf('('); - int indexOfClosingParenthesis = member.indexOf(')'); - final String signatureArguments; - if (indexOfOpeningParenthesis >= 0 - && indexOfClosingParenthesis > 0 - && indexOfClosingParenthesis > indexOfOpeningParenthesis) { - signatureArguments = member.substring(indexOfOpeningParenthesis + 1, indexOfClosingParenthesis); - } else if (indexOfOpeningParenthesis == -1 && indexOfClosingParenthesis >= 0 - || indexOfOpeningParenthesis >= 0 && indexOfOpeningParenthesis == -1) { + return true; + } + + private Optional> parameterTypes(String member) { + int opening = member.indexOf('('); + int closing = member.lastIndexOf(')'); + if (opening < 0 && closing < 0) { + // Without parentheses, match the first overload found, mirroring javadoc. + return Optional.empty(); + } + if (opening < 0 || closing < opening) { throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member); - } else { - // If any method or constructor is entered as a name with no parentheses, such as getValue, - // and if there is no field with the same name, then the javadoc command still creates a - // link to the method. If this method is overloaded, then the javadoc command links to the - // first method its search encounters, which is unspecified - // (Source: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#JSWOR654) - return Collections.emptyList(); } - for (String parameter : signatureArguments.split(",")) { - // strip off argument name, only type is relevant - String canonicalParameter = parameter.trim(); - int spaceIndex = canonicalParameter.indexOf(' '); - final String typeName; - if (spaceIndex > 0) { - typeName = canonicalParameter.substring(0, spaceIndex).trim(); - } else { - typeName = canonicalParameter; - } - if (!typeName.isEmpty()) { - String rawTypeName = getRawTypeName(typeName); - // already check here for unresolvable types due to https://github.com/paul-hammant/qdox/issues/111 - if (typeResolver.resolveType(rawTypeName) == null) { - throw new IllegalArgumentException("Found unresolvable method argument type in " + member); - } - TypeDef typeDef = new TypeDef(getRawTypeName(typeName)); - int dimensions = getDimensions(typeName); - JavaType javaType = TypeAssembler.createUnresolved(typeDef, dimensions, typeResolver); + String arguments = member.substring(opening + 1, closing).trim(); + if (arguments.isEmpty()) { + return Optional.of(Collections.emptyList()); + } + List result = new ArrayList<>(); + for (String argument : splitArguments(arguments)) { + result.add(resolveParameterType(stripArgumentName(argument.trim()))); + } + return Optional.of(result); + } - parameterTypes.add(javaType); + private static List splitArguments(String arguments) { + List result = new ArrayList<>(); + int genericDepth = 0; + int start = 0; + for (int index = 0; index < arguments.length(); index++) { + char ch = arguments.charAt(index); + if (ch == '<') { + genericDepth++; + } else if (ch == '>') { + genericDepth--; + } else if (ch == ',' && genericDepth == 0) { + result.add(arguments.substring(start, index)); + start = index + 1; } } - return parameterTypes; + result.add(arguments.substring(start)); + return result; } - private static int getDimensions(String type) { - return (int) type.chars().filter(ch -> ch == '[').count(); + private static String stripArgumentName(String argument) { + int genericDepth = 0; + for (int index = argument.length() - 1; index >= 0; index--) { + char ch = argument.charAt(index); + if (ch == '>') { + genericDepth++; + } else if (ch == '<') { + genericDepth--; + } else if (Character.isWhitespace(ch) && genericDepth == 0) { + return argument.substring(0, index).trim(); + } + } + return argument; } - private static String getRawTypeName(String typeName) { - // strip dimensions - int indexOfOpeningBracket = typeName.indexOf('['); - if (indexOfOpeningBracket >= 0) { - return typeName.substring(0, indexOfOpeningBracket); + private String resolveParameterType(String typeName) { + String normalized = eraseGenerics(typeName.replace("...", "[]")); + int dimensions = 0; + while (normalized.endsWith("[]")) { + dimensions++; + normalized = normalized.substring(0, normalized.length() - 2); + } + String resolved; + if (isPrimitive(normalized)) { + resolved = normalized; } else { - return typeName; + List candidates = new ArrayList<>(); + candidates.add(normalized); + candidates.addAll(classNameCandidates(normalized)); + resolved = candidates.stream() + .filter(candidate -> sourceModel.resolveType(candidate).isPresent()) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("Found unresolvable method argument type " + typeName)); } + StringBuilder result = new StringBuilder(resolved); + for (int index = 0; index < dimensions; index++) { + result.append("[]"); + } + return result.toString(); } - private static String getMethodName(String member) { - // name is separated from arguments either by '(' or spans the full member - int indexOfOpeningParentheses = member.indexOf('('); - if (indexOfOpeningParentheses == -1) { - return member; - } else { - return member.substring(0, indexOfOpeningParentheses); + private static String eraseGenerics(String value) { + StringBuilder result = new StringBuilder(); + int depth = 0; + for (int index = 0; index < value.length(); index++) { + char ch = value.charAt(index); + if (ch == '<') { + depth++; + } else if (ch == '>') { + depth--; + } else if (depth == 0) { + result.append(ch); + } + } + return result.toString(); + } + + private static boolean isPrimitive(String name) { + return "boolean".equals(name) + || "byte".equals(name) + || "char".equals(name) + || "double".equals(name) + || "float".equals(name) + || "int".equals(name) + || "long".equals(name) + || "short".equals(name); + } + + private FullyQualifiedJavadocReference toReference( + ResolvedReferenceTypeDeclaration type, + Optional member, + Optional memberType, + Optional label) { + return new FullyQualifiedJavadocReference( + type.getPackageName(), + Optional.of(type.getClassName()), + member, + memberType, + label, + !sourceModel.isInternal(type)); + } + + @Override + public String getStaticFieldValue(FullyQualifiedJavadocReference reference) { + String fqcn = reference + .getFullyQualifiedClassName() + .orElseThrow(() -> + new IllegalArgumentException("Given reference does not specify a fully qualified class name!")); + String fieldName = reference + .getMember() + .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!")); + TypeDeclaration type = sourceModel + .getType(fqcn) + .orElseThrow(() -> new IllegalArgumentException("Could not find source class " + fqcn)); + for (FieldDeclaration field : type.getFields()) { + Optional variable = field.getVariables().stream() + .filter(candidate -> candidate.getNameAsString().equals(fieldName)) + .findFirst(); + if (variable.isPresent()) { + if (!field.isStatic()) { + throw new IllegalArgumentException( + "Field with name " + fieldName + " in class " + fqcn + " is not static"); + } + return variable.get() + .getInitializer() + .map(initializer -> initializer.toString()) + .orElseThrow(() -> new IllegalArgumentException( + "Field with name " + fieldName + " in class " + fqcn + " has no initializer")); + } } + throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn); + } + + @Override + public URI getInternalJavadocSiteBaseUrl() { + return linkGenerator.getInternalJavadocSiteBaseUrl(); + } + + private static ResolvedReferenceTypeDeclaration resolve(TypeDeclaration declaration) { + return declaration.resolve(); + } + + private static String methodName(String member) { + int opening = member.indexOf('('); + return opening < 0 ? member : member.substring(0, opening); + } + + private static String canonicalMember(ResolvedMethodLikeDeclaration declaration) { + return declaration.getName() + "(" + + declaration.formalParameterTypes().stream() + .map(type -> type.erasure().describe()) + .collect(Collectors.joining(",")) + + ")"; + } + + private static String simpleName(String name) { + int separator = name.lastIndexOf('.'); + return separator < 0 ? name : name.substring(separator + 1); } @SuppressWarnings("unchecked") diff --git a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java index c6071f01..457060c5 100644 --- a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java +++ b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java @@ -20,11 +20,11 @@ import java.io.File; import java.net.URI; -import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.Collections; -import com.thoughtworks.qdox.JavaProjectBuilder; -import com.thoughtworks.qdox.model.JavaClass; +import com.github.javaparser.ast.body.TypeDeclaration; +import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel; import org.apache.maven.tools.plugin.extractor.annotations.converter.test.CurrentClass; import org.apache.maven.tools.plugin.extractor.annotations.converter.test.OtherClass; import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent; @@ -46,23 +46,24 @@ class JavaClassConverterContextTest { private final String currentPackageName; - private final JavaProjectBuilder builder; + private final JavaSourceModel sourceModel; - private final JavaClass contextClass; + private final TypeDeclaration contextClass; private JavadocLinkGenerator linkGenerator; private URI javadocBaseUri; - JavaClassConverterContextTest() throws URISyntaxException { - builder = new JavaProjectBuilder(); - builder.addSourceFolder(new File("src/test/java")); + JavaClassConverterContextTest() throws Exception { + sourceModel = new JavaSourceModel(StandardCharsets.UTF_8); + sourceModel.addSourceDirectory(new File("src/test/java")); + sourceModel.parse(); - contextClass = builder.getClassByName(CurrentClass.class.getName()); - currentPackageName = contextClass.getPackageName(); + contextClass = sourceModel.getType(CurrentClass.class.getName()).orElseThrow(AssertionError::new); + currentPackageName = contextClass.resolve().getPackageName(); javadocBaseUri = new URI("http://localhost/apidocs"); linkGenerator = new JavadocLinkGenerator(javadocBaseUri, "11"); - context = new JavaClassConverterContext(contextClass, builder, Collections.emptyMap(), linkGenerator, 10); + context = new JavaClassConverterContext(contextClass, sourceModel, Collections.emptyMap(), linkGenerator, 10); } @Test @@ -202,7 +203,7 @@ void getUrl() throws Exception { MojoAnnotatedClass mojoAnnotatedClass = new MojoAnnotatedClass().setMojo(mojoAnnotationContent); context = new JavaClassConverterContext( contextClass, - builder, + sourceModel, Collections.singletonMap( "org.apache.maven.tools.plugin.extractor.annotations.converter.test.OtherClass", mojoAnnotatedClass), From 63ca83f63bb2535a45d1e4ee05666c610f34b768 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 22:27:16 +0200 Subject: [PATCH 4/4] MPLUGIN-421: Remove direct QDox dependency --- .../gh-944-exclude-source-directory/pom.xml | 6 +- .../plugin/coreit/SomeGeneratedModel.java | 2 - .../verify.groovy | 31 ++++++ .../invoker.properties | 18 ++++ .../it/javaparser-generated-source/pom.xml | 100 ++++++++++++++++++ .../apache/maven/plugin/coreit/FirstMojo.java | 36 +++++++ .../plugin/coreit/SomeGeneratedModel.java | 32 ++++++ maven-plugin-tools-annotations/pom.xml | 4 - .../JavadocBlockTagsToXhtmlConverter.java | 5 - pom.xml | 5 - .../resources/images/plugin-tools-deps.svg | 10 +- 11 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 maven-plugin-plugin/src/it/gh-944-exclude-source-directory/verify.groovy create mode 100644 maven-plugin-plugin/src/it/javaparser-generated-source/invoker.properties create mode 100644 maven-plugin-plugin/src/it/javaparser-generated-source/pom.xml create mode 100644 maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/FirstMojo.java create mode 100644 maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java diff --git a/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/pom.xml b/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/pom.xml index 2668f85d..cdee0e30 100644 --- a/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/pom.xml +++ b/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/pom.xml @@ -29,7 +29,7 @@ under the License. Maven Integration Test :: gh-944-exclude-source-directory - Test plugin-plugin, plugin.xml descriptor - shouldn't contain explicitly excluded source directories + Test plugin-plugin, plugin.xml descriptor - shouldn't contain explicitly excluded source directories @@ -96,8 +96,8 @@ under the License. @project.version@ - - ${project.build.directory}/generated-sources/annotations + + ${project.basedir}/src/main/java prefix diff --git a/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java b/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java index c5290edd..50f6463c 100644 --- a/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java +++ b/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java @@ -27,8 +27,6 @@ @Immutable @Modifiable public interface SomeGeneratedModel { - // Triggers https://github.com/paul-hammant/qdox/issues/287 on the generated code when QDox tries - // to scan it. @Nullable List<@Nullable String> getThings(); } diff --git a/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/verify.groovy b/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/verify.groovy new file mode 100644 index 00000000..41dd1b13 --- /dev/null +++ b/maven-plugin-plugin/src/it/gh-944-exclude-source-directory/verify.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import groovy.xml.XmlParser + +File descriptorFile = new File(basedir, "target/classes/META-INF/maven/plugin.xml") +assert descriptorFile.isFile() + +def pluginDescriptor = new XmlParser().parse(descriptorFile) +def mojo = pluginDescriptor.mojos.mojo.find { it.goal.text() == "first" } + +assert mojo != null +assert mojo.description.text() == "" + +return true diff --git a/maven-plugin-plugin/src/it/javaparser-generated-source/invoker.properties b/maven-plugin-plugin/src/it/javaparser-generated-source/invoker.properties new file mode 100644 index 00000000..2f680d3f --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-generated-source/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +invoker.goals = process-classes diff --git a/maven-plugin-plugin/src/it/javaparser-generated-source/pom.xml b/maven-plugin-plugin/src/it/javaparser-generated-source/pom.xml new file mode 100644 index 00000000..bc7beb74 --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-generated-source/pom.xml @@ -0,0 +1,100 @@ + + + + + + 4.0.0 + + org.apache.maven.its.javaparser-generated-source + javaparser-generated-source + 1.0-SNAPSHOT + maven-plugin + + Maven Integration Test :: JavaParser Generated Source + Checks that JavaParser scans generated sources containing type-use annotations. + + + UTF-8 + + + + + org.apache.maven + maven-plugin-api + @maven3Version@ + provided + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + @project.version@ + provided + + + + org.immutables + value + 2.12.0 + provided + + + + org.jspecify + jspecify + 1.0.0 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @version.maven-compiler-plugin@ + + 1.8 + 1.8 + + + + org.immutables + value + 2.12.0 + + + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + @project.version@ + + prefix + + + + + diff --git a/maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/FirstMojo.java b/maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/FirstMojo.java new file mode 100644 index 00000000..df868e1e --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/FirstMojo.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.coreit; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** A Mojo that uses the generated model. */ +@Mojo(name = "first") +public class FirstMojo extends AbstractMojo { + + @Parameter + private ModifiableSomeGeneratedModel model; + + @Override + public void execute() { + // Nothing to do. + } +} diff --git a/maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java b/maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java new file mode 100644 index 00000000..50f6463c --- /dev/null +++ b/maven-plugin-plugin/src/it/javaparser-generated-source/src/main/java/org/apache/maven/plugin/coreit/SomeGeneratedModel.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.coreit; + +import java.util.List; + +import org.immutables.value.Value.Immutable; +import org.immutables.value.Value.Modifiable; +import org.jspecify.annotations.Nullable; + +@Immutable +@Modifiable +public interface SomeGeneratedModel { + @Nullable + List<@Nullable String> getThings(); +} diff --git a/maven-plugin-tools-annotations/pom.xml b/maven-plugin-tools-annotations/pom.xml index 8753d2c1..0b6fb6b0 100644 --- a/maven-plugin-tools-annotations/pom.xml +++ b/maven-plugin-tools-annotations/pom.xml @@ -96,10 +96,6 @@ org.jsoup jsoup - - com.thoughtworks.qdox - qdox - com.github.javaparser javaparser-symbol-solver-core diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocBlockTagsToXhtmlConverter.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocBlockTagsToXhtmlConverter.java index 327f67f7..88454109 100644 --- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocBlockTagsToXhtmlConverter.java +++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavadocBlockTagsToXhtmlConverter.java @@ -24,7 +24,6 @@ import java.util.Map; -import com.thoughtworks.qdox.model.DocletTag; import org.apache.maven.tools.plugin.extractor.annotations.converter.tag.block.JavadocBlockTagToHtmlConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,10 +51,6 @@ public JavadocBlockTagsToXhtmlConverter( this.blockTagConverters = blockTagConverters; } - public String convert(DocletTag docletTag, ConverterContext context) { - return convert(docletTag.getName(), docletTag.getValue(), context); - } - public String convert(String name, String text, ConverterContext context) { JavadocBlockTagToHtmlConverter converter = blockTagConverters.get(name); if (converter == null) { diff --git a/pom.xml b/pom.xml index 9ca5ab55..b438e976 100644 --- a/pom.xml +++ b/pom.xml @@ -235,11 +235,6 @@ 2.4.1 - - com.thoughtworks.qdox - qdox - 2.2.0 - com.github.javaparser javaparser-symbol-solver-core diff --git a/src/site/resources/images/plugin-tools-deps.svg b/src/site/resources/images/plugin-tools-deps.svg index d0c2747c..448bb3be 100644 --- a/src/site/resources/images/plugin-tools-deps.svg +++ b/src/site/resources/images/plugin-tools-deps.svg @@ -69,11 +69,11 @@ - - - - - qdox + + + + + JavaParser