IDEA-144571 (False positive inspection "Abstract class may be interface" on non-stati...
[idea/community.git] / plugins / InspectionGadgets / InspectionGadgetsAnalysis / src / com / siyeh / ig / classlayout / ClassMayBeInterfaceInspection.java
1 /*
2  * Copyright 2003-2015 Dave Griffith, Bas Leijdekkers
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.siyeh.ig.classlayout;
17
18 import com.intellij.codeInsight.FileModificationService;
19 import com.intellij.codeInspection.ProblemDescriptor;
20 import com.intellij.codeInspection.ui.SingleCheckboxOptionsPanel;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.util.WriteExternalException;
23 import com.intellij.psi.*;
24 import com.intellij.psi.search.SearchScope;
25 import com.intellij.psi.search.searches.ClassInheritorsSearch;
26 import com.intellij.psi.util.PsiTreeUtil;
27 import com.intellij.psi.util.PsiUtil;
28 import com.siyeh.InspectionGadgetsBundle;
29 import com.siyeh.ig.BaseInspection;
30 import com.siyeh.ig.BaseInspectionVisitor;
31 import com.siyeh.ig.InspectionGadgetsFix;
32 import com.siyeh.ig.memory.InnerClassReferenceVisitor;
33 import com.siyeh.ig.psiutils.ClassUtils;
34 import org.jdom.Element;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.swing.*;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 public class ClassMayBeInterfaceInspection extends BaseInspection {
43
44   @SuppressWarnings("PublicField")
45   public boolean reportClassesWithNonAbstractMethods = false;
46
47   @Override
48   @NotNull
49   public String getDisplayName() {
50     return InspectionGadgetsBundle.message("class.may.be.interface.display.name");
51   }
52
53   @Override
54   @NotNull
55   protected String buildErrorString(Object... infos) {
56     return InspectionGadgetsBundle.message("class.may.be.interface.problem.descriptor");
57   }
58
59   @Nullable
60   @Override
61   public JComponent createOptionsPanel() {
62     return new SingleCheckboxOptionsPanel(InspectionGadgetsBundle.message("class.may.be.interface.java8.option"), this,
63                                           "reportClassesWithNonAbstractMethods");
64   }
65
66   @Override
67   public void writeSettings(@NotNull Element node) throws WriteExternalException {
68     if (reportClassesWithNonAbstractMethods) {
69       node.addContent(new Element("option").setAttribute("name", "reportClassesWithNonAbstractMethods").setAttribute("value", "true"));
70     }
71   }
72
73   @Override
74   protected InspectionGadgetsFix buildFix(Object... infos) {
75     return new ClassMayBeInterfaceFix();
76   }
77
78   private static class ClassMayBeInterfaceFix extends InspectionGadgetsFix {
79
80     @Override
81     @NotNull
82     public String getName() {
83       return InspectionGadgetsBundle.message("class.may.be.interface.convert.quickfix");
84     }
85     @Override
86     @NotNull
87     public String getFamilyName() {
88       return getName();
89     }
90
91     @Override
92     protected boolean prepareForWriting() {
93       return false;
94     }
95
96     @Override
97     public void doFix(Project project, ProblemDescriptor descriptor) {
98       final PsiIdentifier classNameIdentifier = (PsiIdentifier)descriptor.getPsiElement();
99       final PsiClass interfaceClass = (PsiClass)classNameIdentifier.getParent();
100       final SearchScope searchScope = interfaceClass.getUseScope();
101       final List<PsiClass> elements = new ArrayList();
102       elements.add(interfaceClass);
103       for (final PsiClass inheritor : ClassInheritorsSearch.search(interfaceClass, searchScope, false)) {
104         elements.add(inheritor);
105       }
106       if (!FileModificationService.getInstance().preparePsiElementsForWrite(elements)) {
107         return;
108       }
109       moveSubClassExtendsToImplements(elements);
110       changeClassToInterface(interfaceClass);
111       moveImplementsToExtends(interfaceClass);
112     }
113
114     private static void changeClassToInterface(PsiClass aClass) {
115       for (PsiMethod method : aClass.getMethods()) {
116         PsiUtil.setModifierProperty(method, PsiModifier.PUBLIC, false);
117         if (method.hasModifierProperty(PsiModifier.STATIC) || method.hasModifierProperty(PsiModifier.ABSTRACT)) {
118           continue;
119         }
120         PsiUtil.setModifierProperty(method, PsiModifier.DEFAULT, true);
121       }
122       for (PsiField field : aClass.getFields()) {
123         PsiUtil.setModifierProperty(field, PsiModifier.PUBLIC, false);
124         PsiUtil.setModifierProperty(field, PsiModifier.STATIC, false);
125         PsiUtil.setModifierProperty(field, PsiModifier.FINAL, false);
126       }
127       for (PsiClass innerClass : aClass.getInnerClasses()) {
128         PsiUtil.setModifierProperty(innerClass, PsiModifier.PUBLIC, false);
129       }
130       final PsiIdentifier nameIdentifier = aClass.getNameIdentifier();
131       if (nameIdentifier == null) {
132         return;
133       }
134       final PsiKeyword classKeyword = PsiTreeUtil.getPrevSiblingOfType(nameIdentifier, PsiKeyword.class);
135       final PsiManager manager = aClass.getManager();
136       final PsiElementFactory factory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
137       final PsiKeyword interfaceKeyword = factory.createKeyword(PsiKeyword.INTERFACE);
138       if (classKeyword == null) {
139         return;
140       }
141       PsiUtil.setModifierProperty(aClass, PsiModifier.ABSTRACT, false);
142       PsiUtil.setModifierProperty(aClass, PsiModifier.FINAL, false);
143       classKeyword.replace(interfaceKeyword);
144     }
145
146     private static void moveImplementsToExtends(PsiClass anInterface) {
147       final PsiReferenceList extendsList = anInterface.getExtendsList();
148       if (extendsList == null) {
149         return;
150       }
151       final PsiReferenceList implementsList = anInterface.getImplementsList();
152       if (implementsList == null) {
153         return;
154       }
155       final PsiJavaCodeReferenceElement[] referenceElements = implementsList.getReferenceElements();
156       for (final PsiJavaCodeReferenceElement referenceElement : referenceElements) {
157         extendsList.add(referenceElement);
158         referenceElement.delete();
159       }
160     }
161
162     private static void moveSubClassExtendsToImplements(List<PsiClass> inheritors) {
163       final PsiClass oldClass = inheritors.get(0);
164       final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(oldClass.getProject()).getElementFactory();
165       final PsiJavaCodeReferenceElement classReference = elementFactory.createClassReferenceElement(oldClass);
166       for (int i = 1; i < inheritors.size(); i++) {
167         final PsiClass inheritor = inheritors.get(i);
168         final PsiReferenceList extendsList = inheritor.getExtendsList();
169         if (extendsList == null) {
170           continue;
171         }
172         final PsiReferenceList implementsList = inheritor.getImplementsList();
173         moveReference(extendsList, implementsList, classReference);
174       }
175     }
176
177     private static void moveReference(@NotNull PsiReferenceList source, @Nullable PsiReferenceList target,
178                                       @NotNull PsiJavaCodeReferenceElement reference) {
179       final PsiJavaCodeReferenceElement[] sourceReferences = source.getReferenceElements();
180       final String fqName = reference.getQualifiedName();
181       for (final PsiJavaCodeReferenceElement sourceReference : sourceReferences) {
182         final String implementsReferenceFqName = sourceReference.getQualifiedName();
183         if (fqName.equals(implementsReferenceFqName)) {
184           if (target != null) {
185             target.add(sourceReference);
186           }
187           sourceReference.delete();
188         }
189       }
190     }
191   }
192
193   @Override
194   public BaseInspectionVisitor buildVisitor() {
195     return new ClassMayBeInterfaceVisitor();
196   }
197
198   private class ClassMayBeInterfaceVisitor extends BaseInspectionVisitor {
199
200     @Override
201     public void visitClass(@NotNull PsiClass aClass) {
202       // no call to super, so that it doesn't drill down to inner classes
203       if (aClass.isInterface() || aClass.isAnnotationType() || aClass.isEnum()) {
204         return;
205       }
206       if (aClass instanceof PsiTypeParameter || aClass instanceof PsiAnonymousClass) {
207         return;
208       }
209       if (!aClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
210         return;
211       }
212       if (!mayBeInterface(aClass)) {
213         return;
214       }
215       if (ClassUtils.isInnerClass(aClass)) {
216         final InnerClassReferenceVisitor visitor = new InnerClassReferenceVisitor(aClass);
217         aClass.accept(visitor);
218         if (!visitor.canInnerClassBeStatic()) {
219           return;
220         }
221       }
222       registerClassError(aClass);
223     }
224
225     public boolean mayBeInterface(PsiClass aClass) {
226       final PsiReferenceList extendsList = aClass.getExtendsList();
227       if (extendsList != null) {
228         final PsiJavaCodeReferenceElement[] extendsElements = extendsList.getReferenceElements();
229         if (extendsElements.length > 0) {
230           return false;
231         }
232       }
233       final PsiClassInitializer[] initializers = aClass.getInitializers();
234       if (initializers.length > 0) {
235         return false;
236       }
237       return allMethodsPublicAbstract(aClass) && allFieldsPublicStaticFinal(aClass) && allInnerClassesPublic(aClass);
238     }
239
240     private boolean allFieldsPublicStaticFinal(PsiClass aClass) {
241       boolean allFieldsStaticFinal = true;
242       final PsiField[] fields = aClass.getFields();
243       for (final PsiField field : fields) {
244         if (!(field.hasModifierProperty(PsiModifier.STATIC) && field.hasModifierProperty(PsiModifier.FINAL)
245               && field.hasModifierProperty(PsiModifier.PUBLIC))) {
246           allFieldsStaticFinal = false;
247         }
248       }
249       return allFieldsStaticFinal;
250     }
251
252     private boolean allMethodsPublicAbstract(PsiClass aClass) {
253       final PsiMethod[] methods = aClass.getMethods();
254       for (final PsiMethod method : methods) {
255         if (!method.hasModifierProperty(PsiModifier.ABSTRACT) &&
256             (!reportClassesWithNonAbstractMethods || !PsiUtil.isLanguageLevel8OrHigher(aClass))) {
257           return false;
258         }
259         else if (!method.hasModifierProperty(PsiModifier.PUBLIC) || method.hasModifierProperty(PsiModifier.FINAL)) {
260           return false;
261         }
262       }
263       return true;
264     }
265
266     private boolean allInnerClassesPublic(PsiClass aClass) {
267       final PsiClass[] innerClasses = aClass.getInnerClasses();
268       for (PsiClass innerClass : innerClasses) {
269         if (!innerClass.hasModifierProperty(PsiModifier.PUBLIC)) {
270           return false;
271         }
272       }
273       return true;
274     }
275   }
276 }