blob: 5d565b2b3e25a9db3dc8121c92212de3f12b994e [file] [log] [blame]
package org.jetbrains.android.inspections.lint;
import com.android.SdkConstants;
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.idea.rendering.PsiProjectListener;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.LintRequest;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.utils.SdkUtils;
import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.DaemonBundle;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.intention.HighPriorityAction;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ex.CustomEditInspectionToolsSettingsAction;
import com.intellij.codeInspection.ex.DisableInspectionToolAction;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.ExternalAnnotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ui.UIUtil;
import com.intellij.xml.util.XmlStringUtil;
import org.jetbrains.android.compiler.AndroidCompileUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyFileType;
import javax.swing.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import static com.android.SdkConstants.*;
import static com.android.tools.lint.detector.api.TextFormat.HTML;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidLintExternalAnnotator extends ExternalAnnotator<State, State> {
static final boolean INCLUDE_IDEA_SUPPRESS_ACTIONS = false;
@Override
public State collectInformation(@NotNull PsiFile file) {
final Module module = ModuleUtilCore.findModuleForPsiElement(file);
if (module == null) {
return null;
}
final AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null && !IntellijLintProject.hasAndroidModule(module.getProject())) {
return null;
}
final VirtualFile vFile = file.getVirtualFile();
if (vFile == null) {
return null;
}
final FileType fileType = file.getFileType();
if (fileType == StdFileTypes.XML) {
if (facet == null || facet.getLocalResourceManager().getFileResourceType(file) == null &&
!SdkConstants.ANDROID_MANIFEST_XML.equals(vFile.getName())) {
return null;
}
}
else if (fileType == FileTypes.PLAIN_TEXT) {
if (!AndroidCommonUtils.PROGUARD_CFG_FILE_NAME.equals(file.getName()) &&
!AndroidCompileUtil.OLD_PROGUARD_CFG_FILE_NAME.equals(file.getName())) {
return null;
}
}
else if (fileType == GroovyFileType.GROOVY_FILE_TYPE) {
if (!SdkUtils.endsWithIgnoreCase(file.getName(), DOT_GRADLE)) {
return null;
}
// Ensure that we're listening to the PSI structure for Gradle file edit notifications
Project project = file.getProject();
if (Projects.requiresAndroidModel(project)) {
PsiProjectListener.getListener(project);
}
}
else if (fileType != StdFileTypes.JAVA && fileType != StdFileTypes.PROPERTIES) {
return null;
}
final List<Issue> issues = getIssuesFromInspections(file.getProject(), file);
if (issues.size() == 0) {
return null;
}
return new State(module, vFile, file.getText(), issues);
}
@Override
public State doAnnotate(final State state) {
final IntellijLintClient client = IntellijLintClient.forEditor(state);
try {
final LintDriver lint = new LintDriver(new IntellijLintIssueRegistry(), client);
EnumSet<Scope> scope;
VirtualFile mainFile = state.getMainFile();
final FileType fileType = mainFile.getFileType();
String name = mainFile.getName();
if (fileType == StdFileTypes.XML) {
if (name.equals(ANDROID_MANIFEST_XML)) {
scope = Scope.MANIFEST_SCOPE;
} else {
scope = Scope.RESOURCE_FILE_SCOPE;
}
} else if (fileType == StdFileTypes.JAVA) {
scope = Scope.JAVA_FILE_SCOPE;
} else if (name.equals(OLD_PROGUARD_FILE) || name.equals(FN_PROJECT_PROGUARD_FILE)) {
scope = EnumSet.of(Scope.PROGUARD_FILE);
} else if (fileType == GroovyFileType.GROOVY_FILE_TYPE) {
scope = Scope.GRADLE_SCOPE;
} else if (fileType == StdFileTypes.PROPERTIES) {
scope = Scope.PROPERTY_SCOPE;
} else {
// #collectionInformation above should have prevented this
assert false;
return state;
}
Project project = state.getModule().getProject();
if (project.isDisposed()) {
return state;
}
List<VirtualFile> files = Collections.singletonList(mainFile);
LintRequest request = new IntellijLintRequest(client, project, files,
Collections.singletonList(state.getModule()), true /* incremental */);
request.setScope(scope);
lint.analyze(request);
}
finally {
Disposer.dispose(client);
}
return state;
}
@NotNull
static List<Issue> getIssuesFromInspections(@NotNull Project project, @Nullable PsiElement context) {
final List<Issue> result = new ArrayList<Issue>();
final IssueRegistry fullRegistry = new IntellijLintIssueRegistry();
for (Issue issue : fullRegistry.getIssues()) {
final String inspectionShortName = AndroidLintInspectionBase.getInspectionShortNameByIssue(project, issue);
if (inspectionShortName == null) {
continue;
}
final HighlightDisplayKey key = HighlightDisplayKey.find(inspectionShortName);
if (key == null) {
continue;
}
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
final boolean enabled = context != null ? profile.isToolEnabled(key, context) : profile.isToolEnabled(key);
if (!enabled) {
continue;
}
result.add(issue);
}
return result;
}
@Override
public void apply(@NotNull PsiFile file, State state, @NotNull AnnotationHolder holder) {
if (state.isDirty()) {
return;
}
final Project project = file.getProject();
for (ProblemData problemData : state.getProblems()) {
final Issue issue = problemData.getIssue();
final String message = problemData.getMessage();
final TextRange range = problemData.getTextRange();
if (range.getStartOffset() == range.getEndOffset()) {
continue;
}
final Pair<AndroidLintInspectionBase, HighlightDisplayLevel> pair =
AndroidLintUtil.getHighlighLevelAndInspection(project, issue, file);
if (pair == null) {
continue;
}
final AndroidLintInspectionBase inspection = pair.getFirst();
HighlightDisplayLevel displayLevel = pair.getSecond();
if (inspection != null) {
final HighlightDisplayKey key = HighlightDisplayKey.find(inspection.getShortName());
if (key != null) {
final PsiElement startElement = file.findElementAt(range.getStartOffset());
final PsiElement endElement = file.findElementAt(range.getEndOffset() - 1);
if (startElement != null && endElement != null && !inspection.isSuppressedFor(startElement)) {
if (problemData.getConfiguredSeverity() != null) {
HighlightDisplayLevel configuredLevel =
AndroidLintInspectionBase.toHighlightDisplayLevel(problemData.getConfiguredSeverity());
if (configuredLevel != null) {
displayLevel = configuredLevel;
}
}
final Annotation annotation = createAnnotation(holder, message, range, displayLevel, issue);
for (AndroidLintQuickFix fix : inspection.getQuickFixes(startElement, endElement, message)) {
if (fix.isApplicable(startElement, endElement, AndroidQuickfixContexts.EditorContext.TYPE)) {
annotation.registerFix(new MyFixingIntention(fix, startElement, endElement));
}
}
for (IntentionAction intention : inspection.getIntentions(startElement, endElement)) {
annotation.registerFix(intention);
}
annotation.registerFix(new SuppressLintIntentionAction(key.getID(), startElement));
annotation.registerFix(new MyDisableInspectionFix(key));
annotation.registerFix(new MyEditInspectionToolsSettingsAction(key, inspection));
if (INCLUDE_IDEA_SUPPRESS_ACTIONS) {
final SuppressQuickFix[] suppressActions = inspection.getBatchSuppressActions(startElement);
for (SuppressQuickFix action : suppressActions) {
if (action.isAvailable(project, startElement)) {
ProblemHighlightType type = annotation.getHighlightType();
annotation.registerFix(action, null, key, InspectionManager.getInstance(project).createProblemDescriptor(
startElement, endElement, message, type, true, LocalQuickFix.EMPTY_ARRAY));
}
}
}
}
}
}
}
}
@SuppressWarnings("deprecation")
@NotNull
private Annotation createAnnotation(@NotNull AnnotationHolder holder,
@NotNull String message,
@NotNull TextRange range,
@NotNull HighlightDisplayLevel displayLevel,
@NotNull Issue issue) {
// Convert from inspection severity to annotation severity
HighlightSeverity severity;
if (displayLevel == HighlightDisplayLevel.ERROR) {
severity = HighlightSeverity.ERROR;
} else if (displayLevel == HighlightDisplayLevel.WARNING) {
severity = HighlightSeverity.WARNING;
} else if (displayLevel == HighlightDisplayLevel.WEAK_WARNING) {
severity = HighlightSeverity.WEAK_WARNING;
} else if (displayLevel == HighlightDisplayLevel.INFO) {
severity = HighlightSeverity.INFO;
} else {
severity = HighlightSeverity.WARNING;
}
// Attempt to mark up as HTML? Only if available
Method createHtmlAnnotation = getCreateHtmlAnnotation();
if (createHtmlAnnotation != null) {
// Based on LocalInspectionsPass#createHighlightInfo
String link = " <a "
+"href=\"#lint/" + issue.getId() + "\""
+ (UIUtil.isUnderDarcula() ? " color=\"7AB4C9\" " : "")
+">" + DaemonBundle.message("inspection.extended.description")
+"</a> " + getShowMoreShortCut();
String tooltip = XmlStringUtil.wrapInHtml(RAW.convertTo(message, HTML) + link);
try {
return (Annotation)createHtmlAnnotation.invoke(holder, severity, range, message, tooltip);
}
catch (IllegalAccessException ignored) {
ourCreateHtmlAnnotationMethod = null;
//noinspection AssignmentToStaticFieldFromInstanceMethod
ourCreateHtmlAnnotationMethodFailed = true;
}
catch (InvocationTargetException e) {
ourCreateHtmlAnnotationMethod = null;
//noinspection AssignmentToStaticFieldFromInstanceMethod
ourCreateHtmlAnnotationMethodFailed = true;
}
}
return holder.createAnnotation(severity, range, message);
}
private static boolean ourCreateHtmlAnnotationMethodFailed;
private static Method ourCreateHtmlAnnotationMethod;
@Nullable
private static Method getCreateHtmlAnnotation() {
if (ourCreateHtmlAnnotationMethod != null) {
return ourCreateHtmlAnnotationMethod;
}
if (ourCreateHtmlAnnotationMethodFailed) {
return null;
} else {
ourCreateHtmlAnnotationMethodFailed = true;
try {
ourCreateHtmlAnnotationMethod = AnnotationHolder.class.getMethod("createAnnotation", HighlightSeverity.class,
TextRange.class, String.class, String.class);
}
catch (NoSuchMethodException ignore) {
}
return ourCreateHtmlAnnotationMethod;
}
}
// Based on similar code in the LocalInspectionsPass constructor
private String myShortcutText;
private String getShowMoreShortCut() {
if (myShortcutText == null) {
final KeymapManager keymapManager = KeymapManager.getInstance();
if (keymapManager != null) {
final Keymap keymap = keymapManager.getActiveKeymap();
myShortcutText =
keymap == null ? "" : "(" + KeymapUtil.getShortcutsText(keymap.getShortcuts(IdeActions.ACTION_SHOW_ERROR_DESCRIPTION)) + ")";
}
else {
myShortcutText = "";
}
}
return myShortcutText;
}
private static class MyDisableInspectionFix implements IntentionAction, Iconable {
private final DisableInspectionToolAction myDisableInspectionToolAction;
private MyDisableInspectionFix(@NotNull HighlightDisplayKey key) {
myDisableInspectionToolAction = new DisableInspectionToolAction(key);
}
@NotNull
@Override
public String getText() {
return "Disable inspection";
}
@NotNull
@Override
public String getFamilyName() {
return getText();
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
myDisableInspectionToolAction.invoke(project, editor, file);
}
@Override
public boolean startInWriteAction() {
return myDisableInspectionToolAction.startInWriteAction();
}
@Override
public Icon getIcon(@IconFlags int flags) {
return myDisableInspectionToolAction.getIcon(flags);
}
}
public static class MyFixingIntention implements IntentionAction, HighPriorityAction {
private final AndroidLintQuickFix myQuickFix;
private final PsiElement myStartElement;
private final PsiElement myEndElement;
public MyFixingIntention(@NotNull AndroidLintQuickFix quickFix, @NotNull PsiElement startElement, @NotNull PsiElement endElement) {
myQuickFix = quickFix;
myStartElement = startElement;
myEndElement = endElement;
}
@NotNull
@Override
public String getText() {
return myQuickFix.getName();
}
@NotNull
@Override
public String getFamilyName() {
return AndroidBundle.message("android.lint.quickfixes.family");
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
FileModificationService.getInstance().prepareFileForWrite(file);
myQuickFix.apply(myStartElement, myEndElement, AndroidQuickfixContexts.EditorContext.getInstance(editor));
}
@Override
public boolean startInWriteAction() {
return true;
}
}
private static class MyEditInspectionToolsSettingsAction extends CustomEditInspectionToolsSettingsAction {
private MyEditInspectionToolsSettingsAction(@NotNull HighlightDisplayKey key, @NotNull final AndroidLintInspectionBase inspection) {
super(key, new Computable<String>() {
@Override
public String compute() {
return "Edit '" + inspection.getDisplayName() + "' inspection settings";
}
});
}
}
}