[java] module dependency quick fixes on export qualifiers
[idea/community.git] / java / java-analysis-impl / src / com / intellij / codeInsight / daemon / impl / analysis / ModuleHighlightUtil.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.codeInsight.daemon.impl.analysis;
17
18 import com.intellij.codeInsight.daemon.JavaErrorMessages;
19 import com.intellij.codeInsight.daemon.QuickFixBundle;
20 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
21 import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
22 import com.intellij.codeInsight.daemon.impl.quickfix.*;
23 import com.intellij.codeInsight.intention.QuickFixFactory;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.module.Module;
26 import com.intellij.openapi.module.ModuleUtilCore;
27 import com.intellij.openapi.module.impl.scopes.ModulesScope;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.roots.ProjectFileIndex;
30 import com.intellij.openapi.util.TextRange;
31 import com.intellij.openapi.util.text.StringUtil;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.psi.*;
34 import com.intellij.psi.search.FilenameIndex;
35 import com.intellij.psi.util.InheritanceUtil;
36 import com.intellij.psi.util.PsiTreeUtil;
37 import com.intellij.psi.util.PsiUtil;
38 import com.intellij.util.ObjectUtils;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.containers.JBIterable;
41 import com.intellij.util.graph.Graph;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44 import org.jetbrains.annotations.PropertyKey;
45
46 import java.util.Collection;
47 import java.util.List;
48 import java.util.Optional;
49 import java.util.Set;
50 import java.util.function.Function;
51 import java.util.stream.Collectors;
52 import java.util.stream.Stream;
53
54 import static com.intellij.openapi.util.Pair.pair;
55 import static com.intellij.psi.PsiJavaModule.MODULE_INFO_FILE;
56 import static com.intellij.psi.SyntaxTraverser.psiTraverser;
57
58 public class ModuleHighlightUtil {
59   @Nullable
60   static HighlightInfo checkFileName(@NotNull PsiJavaModule element, @NotNull PsiFile file) {
61     if (!MODULE_INFO_FILE.equals(file.getName())) {
62       String message = JavaErrorMessages.message("module.file.wrong.name");
63       HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(element)).description(message).create();
64       QuickFixAction.registerQuickFixAction(info, factory().createRenameFileFix(MODULE_INFO_FILE));
65       return info;
66     }
67
68     return null;
69   }
70
71   @Nullable
72   static HighlightInfo checkFileDuplicates(@NotNull PsiJavaModule element, @NotNull PsiFile file) {
73     Module module = ModuleUtilCore.findModuleForPsiElement(element);
74     if (module != null) {
75       Project project = file.getProject();
76       Collection<VirtualFile> others = FilenameIndex.getVirtualFilesByName(project, MODULE_INFO_FILE, new ModulesScope(module));
77       if (others.size() > 1) {
78         String message = JavaErrorMessages.message("module.file.duplicate");
79         HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(element)).description(message).create();
80         others.stream().map(f -> PsiManager.getInstance(project).findFile(f)).filter(f -> f != file).findFirst().ifPresent(
81           duplicate -> QuickFixAction.registerQuickFixAction(info, new GoToSymbolFix(duplicate, JavaErrorMessages.message("module.open.duplicate.text")))
82         );
83         return info;
84       }
85     }
86
87     return null;
88   }
89
90   @NotNull
91   static List<HighlightInfo> checkDuplicateStatements(@NotNull PsiJavaModule module) {
92     List<HighlightInfo> results = ContainerUtil.newSmartList();
93
94     checkDuplicateRefs(
95       psiTraverser().children(module).filter(PsiRequiresStatement.class),
96       st -> Optional.ofNullable(st.getReferenceElement()).map(PsiJavaModuleReferenceElement::getReferenceText),
97       "module.duplicate.requires", results);
98
99     checkDuplicateRefs(
100       psiTraverser().children(module).filter(PsiExportsStatement.class),
101       st -> Optional.ofNullable(st.getPackageReference()).map(ModuleHighlightUtil::refText),
102       "module.duplicate.export", results);
103
104     checkDuplicateRefs(
105       psiTraverser().children(module).filter(PsiUsesStatement.class),
106       st -> Optional.ofNullable(st.getClassReference()).map(ModuleHighlightUtil::refText),
107       "module.duplicate.uses", results);
108
109     checkDuplicateRefs(
110       psiTraverser().children(module).filter(PsiProvidesStatement.class),
111       st -> Optional.of(pair(st.getInterfaceReference(), st.getImplementationReference()))
112         .map(p -> p.first != null && p.second != null ? refText(p.first) + " / " + refText(p.second) : null),
113       "module.duplicate.provides", results);
114
115     return results;
116   }
117
118   @NotNull
119   static List<HighlightInfo> checkUnusedServices(@NotNull PsiJavaModule module) {
120     List<HighlightInfo> results = ContainerUtil.newSmartList();
121
122     Set<String> exports = ContainerUtil.newTroveSet(), uses = ContainerUtil.newTroveSet();
123     for (PsiElement child : psiTraverser().children(module)) {
124       if (child instanceof PsiExportsStatement) {
125         PsiJavaCodeReferenceElement ref = ((PsiExportsStatement)child).getPackageReference();
126         if (ref != null) exports.add(refText(ref));
127       }
128       else if (child instanceof PsiUsesStatement) {
129         PsiJavaCodeReferenceElement ref = ((PsiUsesStatement)child).getClassReference();
130         if (ref != null) uses.add(refText(ref));
131       }
132     }
133
134     Module host = ModuleUtilCore.findModuleForPsiElement(module);
135     for (PsiProvidesStatement statement : psiTraverser().children(module).filter(PsiProvidesStatement.class)) {
136       PsiJavaCodeReferenceElement ref = statement.getInterfaceReference();
137       if (ref != null) {
138         PsiElement target = ref.resolve();
139         if (target instanceof PsiClass && ModuleUtilCore.findModuleForPsiElement(target) == host) {
140           String className = refText(ref), packageName = StringUtil.getPackageName(className);
141           if (!exports.contains(packageName) && !uses.contains(className)) {
142             String message = JavaErrorMessages.message("module.service.unused");
143             results.add(HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING).range(range(ref)).description(message).create());
144           }
145         }
146       }
147     }
148
149     return results;
150   }
151
152   private static <T extends PsiElement> void checkDuplicateRefs(Iterable<T> statements,
153                                                                 Function<T, Optional<String>> ref,
154                                                                 @PropertyKey(resourceBundle = JavaErrorMessages.BUNDLE) String key,
155                                                                 List<HighlightInfo> results) {
156     Set<String> filter = ContainerUtil.newTroveSet();
157     for (T statement : statements) {
158       String refText = ref.apply(statement).orElse(null);
159       if (refText != null && !filter.add(refText)) {
160         String message = JavaErrorMessages.message(key, refText);
161         HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(statement).description(message).create();
162         QuickFixAction.registerQuickFixAction(info, new DeleteElementFix(statement));
163         results.add(info);
164       }
165     }
166   }
167
168   private static String refText(PsiJavaCodeReferenceElement ref) {
169     return PsiNameHelper.getQualifiedClassName(ref.getText(), true);
170   }
171
172   @Nullable
173   static HighlightInfo checkFileLocation(@NotNull PsiJavaModule element, @NotNull PsiFile file) {
174     VirtualFile vFile = file.getVirtualFile();
175     if (vFile != null) {
176       VirtualFile root = ProjectFileIndex.SERVICE.getInstance(file.getProject()).getSourceRootForFile(vFile);
177       if (root != null && !root.equals(vFile.getParent())) {
178         String message = JavaErrorMessages.message("module.file.wrong.location");
179         HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING).range(range(element)).description(message).create();
180         QuickFixAction.registerQuickFixAction(info, new MoveFileFix(vFile, root, QuickFixBundle.message("move.file.to.source.root.text")));
181         return info;
182       }
183     }
184
185     return null;
186   }
187
188   @Nullable
189   static HighlightInfo checkModuleReference(@Nullable PsiJavaModuleReferenceElement refElement, @NotNull PsiJavaModule container) {
190     if (refElement != null) {
191       PsiPolyVariantReference ref = refElement.getReference();
192       assert ref != null : refElement.getParent();
193       PsiElement target = ref.resolve();
194       if (!(target instanceof PsiJavaModule)) {
195         return moduleResolveError(refElement, ref);
196       }
197       else if (target == container) {
198         String message = JavaErrorMessages.message("module.cyclic.dependence", container.getModuleName());
199         return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create();
200       }
201       else {
202         Graph<PsiJavaModule> graph = JavaModuleGraphBuilder.getOrBuild(target.getProject());
203         if (graph != null) {
204           Collection<PsiJavaModule> cycle = JavaModuleGraphBuilder.findCycle(graph, (PsiJavaModule)target);
205           if (cycle != null && cycle.contains(container)) {
206             Stream<String> stream = cycle.stream().map(PsiJavaModule::getModuleName);
207             if (ApplicationManager.getApplication().isUnitTestMode()) stream = stream.sorted();
208             String message = JavaErrorMessages.message("module.cyclic.dependence", stream.collect(Collectors.joining(", ")));
209             return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create();
210           }
211         }
212       }
213     }
214
215     return null;
216   }
217
218   @Nullable
219   static HighlightInfo checkPackageReference(@Nullable PsiJavaCodeReferenceElement refElement) {
220     if (refElement != null) {
221       PsiElement target = refElement.resolve();
222       if (target instanceof PsiPackage) {
223         Module module = ModuleUtilCore.findModuleForPsiElement(refElement);
224         if (module != null) {
225           String packageName = ((PsiPackage)target).getQualifiedName();
226           PsiDirectory[] directories = ((PsiPackage)target).getDirectories(new ModulesScope(module));
227           if (directories.length == 0) {
228             String message = JavaErrorMessages.message("package.not.found", packageName);
229             return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create();
230           }
231           if (PsiUtil.isPackageEmpty(directories, packageName)) {
232             String message = JavaErrorMessages.message("package.is.empty", packageName);
233             return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create();
234           }
235         }
236       }
237     }
238
239     return null;
240   }
241
242   @NotNull
243   static List<HighlightInfo> checkExportTargets(@NotNull PsiExportsStatement statement, @NotNull PsiJavaModule container) {
244     List<HighlightInfo> results = ContainerUtil.newSmartList();
245
246     Set<String> targets = ContainerUtil.newTroveSet();
247     for (PsiJavaModuleReferenceElement refElement : psiTraverser().children(statement).filter(PsiJavaModuleReferenceElement.class)) {
248       String refText = refElement.getReferenceText();
249       PsiPolyVariantReference ref = refElement.getReference();
250       assert ref != null : statement;
251       PsiElement target = ref.resolve();
252       if (!(target instanceof PsiJavaModule)) {
253         results.add(moduleResolveError(refElement, ref));
254       }
255       else if (!targets.add(refText)) {
256         String message = JavaErrorMessages.message("module.duplicate.export", refText);
257         results.add(HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create());
258       }
259       else if (target == container) {
260         String message = JavaErrorMessages.message("module.self.export");
261         results.add(HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING).range(refElement).description(message).create());
262       }
263     }
264
265     return results;
266   }
267
268   @Nullable
269   static HighlightInfo checkServiceReference(@Nullable PsiJavaCodeReferenceElement refElement) {
270     if (refElement != null) {
271       PsiElement target = refElement.resolve();
272       if (target instanceof PsiClass && ((PsiClass)target).isEnum()) {
273         String message = JavaErrorMessages.message("module.service.enum", ((PsiClass)target).getName());
274         return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(refElement)).description(message).create();
275       }
276     }
277
278     return null;
279   }
280
281   @Nullable
282   static HighlightInfo checkServiceImplementation(@Nullable PsiJavaCodeReferenceElement implRef,
283                                                   @Nullable PsiJavaCodeReferenceElement intRef) {
284     if (implRef != null && intRef != null) {
285       PsiElement implTarget = implRef.resolve(), intTarget = intRef.resolve();
286       if (implTarget instanceof PsiClass && intTarget instanceof PsiClass) {
287         PsiClass implClass = (PsiClass)implTarget;
288         if (!InheritanceUtil.isInheritorOrSelf(implClass, (PsiClass)intTarget, true)) {
289           String message = JavaErrorMessages.message("module.service.subtype");
290           return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(implRef)).description(message).create();
291         }
292         if (implClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
293           String message = JavaErrorMessages.message("module.service.abstract", implClass.getName());
294           return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(implRef)).description(message).create();
295         }
296
297         PsiMethod[] constructors = implClass.getConstructors();
298         if (constructors.length > 0) {
299           PsiMethod constructor = JBIterable.of(constructors).find(c -> c.getParameterList().getParametersCount() == 0);
300           if (constructor == null) {
301             String message = JavaErrorMessages.message("module.service.no.ctor", implClass.getName());
302             return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(implRef)).description(message).create();
303           }
304           if (!constructor.hasModifierProperty(PsiModifier.PUBLIC)) {
305             String message = JavaErrorMessages.message("module.service.hidden.ctor", implClass.getName());
306             return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(range(implRef)).description(message).create();
307           }
308         }
309       }
310     }
311
312     return null;
313   }
314
315   private static HighlightInfo moduleResolveError(PsiJavaModuleReferenceElement refElement, PsiPolyVariantReference ref) {
316     boolean missing = ref.multiResolve(true).length == 0;
317     String message = JavaErrorMessages.message(missing ? "module.not.found" : "module.not.on.path", refElement.getReferenceText());
318     HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(refElement).description(message).create();
319     if (!missing) {
320       factory().registerOrderEntryFixes(new QuickFixActionRegistrarImpl(info), ref);
321     }
322     return info;
323   }
324
325   private static QuickFixFactory factory() {
326     return QuickFixFactory.getInstance();
327   }
328
329   private static TextRange range(PsiJavaModule module) {
330     PsiKeyword kw = PsiTreeUtil.getChildOfType(module, PsiKeyword.class);
331     return new TextRange(kw != null ? kw.getTextOffset() : module.getTextOffset(), module.getNameElement().getTextRange().getEndOffset());
332   }
333
334   private static PsiElement range(PsiJavaCodeReferenceElement refElement) {
335     return ObjectUtils.notNull(refElement.getReferenceNameElement(), refElement);
336   }
337 }