diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerBase.java b/java/java-analysis-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerBase.java index 9d23670141dd..28768a4e9aff 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerBase.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerBase.java @@ -31,13 +31,14 @@ import com.intellij.openapi.extensions.impl.ExtensionPointImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.JDOMExternalizableStringList; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.profile.codeInspection.InspectionProfileManager; -import com.intellij.psi.PsiDocCommentOwner; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.*; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; +import com.intellij.util.xmlb.SkipDefaultsSerializationFilter; +import com.intellij.util.xmlb.XmlSerializer; +import com.intellij.util.xmlb.annotations.*; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -50,6 +51,7 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen @NonNls private static final String[] STANDARD_ANNOS = { "javax.ws.rs.*", }; + private static final String PATTERN_SUFFIX = ".*"; // null means uninitialized private volatile List ADDITIONAL_ANNOS; @@ -72,6 +74,7 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen } public JDOMExternalizableStringList ADDITIONAL_ANNOTATIONS = new JDOMExternalizableStringList(); private final Map myPersistentEntryPoints; + private final List myPatterns = new ArrayList<>(); private final Set myTemporaryEntryPoints; private static final String VERSION = "2.0"; @NonNls private static final String VERSION_ATTR = "version"; @@ -135,6 +138,13 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen } catch (Throwable ignored) { } + + getPatterns().clear(); + for (Element pattern : element.getChildren("pattern")) { + final ClassPattern classPattern = new ClassPattern(); + XmlSerializer.deserializeInto(classPattern, pattern); + getPatterns().add(classPattern); + } } @Override @@ -142,6 +152,11 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen public Element getState() { Element element = new Element("state"); writeExternal(element, myPersistentEntryPoints, ADDITIONAL_ANNOTATIONS); + if (!getPatterns().isEmpty()) { + for (ClassPattern pattern : getPatterns()) { + element.addContent(XmlSerializer.serialize(pattern, new SkipDefaultsSerializationFilter())); + } + } return element; } @@ -177,6 +192,16 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen ((RefElementImpl)refElement).setPermanentEntry(entryPoint.isPersistent()); } } + + for (ClassPattern pattern : myPatterns) { + final RefEntity refClass = manager.getReference(RefJavaManager.CLASS, pattern.pattern); + if (refClass != null) { + for (RefMethod constructor : ((RefClass)refClass).getConstructors()) { + ((RefMethodImpl)constructor).setEntry(true); + ((RefMethodImpl)constructor).setPermanentEntry(true); + } + } + } }); } } @@ -192,6 +217,21 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen @Override public void addEntryPoint(@NotNull RefElement newEntryPoint, boolean isPersistent) { if (!newEntryPoint.isValid()) return; + if (isPersistent) { + if (newEntryPoint instanceof RefMethod && ((RefMethod)newEntryPoint).isConstructor() || newEntryPoint instanceof RefClass) { + final ClassPattern classPattern = new ClassPattern(); + classPattern.pattern = new SmartRefElementPointerImpl(newEntryPoint, true).getFQName(); + getPatterns().add(classPattern); + + final EntryPointsManager entryPointsManager = getInstance(newEntryPoint.getElement().getProject()); + if (this != entryPointsManager) { + entryPointsManager.addEntryPoint(newEntryPoint, true); + } + + return; + } + } + if (newEntryPoint instanceof RefClass) { RefClass refClass = (RefClass)newEntryPoint; @@ -248,8 +288,8 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen if (key != null) { myPersistentEntryPoints.remove(key); - ((RefElementImpl)anEntryPoint).setEntry(false); } + ((RefElementImpl)anEntryPoint).setEntry(false); if (anEntryPoint.isPermanentEntry() && anEntryPoint.isValid()) { final Project project = anEntryPoint.getElement().getProject(); @@ -258,6 +298,17 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen entryPointsManager.removeEntryPoint(anEntryPoint); } } + + if (anEntryPoint instanceof RefMethod && ((RefMethod)anEntryPoint).isConstructor() || anEntryPoint instanceof RefClass) { + final RefClass aClass = anEntryPoint instanceof RefClass ? (RefClass)anEntryPoint : ((RefMethod)anEntryPoint).getOwnerClass(); + final String qualifiedName = aClass.getQualifiedName(); + for (Iterator iterator = getPatterns().iterator(); iterator.hasNext(); ) { + if (Comparing.equal(iterator.next().pattern, qualifiedName)) { + //todo if inheritance or pattern? + iterator.remove(); + } + } + } } @NotNull @@ -321,6 +372,7 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen public void addAllPersistentEntries(EntryPointsManagerBase manager) { myPersistentEntryPoints.putAll(manager.myPersistentEntryPoints); + myPatterns.addAll(manager.getPatterns()); } public static void convert(Element element, final Map persistentEntryPoints) { @@ -380,7 +432,74 @@ public abstract class EntryPointsManagerBase extends EntryPointsManager implemen return true; } + if (element instanceof PsiClass) { + final String qualifiedName = ((PsiClass)element).getQualifiedName(); + if (qualifiedName != null) { + for (ClassPattern pattern : getPatterns()) { + if (isAcceptedByPattern((PsiClass)element, qualifiedName, pattern, new HashSet<>())) { + return true; + } + } + } + } + return AnnotationUtil.checkAnnotatedUsingPatterns(owner, ADDITIONAL_ANNOTATIONS) || AnnotationUtil.checkAnnotatedUsingPatterns(owner, getAdditionalAnnotations()); } + + private static boolean isAcceptedByPattern(@NotNull PsiClass element, String qualifiedName, ClassPattern pattern, Set visited) { + if (qualifiedName == null) { + return false; + } + + if (qualifiedName.equals(pattern.pattern)) { + return true; + } + + if (pattern.pattern.endsWith(PATTERN_SUFFIX) && qualifiedName.startsWith(StringUtil.trimEnd(pattern.pattern, PATTERN_SUFFIX))) { + return true; + } + + if (pattern.hierarchically) { + for (PsiClass superClass : element.getSupers()) { + final String superClassQualifiedName = superClass.getQualifiedName(); + if (visited.add(superClass) && isAcceptedByPattern(superClass, superClassQualifiedName, pattern, visited)) { + return true; + } + } + } + return false; + } + + public List getPatterns() { + return myPatterns; + } + + @Tag("pattern") + public static class ClassPattern { + @Attribute("value") + public String pattern; + @Attribute("hierarchically") + public boolean hierarchically = false; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClassPattern otherPattern = (ClassPattern)o; + + if (hierarchically != otherPattern.hierarchically) return false; + if (!pattern.equals(otherPattern.pattern)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = pattern.hashCode(); + result = 31 * result + (hierarchically ? 1 : 0); + return result; + } + } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/reference/RefJavaManagerImpl.java b/java/java-analysis-impl/src/com/intellij/codeInspection/reference/RefJavaManagerImpl.java index 73a591880951..2bdf06be8cc5 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/reference/RefJavaManagerImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/reference/RefJavaManagerImpl.java @@ -372,6 +372,9 @@ public class RefJavaManagerImpl extends RefJavaManager { } + @Override + public void configureEntryClassPatterns() {} + @Override public JButton createConfigureAnnotationsBtn() { return null; diff --git a/java/java-impl/src/com/intellij/codeInspection/deadCode/UnusedDeclarationInspection.java b/java/java-impl/src/com/intellij/codeInspection/deadCode/UnusedDeclarationInspection.java index 3cf9258166ce..1fba84acfba5 100644 --- a/java/java-impl/src/com/intellij/codeInspection/deadCode/UnusedDeclarationInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/deadCode/UnusedDeclarationInspection.java @@ -128,10 +128,13 @@ public class UnusedDeclarationInspection extends UnusedDeclarationInspectionBase gc.gridy++; add(myNonJavaCheckbox, gc); - JButton configureAnnotations = EntryPointsManagerImpl.createConfigureAnnotationsButton(); + final JButton configureClassPatternsButton = EntryPointsManagerImpl.createConfigureClassPatternsButton(); gc.fill = GridBagConstraints.NONE; gc.gridy++; - gc.insets.top = 10; + add(configureClassPatternsButton, gc); + + JButton configureAnnotations = EntryPointsManagerImpl.createConfigureAnnotationsButton(); + gc.gridy++; gc.weighty = 1; add(configureAnnotations, gc); diff --git a/java/java-impl/src/com/intellij/codeInspection/ex/ConfigureClassPatternsDialog.java b/java/java-impl/src/com/intellij/codeInspection/ex/ConfigureClassPatternsDialog.java new file mode 100644 index 000000000000..0499faf6ed0b --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/ex/ConfigureClassPatternsDialog.java @@ -0,0 +1,172 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.ex; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.ui.*; +import com.intellij.ui.table.JBTable; +import com.intellij.util.ui.ItemRemovable; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +class ConfigureClassPatternsDialog extends DialogWrapper { + + private final List myModifiedPatterns; + private final List myPatterns; + private final Project myProject; + public ConfigureClassPatternsDialog(List patterns, Project project) { + super(project); + myModifiedPatterns = new ArrayList<>(patterns); + myPatterns = patterns; + myProject = project; + init(); + setTitle("Configure Class Patterns"); + } + @Override + protected JComponent createCenterPanel() { + final JBTable table = createTableForPatterns(); + final ToolbarDecorator toolbarDecorator = ToolbarDecorator.createDecorator(table) + .setAddAction(new AnActionButtonRunnable() { + @Override + public void run(AnActionButton button) { + myModifiedPatterns.add(new EntryPointsManagerBase.ClassPattern()); + AbstractTableModel model = (AbstractTableModel)table.getModel(); + final int row = myModifiedPatterns.size() - 1; + model.fireTableRowsInserted(row, row); + table.setRowSelectionInterval(row, row); + table.editCellAt(row, 1); + } + }).setRemoveAction(new AnActionButtonRunnable() { + @Override + public void run(AnActionButton button) { + TableUtil.removeSelectedItems(table); + table.repaint(); + } + }) + .setRemoveActionUpdater(new AnActionButtonUpdater() { + @Override + public boolean isEnabled(AnActionEvent e) { + return table.getSelectedRow() >= 0; + } + }) + .setButtonComparator("Add", "Remove"); + JPanel panel = new JPanel(new BorderLayout()); + panel.add(SeparatorFactory.createSeparator("Mark class as reachable if name matches", null), BorderLayout.NORTH); + panel.add(toolbarDecorator.createPanel(), BorderLayout.CENTER); + return panel; + } + + @Override + protected void doOKAction() { + myPatterns.clear(); + myPatterns.addAll(myModifiedPatterns); + DaemonCodeAnalyzer.getInstance(myProject).restart(); + super.doOKAction(); + } + + private JBTable createTableForPatterns() { + TableModel dataModel = new MyTableModel(); + + final JBTable result = new JBTable(dataModel); + result.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableCellEditor editor = result.getDefaultEditor(String.class); + if (editor instanceof DefaultCellEditor) { + ((DefaultCellEditor)editor).setClickCountToStart(1); + } + + final TableColumn column = result.getTableHeader().getColumnModel().getColumn(0); + column.setResizable(false); + final int width = 15 + result.getTableHeader().getFontMetrics(result.getTableHeader().getFont()).stringWidth(result.getColumnName(0)); + column.setMaxWidth(width); + column.setMinWidth(width); + + return result; + } + + private class MyTableModel extends AbstractTableModel implements ItemRemovable { + private final String[] myNames; + + public MyTableModel() { + myNames = new String[] {"With Subclasses", "Class"}; + } + + public int getColumnCount() { + return 2; + } + + public int getRowCount() { + return myModifiedPatterns.size(); + } + + @Nullable + public Object getValueAt(int row, int col) { + if (row < 0) return null; + final EntryPointsManagerBase.ClassPattern classPattern = myModifiedPatterns.get(row); + if (classPattern == null) return null; + if (col == 0) { + return classPattern.hierarchically; + } + return classPattern.pattern; + } + + public String getColumnName(int column) { + return myNames[column]; + } + + public Class getColumnClass(int col) { + if (col == 0) { + return Boolean.class; + } + if (col == 1) { + return String.class; + } + throw new IllegalArgumentException(String.valueOf(col)); + } + + public boolean isCellEditable(int row, int col) { + return true; + } + + public void setValueAt(Object aValue, int row, int col) { + EntryPointsManagerBase.ClassPattern classPattern = myModifiedPatterns.get(row); + if (classPattern == null) return; + if (col == 0) { + classPattern.hierarchically = (boolean)aValue; + } + else { + classPattern.pattern = (String)aValue; + } + fireTableRowsUpdated(row, row); + } + + @Override + public void removeRow(int idx) { + myModifiedPatterns.remove(idx); + } + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerImpl.java b/java/java-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerImpl.java index f80a4ff7cdca..f7fde0b5b6ca 100644 --- a/java/java-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerImpl.java +++ b/java/java-impl/src/com/intellij/codeInspection/ex/EntryPointsManagerImpl.java @@ -70,6 +70,11 @@ public class EntryPointsManagerImpl extends EntryPointsManagerBase implements Pe }.show(); } + @Override + public void configureEntryClassPatterns() { + new ConfigureClassPatternsDialog(getPatterns(), myProject).show(); + } + @Override public JButton createConfigureAnnotationsBtn() { return createConfigureAnnotationsButton(); @@ -85,4 +90,15 @@ public class EntryPointsManagerImpl extends EntryPointsManagerBase implements Pe }); return configureAnnotations; } -} + + public static JButton createConfigureClassPatternsButton() { + final JButton configureAnnotations = new JButton("Configure class patterns..."); + configureAnnotations.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + getInstance(ProjectUtil.guessCurrentProject(configureAnnotations)).configureEntryClassPatterns(); + } + }); + return configureAnnotations; + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/UnusedDeclarationClassPatternsTest.kt b/java/java-tests/testSrc/com/intellij/codeInspection/UnusedDeclarationClassPatternsTest.kt new file mode 100644 index 000000000000..6acb31fb748c --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInspection/UnusedDeclarationClassPatternsTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection; + +import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection +import com.intellij.codeInspection.ex.EntryPointsManagerBase +import com.intellij.codeInspection.ex.InspectionManagerEx +import com.intellij.codeInspection.reference.RefClass +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase + +class UnusedDeclarationClassPatternsTest : LightCodeInsightFixtureTestCase() { + + fun testClassPattern() { + val unusedDeclarationInspection = UnusedDeclarationInspection(true) + myFixture.enableInspections(unusedDeclarationInspection) + val classPattern = EntryPointsManagerBase.ClassPattern() + classPattern.hierarchically = true; + classPattern.pattern = "java.lang.Runnable" + val patterns = EntryPointsManagerBase.getInstance(project).patterns + try { + patterns.add(classPattern) + myFixture.configureByText("C.java", "public abstract class C implements Runnable {}") + myFixture.checkHighlighting() + } + finally { + patterns.remove(classPattern) + myFixture.disableInspections(unusedDeclarationInspection) + } + } + + fun testNoClassPattern() { + val unusedDeclarationInspection = UnusedDeclarationInspection(true) + try { + myFixture.enableInspections(unusedDeclarationInspection) + myFixture.configureByText("C.java", "public abstract class C implements Runnable {}") + myFixture.checkHighlighting() + } + finally { + myFixture.disableInspections(unusedDeclarationInspection) + } + } + + fun testAddEntryPoint() { + val aClass = myFixture.addClass("public class Foo {}") + val entryPointsManager = EntryPointsManagerBase.getInstance(project) + val context = (InspectionManager.getInstance(project) as InspectionManagerEx).createNewGlobalContext(false) + try { + val refClass = context.refManager.getReference(aClass) + assertNotNull(refClass) + val patterns = entryPointsManager.patterns + assertEmpty(patterns) + + //add class as entry point + entryPointsManager.addEntryPoint(refClass!!, true) + assertSize(1, patterns) + assertEquals("Foo", patterns[0].pattern) + assertEmpty(entryPointsManager.entryPoints) + + //remove class entry point with constructors - ensure nothing is left in the entries + entryPointsManager.removeEntryPoint(refClass) + for (constructor in (refClass as RefClass).constructors) { + entryPointsManager.removeEntryPoint(constructor) + } + + assertEmpty(patterns) + assertEmpty(entryPointsManager.entryPoints) + } + finally { + context.cleanup() + } + } +} \ No newline at end of file diff --git a/platform/analysis-api/src/com/intellij/codeInspection/ex/EntryPointsManager.java b/platform/analysis-api/src/com/intellij/codeInspection/ex/EntryPointsManager.java index 02f3f3cfac1a..e835257a9ed8 100644 --- a/platform/analysis-api/src/com/intellij/codeInspection/ex/EntryPointsManager.java +++ b/platform/analysis-api/src/com/intellij/codeInspection/ex/EntryPointsManager.java @@ -52,6 +52,8 @@ public abstract class EntryPointsManager implements Disposable { public abstract void configureAnnotations(); + public abstract void configureEntryClassPatterns(); + /** * {@link com.intellij.codeInspection.ex.EntryPointsManagerImpl#createConfigureAnnotationsButton()} should be used instead */