blob: 46934950036614d028f96514ae014caa077097b3 [file] [log] [blame]
package org.jetbrains.android.inspections.lint;
import com.android.annotations.concurrency.GuardedBy;
import com.android.tools.lint.detector.api.*;
import com.intellij.analysis.AnalysisScope;
import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.lang.annotation.ProblemGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashMap;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.util.*;
import static com.android.tools.lint.detector.api.TextFormat.*;
import static com.intellij.xml.CommonXmlStrings.HTML_END;
import static com.intellij.xml.CommonXmlStrings.HTML_START;
public abstract class AndroidLintInspectionBase extends GlobalInspectionTool {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.inspections.lint.AndroidLintInspectionBase");
private static final Object ISSUE_MAP_LOCK = new Object();
@GuardedBy("ISSUE_MAP_LOCK")
private static volatile Map<Issue, String> ourIssue2InspectionShortName;
protected final Issue myIssue;
private final String[] myGroupPath;
private final String myDisplayName;
protected AndroidLintInspectionBase(@NotNull String displayName, @NotNull Issue issue) {
myIssue = issue;
final Category category = issue.getCategory();
myGroupPath = ArrayUtil.mergeArrays(new String[]{AndroidBundle.message("android.inspections.group.name"),
AndroidBundle.message("android.lint.inspections.subgroup.name")}, computeAllNames(category));
myDisplayName = displayName;
}
@NotNull
public AndroidLintQuickFix[] getQuickFixes(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull String message) {
return getQuickFixes(message);
}
@NotNull
public AndroidLintQuickFix[] getQuickFixes(@NotNull String message) {
return AndroidLintQuickFix.EMPTY_ARRAY;
}
@NotNull
public IntentionAction[] getIntentions(@NotNull PsiElement startElement, @NotNull PsiElement endElement) {
return IntentionAction.EMPTY_ARRAY;
}
@Override
public boolean isGraphNeeded() {
return false;
}
@NotNull
private LocalQuickFix[] getLocalQuickFixes(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull String message) {
final AndroidLintQuickFix[] fixes = getQuickFixes(startElement, endElement, message);
final LocalQuickFix[] result = new LocalQuickFix[fixes.length];
for (int i = 0; i < fixes.length; i++) {
if (fixes[i].isApplicable(startElement, endElement, AndroidQuickfixContexts.BatchContext.TYPE)) {
result[i] = new MyLocalQuickFix(fixes[i]);
}
}
return result;
}
@Override
public void runInspection(@NotNull AnalysisScope scope,
@NotNull final InspectionManager manager,
@NotNull final GlobalInspectionContext globalContext,
@NotNull final ProblemDescriptionsProcessor problemDescriptionsProcessor) {
final AndroidLintGlobalInspectionContext androidLintContext = globalContext.getExtension(AndroidLintGlobalInspectionContext.ID);
if (androidLintContext == null) {
return;
}
final Map<Issue, Map<File, List<ProblemData>>> problemMap = androidLintContext.getResults();
if (problemMap == null) {
return;
}
final Map<File, List<ProblemData>> file2ProblemList = problemMap.get(myIssue);
if (file2ProblemList == null) {
return;
}
for (final Map.Entry<File, List<ProblemData>> entry : file2ProblemList.entrySet()) {
final File file = entry.getKey();
final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file);
if (vFile == null) {
continue;
}
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
final PsiManager psiManager = PsiManager.getInstance(globalContext.getProject());
final PsiFile psiFile = psiManager.findFile(vFile);
if (psiFile != null) {
final ProblemDescriptor[] descriptors = computeProblemDescriptors(psiFile, manager, entry.getValue());
if (descriptors.length > 0) {
problemDescriptionsProcessor.addProblemElement(globalContext.getRefManager().getReference(psiFile), descriptors);
}
} else if (vFile.isDirectory()) {
final PsiDirectory psiDirectory = psiManager.findDirectory(vFile);
if (psiDirectory != null) {
final ProblemDescriptor[] descriptors = computeProblemDescriptors(psiDirectory, manager, entry.getValue());
if (descriptors.length > 0) {
problemDescriptionsProcessor.addProblemElement(globalContext.getRefManager().getReference(psiDirectory), descriptors);
}
}
}
}
});
}
}
@NotNull
private ProblemDescriptor[] computeProblemDescriptors(@NotNull PsiElement psiFile,
@NotNull InspectionManager manager,
@NotNull List<ProblemData> problems) {
final List<ProblemDescriptor> result = new ArrayList<ProblemDescriptor>();
for (ProblemData problemData : problems) {
final String originalMessage = problemData.getMessage();
// We need to have explicit <html> and </html> tags around the text; inspection infrastructure
// such as the {@link com.intellij.codeInspection.ex.DescriptorComposer} will call
// {@link com.intellij.xml.util.XmlStringUtil.isWrappedInHtml}. See issue 177283 for uses.
// Note that we also need to use HTML with unicode characters here, since the HTML display
// in the inspections view does not appear to support numeric code character entities.
String formattedMessage = HTML_START + RAW.convertTo(originalMessage, HTML_WITH_UNICODE) + HTML_END;
// The inspections UI does not correctly handle
final TextRange range = problemData.getTextRange();
if (range.getStartOffset() == range.getEndOffset()) {
if (psiFile instanceof PsiBinaryFile || psiFile instanceof PsiDirectory) {
final LocalQuickFix[] fixes = getLocalQuickFixes(psiFile, psiFile, originalMessage);
result.add(new NonTextFileProblemDescriptor((PsiFileSystemItem)psiFile, formattedMessage, fixes));
} else if (!isSuppressedFor(psiFile)) {
result.add(manager.createProblemDescriptor(psiFile, formattedMessage, false,
getLocalQuickFixes(psiFile, psiFile, originalMessage),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
}
}
else {
final PsiElement startElement = psiFile.findElementAt(range.getStartOffset());
final PsiElement endElement = psiFile.findElementAt(range.getEndOffset() - 1);
if (startElement != null && endElement != null && !isSuppressedFor(startElement)) {
result.add(manager.createProblemDescriptor(startElement, endElement, formattedMessage,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, false,
getLocalQuickFixes(startElement, endElement, originalMessage)));
}
}
}
return result.toArray(new ProblemDescriptor[result.size()]);
}
@NotNull
@Override
public SuppressQuickFix[] getBatchSuppressActions(@Nullable PsiElement element) {
SuppressLintQuickFix suppressLintQuickFix = new SuppressLintQuickFix(myIssue);
if (AndroidLintExternalAnnotator.INCLUDE_IDEA_SUPPRESS_ACTIONS) {
final List<SuppressQuickFix> result = new ArrayList<SuppressQuickFix>();
result.add(suppressLintQuickFix);
result.addAll(Arrays.asList(BatchSuppressManager.SERVICE.getInstance().createBatchSuppressActions(HighlightDisplayKey.find(getShortName()))));
result.addAll(Arrays.asList(new XmlSuppressableInspectionTool.SuppressTagStatic(getShortName()),
new XmlSuppressableInspectionTool.SuppressForFile(getShortName())));
return result.toArray(new SuppressQuickFix[result.size()]);
} else {
return new SuppressQuickFix[] { suppressLintQuickFix };
}
}
private static class SuppressLintQuickFix implements SuppressQuickFix {
private Issue myIssue;
private SuppressLintQuickFix(Issue issue) {
myIssue = issue;
}
@Override
public boolean isAvailable(@NotNull Project project, @NotNull PsiElement context) {
return true;
}
@NotNull
@Override
public String getName() {
return "Suppress with @SuppressLint (Java) or tools:ignore (XML)";
}
@NotNull
@Override
public String getFamilyName() {
return "Suppress";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement myElement = descriptor.getPsiElement();
PsiFile file = PsiTreeUtil.getParentOfType(myElement, PsiFile.class, false);
if (file != null) {
new SuppressLintIntentionAction(myIssue.getId(), myElement).invoke(project, null, file);
}
}
}
@TestOnly
public static void invalidateInspectionShortName2IssueMap() {
ourIssue2InspectionShortName = null;
}
public static String getInspectionShortNameByIssue(@NotNull Project project, @NotNull Issue issue) {
synchronized (ISSUE_MAP_LOCK) {
if (ourIssue2InspectionShortName == null) {
ourIssue2InspectionShortName = new HashMap<Issue, String>();
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
for (InspectionToolWrapper e : profile.getInspectionTools(null)) {
final String shortName = e.getShortName();
if (shortName.startsWith("AndroidLint")) {
final InspectionProfileEntry entry = e.getTool();
if (entry instanceof AndroidLintInspectionBase) {
final Issue s = ((AndroidLintInspectionBase)entry).getIssue();
ourIssue2InspectionShortName.put(s, shortName);
}
}
}
}
return ourIssue2InspectionShortName.get(issue);
}
}
@NotNull
private static String[] computeAllNames(@NotNull Category category) {
final List<String> result = new ArrayList<String>();
Category c = category;
while (c != null) {
final String name = c.getName();
if (name == null) {
return ArrayUtil.EMPTY_STRING_ARRAY;
}
result.add(name);
c = c.getParent();
}
return ArrayUtil.reverseArray(ArrayUtil.toStringArray(result));
}
@Nls
@NotNull
@Override
public String getGroupDisplayName() {
return AndroidBundle.message("android.lint.inspections.group.name");
}
@NotNull
@Override
public String[] getGroupPath() {
return myGroupPath;
}
@Nls
@NotNull
@Override
public String getDisplayName() {
return myDisplayName;
}
@SuppressWarnings("deprecation")
@Override
public String getStaticDescription() {
StringBuilder sb = new StringBuilder(1000);
sb.append("<html><body>");
sb.append(myIssue.getBriefDescription(HTML));
sb.append("<br><br>");
sb.append(myIssue.getExplanation(HTML));
List<String> urls = myIssue.getMoreInfo();
if (!urls.isEmpty()) {
boolean separated = false;
for (String url : urls) {
if (!myIssue.getExplanation(RAW).contains(url)) {
if (!separated) {
sb.append("<br><br>");
separated = true;
} else {
sb.append("<br>");
}
sb.append("<a href=\"");
sb.append(url);
sb.append("\">");
sb.append(url);
sb.append("</a>");
}
}
}
sb.append("</body></html>");
return sb.toString();
}
@Override
public boolean isEnabledByDefault() {
return myIssue.isEnabledByDefault();
}
@NotNull
@Override
public String getShortName() {
return InspectionProfileEntry.getShortName(getClass().getSimpleName());
}
@NotNull
@Override
public HighlightDisplayLevel getDefaultLevel() {
final Severity defaultSeverity = myIssue.getDefaultSeverity();
if (defaultSeverity == null) {
return HighlightDisplayLevel.WARNING;
}
final HighlightDisplayLevel displayLevel = toHighlightDisplayLevel(defaultSeverity);
return displayLevel != null ? displayLevel : HighlightDisplayLevel.WARNING;
}
@Nullable
static HighlightDisplayLevel toHighlightDisplayLevel(@NotNull Severity severity) {
switch (severity) {
case ERROR:
return HighlightDisplayLevel.ERROR;
case FATAL:
return HighlightDisplayLevel.ERROR;
case WARNING:
return HighlightDisplayLevel.WARNING;
case INFORMATIONAL:
return HighlightDisplayLevel.WEAK_WARNING;
case IGNORE:
return null;
default:
LOG.error("Unknown severity " + severity);
return null;
}
}
/** Returns true if the given analysis scope is adequate for single-file analysis */
private static boolean isSingleFileScope(EnumSet<Scope> scopes) {
if (scopes.size() != 1) {
return false;
}
final Scope scope = scopes.iterator().next();
return scope == Scope.JAVA_FILE || scope == Scope.RESOURCE_FILE || scope == Scope.MANIFEST
|| scope == Scope.PROGUARD_FILE || scope == Scope.OTHER;
}
@Override
public boolean worksInBatchModeOnly() {
Implementation implementation = myIssue.getImplementation();
if (isSingleFileScope(implementation.getScope())) {
return false;
}
for (EnumSet<Scope> scopes : implementation.getAnalysisScopes()) {
if (isSingleFileScope(scopes)) {
return false;
}
}
return true;
}
@NotNull
public Issue getIssue() {
return myIssue;
}
static class MyLocalQuickFix implements LocalQuickFix {
private final AndroidLintQuickFix myLintQuickFix;
MyLocalQuickFix(@NotNull AndroidLintQuickFix lintQuickFix) {
myLintQuickFix = lintQuickFix;
}
@NotNull
@Override
public String getName() {
return myLintQuickFix.getName();
}
@NotNull
@Override
public String getFamilyName() {
return AndroidBundle.message("android.lint.quickfixes.family");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
myLintQuickFix.apply(descriptor.getStartElement(), descriptor.getEndElement(), AndroidQuickfixContexts.BatchContext.getInstance());
}
}
/**
* A {@link com.intellij.codeInspection.ProblemDescriptor} for image and directory files. This is
* necessary because the {@link InspectionManager}'s createProblemDescriptor methods
* all use {@link com.intellij.codeInspection.ProblemDescriptorBase} where in the constructor
* it insists that the start and end {@link PsiElement} instances must have a valid
* <b>text</b> range, which does not apply for images.
* <p>
* This custom descriptor allows the batch lint analysis to correctly handle lint errors
* associated with image files (such as the various {@link com.android.tools.lint.checks.IconDetector}
* warnings), as well as directory errors (such as incorrect locale folders),
* and clicking on them will navigate to the correct icon.
*/
private static class NonTextFileProblemDescriptor implements ProblemDescriptor {
private final PsiFileSystemItem myFile;
private final String myMessage;
private final LocalQuickFix[] myFixes;
private ProblemGroup myGroup;
public NonTextFileProblemDescriptor(@NotNull PsiFileSystemItem file, @NotNull String message, @NotNull LocalQuickFix[] fixes) {
myFile = file;
myMessage = message;
myFixes = fixes;
}
@Override
public PsiElement getPsiElement() {
return myFile;
}
@Override
public PsiElement getStartElement() {
return myFile;
}
@Override
public PsiElement getEndElement() {
return myFile;
}
@Override
public TextRange getTextRangeInElement() {
return new TextRange(0, 0);
}
@Override
public int getLineNumber() {
return 0;
}
@NotNull
@Override
public ProblemHighlightType getHighlightType() {
return ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
}
@Override
public boolean isAfterEndOfLine() {
return false;
}
@Override
public void setTextAttributes(TextAttributesKey key) {
}
@Nullable
@Override
public ProblemGroup getProblemGroup() {
return myGroup;
}
@Override
public void setProblemGroup(@Nullable ProblemGroup problemGroup) {
myGroup = problemGroup;
}
@Override
public boolean showTooltip() {
return false;
}
@NotNull
@Override
public String getDescriptionTemplate() {
return myMessage;
}
@Nullable
@Override
public QuickFix[] getFixes() {
return myFixes;
}
}
}