IDEA-200832 Deprecation inspection: recognise externally deprecated APIs.

Extend API to support repeatable annotations and annotations from different external roots.
This commit is contained in:
Sergey Patrikeev
2018-10-24 13:10:31 +03:00
parent f6ae7924aa
commit 72607096d0
9 changed files with 232 additions and 72 deletions
@@ -2,6 +2,7 @@
package com.intellij.codeInspection.deprecation;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.ExternalAnnotationsManager;
import com.intellij.codeInsight.daemon.JavaErrorMessages;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightMessageUtil;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
@@ -26,6 +27,7 @@ import com.intellij.psi.javadoc.PsiDocTag;
import com.intellij.psi.util.*;
import com.intellij.refactoring.util.RefactoringChangeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.MoreCollectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -145,7 +147,7 @@ abstract class DeprecationInspectionBase extends AbstractBaseJavaLocalInspection
final PsiClass containingClass = method.getContainingClass();
assert containingClass != null;
final PsiClass superClass = containingClass.getSuperClass();
if (hasDefaultDeprecatedConstructor(superClass, myForRemoval)) {
if (superClass != null && hasDefaultDeprecatedConstructor(superClass, myForRemoval)) {
if (superClass instanceof PsiAnonymousClass) {
final PsiExpressionList argumentList = ((PsiAnonymousClass)superClass).getArgumentList();
if (argumentList != null && !argumentList.isEmpty()) return;
@@ -174,7 +176,7 @@ abstract class DeprecationInspectionBase extends AbstractBaseJavaLocalInspection
final PsiMethod[] currentConstructors = aClass.getConstructors();
if (currentConstructors.length == 0) {
final PsiClass superClass = aClass.getSuperClass();
if (hasDefaultDeprecatedConstructor(superClass, myForRemoval)) {
if (superClass != null && hasDefaultDeprecatedConstructor(superClass, myForRemoval)) {
final boolean isAnonymous = aClass instanceof PsiAnonymousClass;
if (isAnonymous) {
final PsiExpressionList argumentList = ((PsiAnonymousClass)aClass).getArgumentList();
@@ -225,8 +227,27 @@ abstract class DeprecationInspectionBase extends AbstractBaseJavaLocalInspection
}
}
private static boolean hasDefaultDeprecatedConstructor(PsiClass superClass, boolean forRemoval) {
return superClass != null && Arrays.stream(superClass.getConstructors())
private static boolean hasDefaultDeprecatedConstructor(@NotNull PsiClass superClass, boolean forRemoval) {
PsiMethod[] constructors = superClass.getConstructors();
if (constructors.length == 0) {
/*
The default constructor of a class can be externally annotated (IDEA-200832).
There cannot be inferred annotations for a default constructor,
so here is no need to check all annotations returned
by `AnnotationUtil.findAnnotations()`, but only external ones.
Note that there may be multiple external annotations roots,
so we check them all.
*/
List<PsiAnnotation> externalDeprecated = ExternalAnnotationsManager
.getInstance(superClass.getProject())
.findDefaultConstructorExternalAnnotations(superClass, CommonClassNames.JAVA_LANG_DEPRECATED);
return externalDeprecated != null
&& !externalDeprecated.isEmpty()
&& ContainerUtil.exists(externalDeprecated, annotation -> isMarkedForRemoval(annotation) == forRemoval);
}
return Arrays.stream(constructors)
.anyMatch(constructor -> constructor.getParameterList().isEmpty() &&
constructor.isDeprecated() &&
isMarkedForRemoval(constructor, forRemoval));
@@ -306,7 +327,11 @@ abstract class DeprecationInspectionBase extends AbstractBaseJavaLocalInspection
}
private static boolean isMarkedForRemoval(PsiModifierListOwner element, boolean forRemoval) {
return isMarkedForRemoval(element) == forRemoval;
PsiAnnotation annotation = AnnotationUtil.findAnnotation(element, CommonClassNames.JAVA_LANG_DEPRECATED);
if (annotation == null) {
return !forRemoval;
}
return isMarkedForRemoval(annotation) == forRemoval;
}
private static boolean isInSameOutermostClass(PsiElement refElement, PsiElement elementToHighlight, boolean ignoreInSameOutermostClass) {
@@ -324,12 +349,15 @@ abstract class DeprecationInspectionBase extends AbstractBaseJavaLocalInspection
return maybeClass instanceof PsiClass ? (PsiClass)maybeClass : null;
}
private static boolean isMarkedForRemoval(@Nullable PsiModifierListOwner element) {
PsiAnnotation annotation = AnnotationUtil.findAnnotation(element, CommonClassNames.JAVA_LANG_DEPRECATED);
if (annotation == null) {
return false;
}
PsiAnnotationMemberValue value = annotation.findAttributeValue("forRemoval");
/**
* Returns value of {@link Deprecated#forRemoval} attribute, which is available since Java 9.
*
* @param deprecatedAnnotation annotation instance to extract value of
* @return {@code true} if the {@code forRemoval} attribute is set to true,
* {@code false} if it isn't set or is set to {@code false}.
*/
private static boolean isMarkedForRemoval(@NotNull PsiAnnotation deprecatedAnnotation) {
PsiAnnotationMemberValue value = deprecatedAnnotation.findAttributeValue("forRemoval");
Object result = null;
if (value instanceof PsiLiteral) {
result = ((PsiLiteral)value).getValue();
@@ -182,7 +182,7 @@ public class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsM
notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
return false;
}
String externalName = getExternalName(listOwner, false);
String externalName = getExternalName(listOwner);
WriteCommandAction.writeCommandAction(project).run(() -> {
appendChosenAnnotationsRoot(entry, newRoot);
XmlFile xmlFileInRoot = findXmlFileInRoot(findExternalAnnotationsXmlFiles(listOwner), newRoot);
@@ -279,7 +279,7 @@ public class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsM
}
Set<PsiFile> annotationFiles = xmlFiles == null ? new THashSet<>() : new THashSet<>(xmlFiles);
String externalName = getExternalName(listOwner, false);
String externalName = getExternalName(listOwner);
WriteCommandAction.writeCommandAction(project).run(() -> {
if (existingXml != null) {
annotateExternally(listOwner, annotationFQName, existingXml, fromFile, value, externalName);
@@ -355,7 +355,7 @@ public class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsM
.runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
try {
tag.setAttribute("name", StringUtil.escapeXml(getExternalName(element, false)));
tag.setAttribute("name", StringUtil.escapeXml(getExternalName(element)));
commitChanges(file);
}
catch (IncorrectOperationException e) {
@@ -410,7 +410,7 @@ public class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsM
if (rootTag == null) {
continue;
}
final String externalName = getExternalName(listOwner, false);
final String externalName = getExternalName(listOwner);
final List<XmlTag> tagsToProcess = new ArrayList<>();
for (XmlTag tag : rootTag.getSubTags()) {
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2015 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.
*/
// 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.codeInsight;
import com.intellij.openapi.components.ServiceManager;
@@ -50,12 +36,64 @@ public abstract class ExternalAnnotationsManager {
@Nullable
public abstract PsiAnnotation findExternalAnnotation(@NotNull PsiModifierListOwner listOwner, @NotNull String annotationFQN);
/**
* Returns external annotations with fully qualified name of {@code annotationFQN}
* associated with {@code listOwner}.
*
* Multiple results may be returned for repeatable annotations and annotations
* from several external annotations roots.
*
* @param listOwner API element to return external annotations of
* @param annotationFQN fully qualified name of the annotation to search for
* @return external annotations of the {@code listOwner}
*/
@NotNull
public abstract List<PsiAnnotation> findExternalAnnotations(@NotNull PsiModifierListOwner listOwner, @NotNull String annotationFQN);
// Method used in Kotlin plugin
public abstract boolean isExternalAnnotationWritable(@NotNull PsiModifierListOwner listOwner, @NotNull String annotationFQN);
@Nullable
public abstract PsiAnnotation[] findExternalAnnotations(@NotNull PsiModifierListOwner listOwner);
/**
* Returns external annotations associated with default
* constructor of the {@code aClass}, if the constructor exists.
* <p>
* Default constructors should be handled specially
* because they don't have {@code PsiModifierListOwner},
* nor they are returned in {@link PsiClass#getConstructors()}.
* <p>
* Yet default constructors may be externally annotated
* in corresponding {@code annotations.xml}:
* <pre>{@code <item name='com.example.Foo Foo()'>
* <annotation name='org.some.Annotation'/>
* </item>}</pre>
*
* @param aClass class of which default constructor's external annotations are to be found
* @return external annotations of the default constructor of {@code aClass} or {@code null}
* if the class doesn't have a default constructor
*/
@Nullable
public abstract List<PsiAnnotation> findDefaultConstructorExternalAnnotations(@NotNull PsiClass aClass);
/**
* Returns external annotations with fully qualified name of {@code annotationFQN}
* associated with default constructor of the {@code aClass}, if the constructor exists.
*
* Multiple annotations may be returned since there may be repeatable annotations
* or annotations from several external annotations roots.
*
* @param aClass class of which default constructor's external annotations are to be found
* @param annotationFQN fully qualified name of annotation class to search for
* @return annotations of the default constructor of {@code aClass}, or {@code null} if the
* class doesn't have a default constructor.
* @see #findDefaultConstructorExternalAnnotations(PsiClass)
*/
@Nullable
public abstract List<PsiAnnotation> findDefaultConstructorExternalAnnotations(@NotNull PsiClass aClass, @NotNull String annotationFQN);
public abstract void annotateExternally(@NotNull PsiModifierListOwner listOwner,
@NotNull String annotationFQName,
@NotNull PsiFile fromFile,
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2017 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.
*/
// 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.codeInsight;
import com.intellij.lang.java.parser.JavaParser;
@@ -35,6 +21,8 @@ import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MostlySingularMultiMap;
import com.intellij.util.text.CharSequenceReader;
import gnu.trove.THashSet;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.Attributes;
@@ -48,6 +36,7 @@ import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
public abstract class BaseExternalAnnotationsManager extends ExternalAnnotationsManager {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.BaseExternalAnnotationsManager");
@@ -65,6 +54,25 @@ public abstract class BaseExternalAnnotationsManager extends ExternalAnnotations
LowMemoryWatcher.register(this::dropCache, psiManager.getProject());
}
/**
* Returns canonical string presentation of {@code listOwner}
* used in external annotations files.
*
* @param listOwner API element to return external name of
* @return external name or {@code null} if the {@code listOwner}
* is of unknown type (neither class, method, field nor parameter)
*/
@Nullable
protected static String getExternalName(@NotNull PsiModifierListOwner listOwner) {
return getExternalName(listOwner, false);
}
/**
* @deprecated use {@link #getExternalName(PsiModifierListOwner)} instead
* since external annotations files don't contain parameters' names anyway.
*/
@ApiStatus.ScheduledForRemoval(inVersion = "2019.3")
@Deprecated
@Nullable
protected static String getExternalName(@NotNull PsiModifierListOwner listOwner, boolean showParamName) {
return PsiFormatUtil.getExternalName(listOwner, showParamName, Integer.MAX_VALUE);
@@ -85,9 +93,45 @@ public abstract class BaseExternalAnnotationsManager extends ExternalAnnotations
@Override
@Nullable
public PsiAnnotation findExternalAnnotation(@NotNull final PsiModifierListOwner listOwner, @NotNull final String annotationFQN) {
List<AnnotationData> list = collectExternalAnnotations(listOwner);
AnnotationData data = findByFQN(list, annotationFQN);
return data == null ? null : data.getAnnotation(this);
List<PsiAnnotation> result = findExternalAnnotations(listOwner, annotationFQN);
return result.isEmpty() ? null : result.get(0);
}
@NotNull
@Override
public List<PsiAnnotation> findExternalAnnotations(@NotNull PsiModifierListOwner listOwner, @NotNull String annotationFQN) {
List<AnnotationData> result = collectExternalAnnotations(listOwner);
return filterAnnotations(result, annotationFQN);
}
@Nullable
@Override
public List<PsiAnnotation> findDefaultConstructorExternalAnnotations(@NotNull PsiClass aClass, @NotNull String annotationFQN) {
if (aClass.getConstructors().length > 0) {
return null;
}
List<AnnotationData> result = collectDefaultConstructorExternalAnnotations(aClass);
return filterAnnotations(result, annotationFQN);
}
@NotNull
private List<PsiAnnotation> filterAnnotations(@NotNull List<AnnotationData> result, @NotNull String annotationFQN) {
return StreamEx.of(result)
.filter(data -> data.annotationClassFqName.equals(annotationFQN))
.map(data -> data.getAnnotation(this))
.toCollection(SmartList::new);
}
@Nullable
@Override
public List<PsiAnnotation> findDefaultConstructorExternalAnnotations(@NotNull PsiClass aClass) {
if (aClass.getConstructors().length > 0) {
return null;
}
List<AnnotationData> result = collectDefaultConstructorExternalAnnotations(aClass);
return StreamEx.of(result)
.map(data -> data.getAnnotation(this))
.toCollection(SmartList::new);
}
@Override
@@ -110,21 +154,36 @@ public abstract class BaseExternalAnnotationsManager extends ExternalAnnotations
}
private static final List<AnnotationData> NO_DATA = new ArrayList<>(1);
private final ConcurrentMostlySingularMultiMap<PsiModifierListOwner, AnnotationData> cache = new ConcurrentMostlySingularMultiMap<>();
private final ConcurrentMostlySingularMultiMap<Object, AnnotationData> cache = new ConcurrentMostlySingularMultiMap<>();
// interner for storing annotation FQN
private final CharTableImpl charTable = new CharTableImpl();
@NotNull
private List<AnnotationData> collectExternalAnnotations(@NotNull PsiModifierListOwner listOwner) {
if (!hasAnyAnnotationsRoots()) return Collections.emptyList();
private List<AnnotationData> collectDefaultConstructorExternalAnnotations(@NotNull PsiClass aClass) {
//External annotations of default constructor are stored at the same annotations files as class' ones.
List<PsiFile> annotationsFiles = findExternalAnnotationsFiles(aClass);
if (annotationsFiles == null) return NO_DATA;
String defCtrExternalName = getExternalName(aClass) + " " + aClass.getName() + "()";
return collectExternalAnnotations(defCtrExternalName, () -> doCollect(defCtrExternalName, annotationsFiles, false));
}
@NotNull
private List<AnnotationData> collectExternalAnnotations(@NotNull PsiModifierListOwner listOwner) {
return collectExternalAnnotations(listOwner, () -> doCollect(listOwner, false));
}
@NotNull
private List<AnnotationData> collectExternalAnnotations(@NotNull Object cacheKey,
@NotNull Supplier<List<AnnotationData>> dataSupplier) {
if (!hasAnyAnnotationsRoots()) return Collections.emptyList();
List<AnnotationData> cached;
while (true) {
cached = (List<AnnotationData>)cache.get(listOwner);
cached = (List<AnnotationData>)cache.get(cacheKey);
if (cached == NO_DATA || !cached.isEmpty()) return cached;
List<AnnotationData> computed = doCollect(listOwner, false);
if (cache.replace(listOwner, cached, computed)) {
List<AnnotationData> computed = dataSupplier.get();
if (cache.replace(cacheKey, cached, computed)) {
cached = computed;
break;
}
@@ -182,11 +241,16 @@ public abstract class BaseExternalAnnotationsManager extends ExternalAnnotations
List<PsiFile> files = findExternalAnnotationsFiles(listOwner);
if (files == null) return NO_DATA;
String externalName = getExternalName(listOwner, false);
String externalName = getExternalName(listOwner);
if (externalName == null) return NO_DATA;
return doCollect(externalName, files, onlyWritable);
}
@NotNull
private List<AnnotationData> doCollect(@NotNull String externalName, @NotNull List<PsiFile> annotationsFiles, boolean onlyWritable) {
SmartList<AnnotationData> result = new SmartList<>();
for (PsiFile file : files) {
for (PsiFile file : annotationsFiles) {
if (!file.isValid()) continue;
if (onlyWritable && !file.isWritable()) continue;
@@ -423,7 +487,7 @@ public abstract class BaseExternalAnnotationsManager extends ExternalAnnotations
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
public void endElement(String uri, String localName, String qName) {
if ("item".equals(qName)) {
myExternalName = null;
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>Test.java</file>
<line>4</line>
<description>Default constructor in 'a.A' is deprecated</description>
</problem>
</problems>
@@ -0,0 +1,5 @@
<root>
<item name='a.A A()'>
<annotation name='java.lang.Deprecated'/>
</item>
</root>
@@ -0,0 +1,5 @@
import a.A;
//Here "Default constructor in A is deprecated" warning must be reported.
public class Test extends A {
}
@@ -0,0 +1,5 @@
package a;
//This class' default constructor is externally deprecated.
public class A {
}
@@ -1,23 +1,13 @@
/*
* Copyright 2000-2017 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.
*/
// 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.java.codeInspection;
import com.intellij.JavaTestUtil;
import com.intellij.codeInspection.deprecation.DeprecationInspection;
import com.intellij.openapi.roots.JavaModuleExternalPaths;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.testFramework.InspectionTestCase;
/**
@@ -79,4 +69,20 @@ public class DeprecationInspectionTest extends InspectionTestCase {
doTest("deprecation/" + getTestName(true), tool);
}
/**
* Sets up external deprecation annotations
* for the current test module.
*/
private void configureExternalAnnotationsUrls(String... urls) {
ModuleRootModificationUtil.updateModel(myModule, (root) -> {
JavaModuleExternalPaths extension = root.getModuleExtension(JavaModuleExternalPaths.class);
extension.setExternalAnnotationUrls(urls);
});
}
public void testExternallyDeprecatedDefaultConstructor() {
String url = VfsUtilCore.pathToUrl(FileUtil.toSystemIndependentName(getTestDataPath()) + "/deprecation/externallyDeprecatedDefaultConstructor/extAnnotations");
configureExternalAnnotationsUrls(url);
doTest();
}
}