Java: Generate module-info files for all modules in project (IDEA-184148)

This commit is contained in:
Pavel Dolgov
2018-01-25 17:32:10 +03:00
parent 0c0b4ea713
commit 7d96a66fdd
4 changed files with 516 additions and 0 deletions

View File

@@ -48,6 +48,7 @@
<orderEntry type="module" module-name="intellij.platform.builtInServer.impl" />
<orderEntry type="module" module-name="intellij.java.uast" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.java.compiler" />
<orderEntry type="module-library">
<library>
<CLASSES>

View File

@@ -0,0 +1,510 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection.java19api;
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
import com.intellij.codeInspection.AbstractDependencyVisitor;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.compiler.CompileScope;
import com.intellij.openapi.compiler.CompilerManager;
import com.intellij.openapi.compiler.CompilerPaths;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.light.LightJavaModule;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
/**
* @author Pavel.Dolgov
*/
public class Java9GenerateModuleDescriptorsAction extends AnAction {
private static final Logger LOG = Logger.getInstance(Java9GenerateModuleDescriptorsAction.class);
private static final String TITLE = "Generate Module Descriptors";
@Override
public void update(AnActionEvent e) {
Project project = e.getProject();
e.getPresentation().setEnabled(project != null && !DumbService.isDumb(project));
}
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
if (project == null) return;
CompilerManager compilerManager = CompilerManager.getInstance(project);
CompileScope scope = compilerManager.createProjectCompileScope(project);
if (!compilerManager.isUpToDate(scope)) {
int result = Messages.showYesNoCancelDialog(project,
"The project needs to be built for better accuracy of dependencies calculation. \n" +
"Start the build before generating module-info descriptors?",
TITLE, null);
if (result == Messages.CANCEL) {
return;
}
if (result == Messages.YES) {
compilerManager.compile(scope, (aborted, errors, warnings, compileContext) -> {
if (!aborted && errors == 0) {
generate(project);
}
});
return;
}
}
generate(project);
}
private static void generate(Project project) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
THashMap<Module, List<File>> classFiles = new THashMap<>();
int totalFiles = collectClassFiles(project, classFiles);
if (totalFiles != 0) {
new DescriptorsGenerator(project).generate(classFiles, totalFiles);
}
}, TITLE, true, project);
}
private static int collectClassFiles(@NotNull Project project, @NotNull Map<Module, List<File>> classFiles) {
ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
indicator.setIndeterminate(true);
indicator.setText("Scanning Compiler Output");
Module[] modules = StreamEx.of(ModuleManager.getInstance(project).getModules())
.filter(module -> mayContainModuleInfo(module))
.toArray(Module.EMPTY_ARRAY);
if (modules.length == 0) {
CommonRefactoringUtil.showErrorHint(project, null, "Found no modules which may contain module-info", TITLE, null);
return 0;
}
int totalFiles = 0;
for (Module module : modules) {
List<File> moduleClasses = new ArrayList<>();
classFiles.put(module, moduleClasses);
String production = CompilerPaths.getModuleOutputPath(module, false);
File productionRoot = production != null ? new File(production) : null;
if (productionRoot != null) {
collectClassFiles(productionRoot, moduleClasses);
}
totalFiles += moduleClasses.size();
}
if (totalFiles == 0) {
CommonRefactoringUtil
.showErrorHint(project, null, "Couldn't generate module descriptors because the project hasn't been built yet", TITLE, null);
}
return totalFiles;
}
private static boolean mayContainModuleInfo(Module module) {
return ReadAction.compute(() -> {
ModuleRootManager moduleManager = ModuleRootManager.getInstance(module);
PsiManager psiManager = PsiManager.getInstance(module.getProject());
return StreamEx.of(moduleManager.getSourceRoots(false))
.map(psiManager::findDirectory)
.nonNull()
.anyMatch(psiDir -> PsiUtil.getLanguageLevel(psiDir).isAtLeast(LanguageLevel.JDK_1_9));
});
}
private static void collectClassFiles(@NotNull File file, @NotNull List<File> files) {
final File[] children = file.listFiles();
if (children != null) { // is Directory
for (File child : children) {
collectClassFiles(child, files);
}
}
else if (file.getName().endsWith(CommonClassNames.CLASS_FILE_EXTENSION)) {
files.add(file);
}
}
private static class ProgressTracker {
ProgressIndicator myIndicator = ProgressManager.getInstance().getProgressIndicator();
int myCount;
int mySize;
int myPhase;
double myUpToNow;
private double[] myPhases;
public ProgressTracker(double... phases) {
myPhases = phases;
myIndicator.setFraction(0);
myIndicator.setIndeterminate(false);
}
void startPhase(String text, int size) {
myIndicator.setText(text);
myCount = 0;
mySize = Math.min(size, 1);
}
public void nextPhase() {
myUpToNow += myPhases[myPhase++];
}
public void increment() {
myIndicator.setFraction(myUpToNow + myPhases[myPhase] * ++myCount / (double)mySize);
}
}
private static class DescriptorsGenerator {
private Project myProject;
List<ModuleNode> moduleNodes = new ArrayList<>();
Set<String> usedExternallyPackages = new THashSet<>();
ProgressTracker myProgressTracker = new ProgressTracker(0.3, 0.2, 0.2, 0.3);
public DescriptorsGenerator(Project project) {
myProject = project;
}
void generate(THashMap<Module, List<File>> classFiles, int totalFiles) {
myProgressTracker.startPhase("Collecting Dependencies", totalFiles);
Map<String, Set<ModuleNode>> packagesDeclaredInModules = collectDependencies(classFiles);
myProgressTracker.nextPhase();
myProgressTracker.startPhase("Analysing Dependencies", moduleNodes.size());
analyseDependencies(packagesDeclaredInModules);
myProgressTracker.nextPhase();
myProgressTracker.startPhase("Generating Code", moduleNodes.size());
List<Pair.NonNull<PsiDirectory, String>> generatedCode = generateCode();
myProgressTracker.nextPhase();
myProgressTracker.startPhase("Formatting Code", generatedCode.size());
createFiles(generatedCode);
}
private Map<String, Set<ModuleNode>> collectDependencies(THashMap<Module, List<File>> classFiles) {
PackageNamesCache packageNamesCache = new PackageNamesCache(myProject);
Map<String, Set<ModuleNode>> packagesDeclaredInModules = new THashMap<>();
for (Map.Entry<Module, List<File>> entry : classFiles.entrySet()) {
Module module = entry.getKey();
ModuleVisitor visitor = new ModuleVisitor(packageNamesCache);
for (File file : entry.getValue()) {
visitor.processFile(file);
myProgressTracker.increment();
}
Set<String> declaredPackages = visitor.getDeclaredPackages();
Set<String> requiredPackages = visitor.getRequiredPackages();
requiredPackages.removeAll(declaredPackages);
usedExternallyPackages.addAll(requiredPackages);
ModuleNode moduleNode = new ModuleNode(module, declaredPackages, requiredPackages);
moduleNodes.add(moduleNode);
for (String declaredPackage : declaredPackages) {
packagesDeclaredInModules.computeIfAbsent(declaredPackage, __ -> new THashSet<>()).add(moduleNode);
}
}
return packagesDeclaredInModules;
}
private void analyseDependencies(Map<String, Set<ModuleNode>> packagesDeclaredInModules) {
Map<PsiJavaModule, ModuleNode> nodesByDescriptor = new THashMap<>();
for (ModuleNode moduleNode : moduleNodes) {
if (moduleNode.getDescriptor() != null) {
nodesByDescriptor.put(moduleNode.getDescriptor(), moduleNode);
}
for (String packageName : moduleNode.getRequiredPackages()) {
Set<ModuleNode> set = packagesDeclaredInModules.get(packageName);
if (set == null) {
PsiPackage psiPackage = JavaPsiFacade.getInstance(myProject).findPackage(packageName);
if (psiPackage != null) {
PsiJavaModule descriptor = ReadAction.compute(() -> findDescriptor(psiPackage));
if (descriptor != null) {
ModuleNode dependencyNode = nodesByDescriptor.computeIfAbsent(descriptor, ModuleNode::new);
moduleNode.getDependencies().add(dependencyNode);
}
}
}
else {
if (set.size() != 1) {
LOG.debug("Split package " + packageName + " in " + set);
}
moduleNode.getDependencies().addAll(set);
}
}
myProgressTracker.increment();
}
}
private List<Pair.NonNull<PsiDirectory, String>> generateCode() {
List<Pair.NonNull<PsiDirectory, String>> generatedCode = new ArrayList<>();
for (ModuleNode moduleNode : moduleNodes) {
if (moduleNode.getDescriptor() != null) {
LOG.debug("Descriptor already exists in " + moduleNode);
continue;
}
for (String packageName : moduleNode.getDeclaredPackages()) {
if (usedExternallyPackages.contains(packageName)) {
moduleNode.getExports().add(packageName);
}
}
StringBuilder text = new StringBuilder();
text.append("module ").append(moduleNode.getName()).append(" {");
List<ModuleNode> sortedDependencies = moduleNode.getSortedDependencies();
List<String> sortedExports = moduleNode.getSortedExports();
for (ModuleNode dependencyNode : sortedDependencies) {
if (!"java.base".equals(dependencyNode.getName())) {
text.append("\n requires ").append(dependencyNode.getName()).append(";");
}
}
if (!sortedDependencies.isEmpty() && !sortedExports.isEmpty()) {
text.append('\n');
}
for (String packageName : sortedExports) {
text.append("\n exports ").append(packageName).append(";");
}
text.append("\n}");
PsiDirectory rootDir = moduleNode.getRootDir();
if (rootDir != null) {
generatedCode.add(Pair.createNonNull(rootDir, text.toString()));
}
else {
LOG.debug("Skipped module " + moduleNode);
}
myProgressTracker.increment();
}
return generatedCode;
}
private void createFiles(List<Pair.NonNull<PsiDirectory, String>> generatedCode) {
ApplicationManager.getApplication().invokeAndWait(
() -> CommandProcessor.getInstance().executeCommand(myProject,
() -> createFilesImpl(generatedCode),
TITLE, null));
}
private void createFilesImpl(List<Pair.NonNull<PsiDirectory, String>> generatedCode) {
for (Pair.NonNull<PsiDirectory, String> code : generatedCode) {
PsiDirectory rootDir = code.getFirst();
String text = code.getSecond();
PsiFile file = PsiFileFactory.getInstance(myProject)
.createFileFromText(PsiJavaModule.MODULE_INFO_FILE, JavaLanguage.INSTANCE, text);
WriteAction.run(
() -> {
PsiElement added = rootDir.add(file);
CodeStyleManager.getInstance(myProject).reformat(added);
});
myProgressTracker.increment();
}
}
@Nullable
private static PsiJavaModule findDescriptor(PsiPackage psiPackage) {
PsiManager psiManager = psiPackage.getManager();
return StreamEx.of(psiPackage.getDirectories())
.map(PsiDirectory::getVirtualFile)
.nonNull()
.map(psiManager::findDirectory)
.nonNull()
.findAny()
.map(JavaModuleGraphUtil::findDescriptorByElement)
.orElse(null);
}
}
private static class ModuleNode implements Comparable<ModuleNode> {
private final Module myModule;
private final Set<String> myDeclaredPackages;
private final Set<String> myRequiredPackages;
private final Set<ModuleNode> myDependencies = new THashSet<>();
private final Set<String> myExports = new THashSet<>();
private final PsiJavaModule myDescriptor;
private String myName;
public ModuleNode(@NotNull Module module, @NotNull Set<String> declaredPackages, @NotNull Set<String> requiredPackages) {
myModule = module;
myDeclaredPackages = declaredPackages;
myRequiredPackages = requiredPackages;
myDescriptor = ReadAction.compute(() -> {
VirtualFile[] sourceRoots = ModuleRootManager.getInstance(module).getSourceRoots(false);
return sourceRoots.length != 0 ? findDescriptor(module, sourceRoots[0]) : null;
});
myName = myDescriptor != null ? myDescriptor.getName() : LightJavaModule.moduleName(myModule.getName());
}
@Nullable
private static PsiJavaModule findDescriptor(@NotNull Module module, VirtualFile root) {
return JavaModuleGraphUtil.findDescriptorByElement(PsiManager.getInstance(module.getProject()).findDirectory(root));
}
public ModuleNode(@NotNull PsiJavaModule descriptor) {
myModule = ReadAction.compute(() -> ModuleUtilCore.findModuleForPsiElement(descriptor));
myDeclaredPackages = Collections.emptySet();
myRequiredPackages = Collections.emptySet();
myDescriptor = descriptor;
myName = myDescriptor.getName();
}
public Set<String> getDeclaredPackages() {
return myDeclaredPackages;
}
public Set<String> getRequiredPackages() {
return myRequiredPackages;
}
public Set<ModuleNode> getDependencies() {
return myDependencies;
}
public List<ModuleNode> getSortedDependencies() {
ArrayList<ModuleNode> list = new ArrayList<>(myDependencies);
list.sort(ModuleNode::compareTo);
return list;
}
public Set<String> getExports() {
return myExports;
}
public List<String> getSortedExports() {
ArrayList<String> list = new ArrayList<>(myExports);
list.sort(String::compareTo);
return list;
}
public PsiJavaModule getDescriptor() {
return myDescriptor;
}
public String getName() {
return myName;
}
@Override
public String toString() {
return myName;
}
@Override
public int compareTo(@NotNull ModuleNode o) {
int m1 = myModule == null ? 0 : 1, m2 = o.myModule == null ? 0 : 1;
if (m1 != m2) return m1 - m2;
int j1 = myName.startsWith("java.") || myName.startsWith("javax.") ? 0 : 1;
int j2 = o.myName.startsWith("java.") || o.myName.startsWith("javax.") ? 0 : 1;
if (j1 != j2) return j1 - j2;
return StringUtil.compare(myName, o.myName, false);
}
public PsiDirectory getRootDir() {
if (myModule == null) return null;
return ReadAction.compute(() -> {
ModuleRootManager moduleManager = ModuleRootManager.getInstance(myModule);
PsiManager psiManager = PsiManager.getInstance(myModule.getProject());
return findJavaDirectory(psiManager, moduleManager.getSourceRoots(false));
});
}
@Nullable
private static PsiDirectory findJavaDirectory(PsiManager psiManager, VirtualFile[] roots) {
return StreamEx.of(roots)
.sorted(Comparator.comparingInt((VirtualFile vFile) -> "java".equals(vFile.getName()) ? 0 : 1)
.thenComparing(VirtualFile::getName))
.map(psiManager::findDirectory)
.nonNull()
.findFirst()
.orElse(null);
}
}
private static class PackageNamesCache {
private final Map<String, Boolean> myPackages = new THashMap<>();
private final JavaPsiFacade myPsiFacade;
public PackageNamesCache(Project project) {
myPsiFacade = JavaPsiFacade.getInstance(project);
}
private String getPackageName(String className) {
for (int dotPos = className.lastIndexOf('.'); dotPos > 0; dotPos = className.lastIndexOf('.', dotPos - 1)) {
String packageName = className.substring(0, dotPos);
Boolean isPackage = myPackages.computeIfAbsent(packageName, this::isExistingPackage);
if (isPackage) {
return packageName;
}
}
return null;
}
@NotNull
private Boolean isExistingPackage(String packageName) {
return ReadAction.compute(() -> myPsiFacade.findPackage(packageName) != null);
}
}
private static class ModuleVisitor extends AbstractDependencyVisitor {
private final Set<String> myRequiredPackages = new THashSet<>();
private final Set<String> myDeclaredPackages = new THashSet<>();
private final PackageNamesCache myPackageNamesCache;
public ModuleVisitor(PackageNamesCache packageNamesCache) {
myPackageNamesCache = packageNamesCache;
}
@Override
protected void addClassName(String className) {
String packageName = myPackageNamesCache.getPackageName(className);
if (packageName != null) {
myRequiredPackages.add(packageName);
}
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
String packageName = myPackageNamesCache.getPackageName(getCurrentClassName());
if (packageName != null) {
myDeclaredPackages.add(packageName);
}
}
public Set<String> getRequiredPackages() {
return myRequiredPackages;
}
public Set<String> getDeclaredPackages() {
return myDeclaredPackages;
}
}
}

View File

@@ -631,6 +631,8 @@ action.ShowPackageCycles.text=Analyze _Cyclic Dependencies...
action.ShowPackageCycles.description=Browse code chosen analysis item used in cycles
action.ShowModulesDependencies.text=Analyze _Module Dependencies...
action.ShowModulesDependencies.description=Show dependencies between modules in project
action.GenerateModuleDescriptors.text=Generate module-info Descriptors
action.GenerateModuleDescriptors.description=Generate module-info files for all modules in the project (for Java 9 and higher)
action.Unscramble.text=Analyze _Stacktrace...
action.Unscramble.description=Open console with the navigatable stacktrace
action.IdeScriptingConsole.text=IDE Scripting Console

View File

@@ -35,6 +35,9 @@
<add-to-group group-id="InspectCodeGroup" anchor="after" relative-to-action="ViewOfflineInspection"/>
</action>
<action id="GenerateModuleDescriptors" class="com.intellij.codeInspection.java19api.Java9GenerateModuleDescriptorsAction">
<add-to-group group-id="InspectCodeGroup" anchor="after" relative-to-action="ViewOfflineInspection"/>
</action>
<action id="StubHierarchy" class="com.intellij.psi.stubsHierarchy.impl.test.BuildStubsHierarchyAction" text="Build Stub Hierarchy" internal="true">
<add-to-group group-id="InspectCodeGroup" anchor="after" relative-to-action="ViewOfflineInspection"/>
</action>