constructor reference: don't ignore constructor parameters during method reference...
[idea/community.git] / java / java-analysis-impl / src / com / intellij / codeInspection / java19modules / Java9ModuleEntryPoint.java
1 // Copyright 2000-2019 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.
2 package com.intellij.codeInspection.java19modules;
3
4 import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
5 import com.intellij.codeInspection.reference.*;
6 import com.intellij.codeInspection.visibility.EntryPointWithVisibilityLevel;
7 import com.intellij.openapi.util.InvalidDataException;
8 import com.intellij.openapi.util.WriteExternalException;
9 import com.intellij.psi.*;
10 import com.intellij.psi.util.CachedValueProvider;
11 import com.intellij.psi.util.CachedValuesManager;
12 import com.intellij.psi.util.PsiUtil;
13 import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters;
14 import com.intellij.util.xmlb.XmlSerializer;
15 import gnu.trove.THashSet;
16 import one.util.streamex.StreamEx;
17 import org.jdom.Element;
18 import org.jetbrains.annotations.NotNull;
19 import org.jetbrains.annotations.Nullable;
20
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24
25 /**
26  * @author Pavel.Dolgov
27  */
28 public class Java9ModuleEntryPoint extends EntryPointWithVisibilityLevel {
29   public static final String ID = "moduleInfo";
30
31   public boolean ADD_EXPORTED_PACKAGES_AND_SERVICES_TO_ENTRIES = true;
32
33   @Override
34   public String getId() {
35     return ID;
36   }
37
38   @NotNull
39   @Override
40   public String getDisplayName() {
41     return "<html>Classes exposed with <code>module-info</code></html>";
42   }
43
44   @Override
45   public String getTitle() {
46     return "Suggest package-private visibility level for classes in exported packages (Java 9+)";
47   }
48
49   @Override
50   public boolean isEntryPoint(@NotNull RefElement refElement, @NotNull PsiElement psiElement) {
51     return isEntryPoint(psiElement);
52   }
53
54   @Override
55   public boolean isEntryPoint(@NotNull PsiElement psiElement) {
56     if (psiElement instanceof PsiClass) {
57       return isServiceOrExported((PsiClass)psiElement);
58     }
59     if (psiElement instanceof PsiMethod) {
60       PsiMethod method = (PsiMethod)psiElement;
61       if (isDefaultConstructor(method) || isProviderMethod(method)) {
62         return isServiceOrExported(method.getContainingClass());
63       }
64     }
65     return false;
66   }
67
68   @Override
69   public int getMinVisibilityLevel(PsiMember member) {
70     if (member instanceof PsiClass) {
71       PsiJavaModule javaModule = getJavaModule(member);
72       if (javaModule != null && !isServiceClass((PsiClass)member, javaModule) && isInExportedPackage((PsiClass)member, javaModule)) {
73         return PsiUtil.ACCESS_LEVEL_PACKAGE_LOCAL;
74       }
75     }
76     return ACCESS_LEVEL_INVALID;
77   }
78
79   @Override
80   public boolean keepVisibilityLevel(boolean entryPointEnabled, @NotNull RefJavaElement refJavaElement) {
81     if (refJavaElement instanceof RefClass) {
82       RefClass refClass = (RefClass)refJavaElement;
83       RefModule refModule = refClass.getModule();
84       if (refModule != null) {
85         RefJavaModule refJavaModule = RefJavaModule.JAVA_MODULE.get(refModule);
86         if (refJavaModule != null) {
87           return isServiceClass(refClass, refJavaModule) || !entryPointEnabled && isInExportedPackage(refClass, refJavaModule);
88         }
89       }
90     }
91     return false;
92   }
93
94   private static boolean isInExportedPackage(@Nullable RefClass refClass, @NotNull RefJavaModule refJavaModule) {
95     RefEntity refOwner = refClass;
96     while (refOwner instanceof RefClass) {
97       String modifier = ((RefClass)refOwner).getAccessModifier();
98       refOwner = PsiModifier.PUBLIC.equals(modifier) || PsiModifier.PROTECTED.equals(modifier) ? refOwner.getOwner() : null;
99     }
100     if (refOwner instanceof RefPackage) {
101       Map<String, List<String>> exportedPackageNames = refJavaModule.getExportedPackageNames();
102       if (exportedPackageNames.containsKey(refOwner.getQualifiedName())) {
103         return true;
104       }
105     }
106     return false;
107   }
108
109   private static boolean isServiceClass(@Nullable RefClass refClass, @NotNull RefJavaModule refJavaModule) {
110     return refJavaModule.getServiceInterfaces().contains(refClass) ||
111            refJavaModule.getServiceImplementations().contains(refClass) ||
112            refJavaModule.getUsedServices().contains(refClass);
113   }
114
115   private static boolean isDefaultConstructor(@NotNull PsiMethod method) {
116     return method.isConstructor() &&
117            method.getParameterList().isEmpty() &&
118            method.hasModifierProperty(PsiModifier.PUBLIC);
119   }
120
121   private static boolean isProviderMethod(@NotNull PsiMethod method) {
122     return "provider".equals(method.getName()) &&
123            method.getParameterList().isEmpty() &&
124            method.hasModifierProperty(PsiModifier.PUBLIC) &&
125            method.hasModifierProperty(PsiModifier.STATIC);
126   }
127
128   private static boolean isServiceOrExported(@Nullable PsiClass psiClass) {
129     PsiJavaModule javaModule = getJavaModule(psiClass);
130     return javaModule != null && (isServiceClass(psiClass, javaModule) || isInExportedPackage(psiClass, javaModule));
131   }
132
133   private static @Nullable PsiJavaModule getJavaModule(@Nullable PsiElement element) {
134     return element != null && PsiUtil.isLanguageLevel9OrHigher(element) ? JavaModuleGraphUtil.findDescriptorByElement(element) : null;
135   }
136
137   private static boolean isInExportedPackage(@NotNull PsiClass psiClass, @NotNull PsiJavaModule javaModule) {
138     String packageName = getPublicApiPackageName(psiClass);
139     return packageName != null && getExportedPackageNames(javaModule).contains(packageName);
140   }
141
142   private static boolean isServiceClass(@NotNull PsiClass psiClass, @NotNull PsiJavaModule javaModule) {
143     String className = psiClass.getQualifiedName();
144     return className != null && getServiceClassNames(javaModule).contains(className);
145   }
146
147   private static @Nullable String getPublicApiPackageName(@NotNull PsiClass psiClass) {
148     if (psiClass.hasModifierProperty(PsiModifier.PUBLIC) || psiClass.hasModifierProperty(PsiModifier.PROTECTED)) {
149       PsiElement parent = psiClass.getParent();
150       if (parent instanceof PsiClass) {
151         return getPublicApiPackageName((PsiClass)parent);
152       }
153       if (parent instanceof PsiJavaFile) {
154         return ((PsiJavaFile)parent).getPackageName();
155       }
156     }
157     return null;
158   }
159
160   private static Set<String> getExportedPackageNames(@NotNull PsiJavaModule javaModule) {
161     return CachedValuesManager.getCachedValue(javaModule, () -> {
162       Set<String> packages = StreamEx.of(javaModule.getExports().spliterator())
163         .map(PsiPackageAccessibilityStatement::getPackageName)
164         .nonNull()
165         .toCollection(THashSet::new);
166       return CachedValueProvider.Result.create(packages, javaModule);
167     });
168   }
169
170   private static Set<String> getServiceClassNames(@NotNull PsiJavaModule javaModule) {
171     return CachedValuesManager.getCachedValue(javaModule, () -> {
172       Set<String> classes = StreamEx.of(javaModule.getProvides().spliterator())
173         .map(PsiProvidesStatement::getImplementationList)
174         .nonNull()
175         .flatMap(list -> StreamEx.of(list.getReferenceElements()))
176         .append(StreamEx.of(javaModule.getUses().spliterator())
177           .map(PsiUsesStatement::getClassReference)
178           .nonNull())
179         .map(PsiJavaCodeReferenceElement::getQualifiedName)
180         .nonNull()
181         .toCollection(THashSet::new);
182       return CachedValueProvider.Result.create(classes, javaModule);
183     });
184   }
185
186   @Override
187   public boolean isSelected() {
188     return ADD_EXPORTED_PACKAGES_AND_SERVICES_TO_ENTRIES;
189   }
190
191   @Override
192   public void setSelected(boolean selected) {
193     ADD_EXPORTED_PACKAGES_AND_SERVICES_TO_ENTRIES = selected;
194   }
195
196   @Override
197   public void readExternal(Element element) throws InvalidDataException {
198     XmlSerializer.deserializeInto(this, element);
199   }
200
201   @Override
202   public void writeExternal(Element element) throws WriteExternalException {
203     XmlSerializer.serializeInto(this, element, new SkipDefaultValuesSerializationFilters());
204   }
205 }