IDEA-179255 - support javadoc generation for multiple jpms modules
authorIlyas Selimov <ilyas.selimov@jetbrains.com>
Fri, 9 Oct 2020 08:19:43 +0000 (15:19 +0700)
committerintellij-monorepo-bot <intellij-monorepo-bot-no-reply@jetbrains.com>
Fri, 9 Oct 2020 09:36:00 +0000 (09:36 +0000)
GitOrigin-RevId: 9ce822caf8ed4c22a8a1ad2a1b7d991b01569ba4

java/java-impl/src/com/intellij/javadoc/JavadocGeneratorRunProfile.java
java/openapi/resources/messages/JavaBundle.properties

index 2000270f90336f122d9ce8ca3648dbaff5ee8442..071607f4b257ce0efb919c3369248f79a0dec0a8 100644 (file)
@@ -2,6 +2,7 @@
 package com.intellij.javadoc;
 
 import com.intellij.analysis.AnalysisScope;
+import com.intellij.codeInspection.SmartHashMap;
 import com.intellij.execution.CantRunException;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
@@ -26,6 +27,7 @@ import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.projectRoots.ex.JavaSdkUtil;
 import com.intellij.openapi.projectRoots.ex.PathUtilEx;
 import com.intellij.openapi.roots.JavadocOrderRootType;
+import com.intellij.openapi.roots.ModuleRootManager;
 import com.intellij.openapi.roots.OrderEnumerator;
 import com.intellij.openapi.roots.ProjectRootManager;
 import com.intellij.openapi.util.SystemInfo;
@@ -35,15 +37,16 @@ import com.intellij.openapi.vfs.CharsetToolkit;
 import com.intellij.openapi.vfs.VfsUtil;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.*;
+import com.intellij.util.PathUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.SmartHashSet;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import javax.swing.*;
 import java.io.*;
 import java.nio.charset.Charset;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -212,84 +215,151 @@ public class JavadocGeneratorRunProfile implements ModuleRunProfile {
         parameters.add(myConfiguration.OUTPUT_DIRECTORY.replace('/', File.separatorChar));
       }
 
-      try {
-        File argsFile = FileUtil.createTempFile("javadoc_args", null);
-        Charset cs = CharsetToolkit.getPlatformCharset();
-
-        try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(argsFile), cs))) {
-          Set<Module> modules = new LinkedHashSet<>();
-          Set<VirtualFile> sources = new HashSet<>();
-          Runnable r = () -> myGenerationOptions.accept(new MyContentIterator(myProject, modules, sources));
-          String title = JavaBundle.message("javadoc.generate.sources.progress");
-          if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(r, title, true, myProject)) {
-            return;
-          }
-          if (sources.isEmpty()) {
-            throw new CantRunException(JavaBundle.message("javadoc.generate.no.classes.in.selected.packages.error"));
-          }
-
-          boolean hasJavaModules = sources.stream().anyMatch(f -> PsiJavaModule.MODULE_INFO_FILE.equals(f.getName()));
-          if (hasJavaModules && modules.size() > 1) {
-            throw new CantRunException(JavaBundle.message("javadoc.gen.error.multiple.modules.with.module.info"));
-          }
+      Set<Module> modules = new LinkedHashSet<>();
+      Set<VirtualFile> sources = new HashSet<>();
+      Runnable r = () -> myGenerationOptions.accept(new MyContentIterator(myProject, modules, sources));
+      String title = JavaBundle.message("javadoc.generate.sources.progress");
+      if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(r, title, true, myProject)) {
+        return;
+      }
+      if (sources.isEmpty()) {
+        throw new CantRunException(JavaBundle.message("javadoc.generate.no.classes.in.selected.packages.error"));
+      }
 
-          OrderEnumerator sourcePathEnumerator = ProjectRootManager.getInstance(myProject).orderEntries(modules);
-          if (!myConfiguration.OPTION_INCLUDE_LIBS) {
-            sourcePathEnumerator = sourcePathEnumerator.withoutSdk().withoutLibraries();
-          }
-          if (!myGenerationOptions.isIncludeTestSource()) {
-            sourcePathEnumerator = sourcePathEnumerator.productionOnly();
-          }
-          List<VirtualFile> sourceRoots = sourcePathEnumerator.getSourcePathsList().getRootDirs();
+      Set<Module> modulesWithoutDescriptor = new SmartHashSet<>(modules);
+      Map<Module, VirtualFile> moduleDescriptors = new SmartHashMap<>();
+      boolean hasJavaModules = false;
+      for (VirtualFile source : sources) {
+        if (!PsiJavaModule.MODULE_INFO_FILE.equals(source.getName())) continue;
+        hasJavaModules = true;
+        Module module = ModuleUtilCore.findModuleForFile(source, myProject);
+        if (module != null) {
+          moduleDescriptors.put(module, source);
+          modulesWithoutDescriptor.remove(module);
+        }
+      }
+      if (hasJavaModules && !modulesWithoutDescriptor.isEmpty()) {
+        // So far we can't generate javadoc for each module independently as we have to merge the results into common files,
+        // e.g index.html, index-all.html and so on. Moreover, the final javadoc seems obscured in the case when one module contains
+        // module-info file but another one is not.
+        throw new CantRunException(JavaBundle.message("javadoc.gen.error.modules.without.module.info", modulesWithoutDescriptor.stream()
+          .map(m -> StringUtil.SINGLE_QUOTER.fun(m.getName())).collect(Collectors.joining(","))));
+      }
 
-          OrderEnumerator classPathEnumerator = ProjectRootManager.getInstance(myProject).orderEntries(modules).withoutModuleSourceEntries();
-          if (jdk.getSdkType() instanceof JavaSdk) {
-            classPathEnumerator = classPathEnumerator.withoutSdk();
-          }
-          if (!myGenerationOptions.isIncludeTestSource()) {
-            classPathEnumerator = classPathEnumerator.productionOnly();
-          }
-          List<VirtualFile> classRoots = classPathEnumerator.getPathsList().getRootDirs();
-
-          if (sourceRoots.size() + classRoots.size() > 0) {
-            if (hasJavaModules && JavaSdkUtil.isJdkAtLeast(jdk, JavaSdkVersion.JDK_1_9)) {
-              if (!sourceRoots.isEmpty()) {
-                String path = sourceRoots.stream().map(MyJavaCommandLineState::localPath).collect(Collectors.joining(File.pathSeparator));
-                writer.println("--source-path");
-                writer.println(StringUtil.wrapWithDoubleQuote(path));
-              }
-              if (!classRoots.isEmpty()) {
-                String path = classRoots.stream().map(MyJavaCommandLineState::localPath).collect(Collectors.joining(File.pathSeparator));
-                writer.println("--module-path");
-                writer.println(StringUtil.wrapWithDoubleQuote(path));
+      File argsFile = createTempArgsFile();
+      List<VirtualFile> sourceRoots = findSourceRoots(modules);
+      List<VirtualFile> classRoots = findClassRoots(modules, jdk);
+
+      Charset cs = CharsetToolkit.getPlatformCharset();
+      try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(argsFile), cs))) {
+        if (sourceRoots.size() + classRoots.size() > 0) {
+          if (hasJavaModules && JavaSdkUtil.isJdkAtLeast(jdk, JavaSdkVersion.JDK_1_9)) {
+            if (modules.size() > 1) {
+              writer.println("--module-source-path");
+              String moduleSourcePath = computeModuleSourcePath(moduleDescriptors);
+              if (moduleSourcePath == null) {
+                throw new CantRunException(JavaBundle.message("javadoc.gen.error.module.source.path.is.not.evaluated"));
               }
+              writer.println(StringUtil.wrapWithDoubleQuote(moduleSourcePath));
             }
-            else {
-              // placing source roots on a classpath is perfectly legal and allows generating correct Javadoc
-              // when a module without a module-info.java file depends on another module that has one
-              Stream<VirtualFile> roots = Stream.concat(sourceRoots.stream(), classRoots.stream());
-              String path = roots.map(MyJavaCommandLineState::localPath).collect(Collectors.joining(File.pathSeparator));
-              writer.println("-classpath");
+            else if (!sourceRoots.isEmpty()) {
+              String path = sourceRoots.stream().map(MyJavaCommandLineState::localPath).collect(Collectors.joining(File.pathSeparator));
+              writer.println("--source-path");
               writer.println(StringUtil.wrapWithDoubleQuote(path));
             }
-          }
 
-          for (VirtualFile source : sources) {
-            writer.println(StringUtil.wrapWithDoubleQuote(source.getPath()));
+            if (!classRoots.isEmpty()) {
+              String path = classRoots.stream().map(MyJavaCommandLineState::localPath).collect(Collectors.joining(File.pathSeparator));
+              writer.println("--module-path");
+              writer.println(StringUtil.wrapWithDoubleQuote(path));
+            }
+          }
+          else {
+            // placing source roots on a classpath is perfectly legal and allows generating correct Javadoc
+            // when a module without a module-info.java file depends on another module that has one
+            Stream<VirtualFile> roots = Stream.concat(sourceRoots.stream(), classRoots.stream());
+            String path = roots.map(MyJavaCommandLineState::localPath).collect(Collectors.joining(File.pathSeparator));
+            writer.println("-classpath");
+            writer.println(StringUtil.wrapWithDoubleQuote(path));
           }
         }
 
-        myArgFileFilter.setPath(argsFile.getPath(), cs);
-        parameters.add("@" + argsFile.getPath());
-        OSProcessHandler.deleteFileOnTermination(cmdLine, argsFile);
-        cmdLine.setCharset(cs);
+        for (VirtualFile source : sources) {
+          writer.println(StringUtil.wrapWithDoubleQuote(source.getPath()));
+        }
+      }
+      catch (FileNotFoundException e) {
+        throw new CantRunException(JavaBundle.message("javadoc.generate.temp.file.does.not.exist"), e);
+      }
+      catch (CantRunException e) {
+        FileUtil.delete(argsFile);
+        throw e;
+      }
+
+      myArgFileFilter.setPath(argsFile.getPath(), cs);
+      parameters.add("@" + argsFile.getPath());
+      OSProcessHandler.deleteFileOnTermination(cmdLine, argsFile);
+      cmdLine.setCharset(cs);
+    }
+
+    @NotNull
+    private static File createTempArgsFile() throws CantRunException {
+      File argsFile;
+      try {
+        argsFile = FileUtil.createTempFile("javadoc_args", null);
       }
       catch (IOException e) {
         throw new CantRunException(JavaBundle.message("javadoc.generate.temp.file.error"), e);
       }
+      return argsFile;
     }
 
-    private static String localPath(VirtualFile root) {
+    @NotNull
+    private List<VirtualFile> findSourceRoots(@NotNull Set<Module> modules) {
+      OrderEnumerator sourcePathEnumerator = ProjectRootManager.getInstance(myProject).orderEntries(modules);
+      if (!myConfiguration.OPTION_INCLUDE_LIBS) {
+        sourcePathEnumerator = sourcePathEnumerator.withoutSdk().withoutLibraries();
+      }
+      if (!myGenerationOptions.isIncludeTestSource()) {
+        sourcePathEnumerator = sourcePathEnumerator.productionOnly();
+      }
+      return sourcePathEnumerator.getSourcePathsList().getRootDirs();
+    }
+
+    @NotNull
+    private List<VirtualFile> findClassRoots(@NotNull Set<Module> modules, @NotNull Sdk jdk) {
+      OrderEnumerator classPathEnumerator = ProjectRootManager.getInstance(myProject).orderEntries(modules).withoutModuleSourceEntries();
+      if (jdk.getSdkType() instanceof JavaSdk) {
+        classPathEnumerator = classPathEnumerator.withoutSdk();
+      }
+      if (!myGenerationOptions.isIncludeTestSource()) {
+        classPathEnumerator = classPathEnumerator.productionOnly();
+      }
+      return classPathEnumerator.getPathsList().getRootDirs();
+    }
+
+    /**
+     * If a project contains multiple jpms modules then we have to form --module-source-path.
+     *
+     * @see https://docs.oracle.com/javase/9/tools/javadoc.htm
+     */
+    @Nullable
+    private static String computeModuleSourcePath(@NotNull Map<Module, VirtualFile> moduleDescriptors) {
+      if (moduleDescriptors.isEmpty()) return null;
+      Set<String> moduleSourcePathParts = new SmartHashSet<>();
+      for (var entry : moduleDescriptors.entrySet()) {
+        String descriptorParentPath = PathUtil.getParentPath(entry.getValue().getPath());
+        VirtualFile modulePath = ContainerUtil.find(ModuleRootManager.getInstance(entry.getKey()).getContentRoots(),
+                                                    f -> descriptorParentPath.contains(f.getName()));
+        if (modulePath == null) return null;
+        String moduleSourcePathPart = descriptorParentPath.replace(modulePath.getName(), "*");
+        moduleSourcePathParts.add(moduleSourcePathPart);
+      }
+      return String.join(File.pathSeparator, moduleSourcePathParts);
+    }
+
+    @NotNull
+    private static String localPath(@NotNull VirtualFile root) {
       // @argfile require forward slashes in quoted paths
       return VfsUtil.getLocalFile(root).getPath();
     }
index a6346d74115c0bfc6b12a6e9938cb59787f7f6cd..d986ae3cdc469e3f2d689b614f959491d29397f3 100644 (file)
@@ -890,6 +890,7 @@ javadoc.generate.output.directory.browse=Browse Output Directory
 javadoc.generate.sources.progress=Looking for sources to generate Javadoc
 javadoc.generate.tag.list.deprecated=deprecated list
 javadoc.generate.temp.file.error=Cannot create temporary file
+javadoc.generate.temp.file.does.not.exist=Temporary file doesn''t exist or cannot be opened
 javadoc.generate.title=Generate JavaDoc
 javadoc.method.in.class={0} in class {1}
 javadoc.method.in.interface={0} in interface {1}
@@ -1493,7 +1494,8 @@ command.name.delegate.detected.change=Delegate
 encapsulate.fields.dialog.javadoc.title=JavaDoc
 find.jar.hint.text.no.libraries.found.for.fqn=No libraries found for ''{0}''
 dependant.sdk.unsatisfied.dependency.message=You need to configure a Java SDK first
-javadoc.gen.error.multiple.modules.with.module.info=At the moment, IDEA cannot generate Javadoc for multiple modules with module-info.java files in them. Sorry. We're working on this.
+javadoc.gen.error.modules.without.module.info=IDEA cannot generate Javadoc as modules {0} don''t contain module-info.java file
+javadoc.gen.error.module.source.path.is.not.evaluated=IDEA cannot generate Javadoc as module-source-path cannot be evaluated
 generate.members.implement.command=implement
 code.style.settings.angle.spacing.brackets=Angle brackets
 code.style.settings.spacing.after.closing.angle.bracket=After closing angle bracket