package org.intellij.sonar.analysis;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import com.google.common.collect.Sets;
import com.intellij.codeInsight.daemon.DaemonBundle;
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.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.MarkupModel;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.ui.UIUtil;
import com.intellij.xml.util.XmlStringUtil;
import org.intellij.sonar.DocumentChangeListener;
import org.intellij.sonar.index.IssuesByFileIndex;
import org.intellij.sonar.index.SonarIssue;
import org.intellij.sonar.util.Finders;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SonarExternalAnnotator
extends ExternalAnnotator<SonarExternalAnnotator.InitialInfo,SonarExternalAnnotator.AnnotationResult> {
public static final Key<Set<SonarIssue>> KEY = new Key<Set<SonarIssue>>("issues");
public static class InitialInfo {
public PsiFile psiFile;
}
@Nullable
@Override
public InitialInfo collectInformation(@NotNull PsiFile file) {
InitialInfo initialInfo = new InitialInfo();
initialInfo.psiFile = file;
return initialInfo;
}
@Nullable
@Override
public AnnotationResult doAnnotate(final InitialInfo initialInfo) {
final AnnotationResult annotationResult = new AnnotationResult();
try {
ApplicationManager.getApplication().executeOnPooledThread(
() -> {
final Set<SonarIssue> issues = createSonarIssues(initialInfo.psiFile);
annotationResult.sonarIssues = issues;
}
).get();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException(e);
}
return annotationResult;
}
public static class AnnotationResult {
public Set<SonarIssue> sonarIssues = new HashSet<SonarIssue>();
}
@Override
public void apply(
@NotNull final PsiFile file,
final AnnotationResult annotationResult,
@NotNull final AnnotationHolder holder
) {
if (
null == file.getVirtualFile() ||
null == ProjectFileIndex.SERVICE.getInstance(file.getProject()).getContentRootForFile(file.getVirtualFile()) ||
(
// Fixes #106: Annotations in PHPStorm shown twice per File
"HTML".equals(file.getFileType().getName()) &&
"php".equals(file.getVirtualFile().getExtension()))
) {
return;
}
createAnnotations(file,annotationResult,holder);
}
private void createAnnotations(
@NotNull final PsiFile psiFile,
AnnotationResult annotationResult,
@NotNull AnnotationHolder holder
) {
final Set<SonarIssue> issues = annotationResult.sonarIssues;
for (SonarIssue issue : issues) {
Optional<Annotation> annotation = createAnnotation(holder,psiFile,issue);
if (annotation.isPresent()) {
String tooltip = createTooltip(issue);
annotation.get().setTooltip(tooltip);
}
}
}
@NotNull
private Set<SonarIssue> createSonarIssues(@NotNull PsiFile psiFile) {
final Set<SonarIssue> issues;
if (!DocumentChangeListener.CHANGED_FILES.contains(psiFile.getVirtualFile())) {
issues = IssuesByFileIndex.getIssuesForFile(psiFile);
for (SonarIssue issue : issues) {
final TextRange textRange = Finders.getLineRange(psiFile,issue.getLine());
createInvisibleHighlighter(psiFile,issue,textRange);
}
} else {
final Set<SonarIssue> issuesFromHighlighters = Sets.newLinkedHashSet();
Optional<Document> document = Finders.findDocumentFromPsiFile(psiFile);
if (document.isPresent()) {
Set<RangeHighlighter> highlighters = Finders.findAllRangeHighlightersFrom(document.get());
for (RangeHighlighter highlighter : highlighters) {
Optional<Set<SonarIssue>> issuesFromHighlighter = Optional.ofNullable(highlighter.getUserData(KEY));
issuesFromHighlighter.ifPresent(issuesFromHighlighters::addAll);
}
}
issues = issuesFromHighlighters;
}
return issues;
}
private void createInvisibleHighlighter(PsiFile psiFile,final SonarIssue issue,final TextRange textRange) {
final Optional<Document> document = Finders.findDocumentFromPsiFile(psiFile);
final List<Editor> editors = Finders.findEditorsFrom(document.get());
for (final Editor editor : editors) {
final MarkupModel markupModel = editor.getMarkupModel();
ApplicationManager.getApplication().invokeLater(
() -> {
final Optional<RangeHighlighter> rangeHighlighterAtLine = Finders.findRangeHighlighterAtLine(
editor,
issue.getLine()
);
if (rangeHighlighterAtLine.isPresent()) {
final Set<SonarIssue> issuesOfHighlighter = rangeHighlighterAtLine.get().getUserData(KEY);
if (null != issuesOfHighlighter) {
issuesOfHighlighter.add(issue);
}
} else {
TextAttributes attrs = new TextAttributes();
final RangeHighlighter rangeHighlighter = markupModel.addRangeHighlighter(
textRange.getStartOffset(),
textRange.getEndOffset(),
0,
attrs,
HighlighterTargetArea.EXACT_RANGE
);
Set<SonarIssue> issuesOfHighlighter = Sets.newLinkedHashSet();
issuesOfHighlighter.add(issue);
rangeHighlighter.putUserData(KEY,issuesOfHighlighter);
}
}
);
}
}
public static Optional<Annotation> createAnnotation(AnnotationHolder holder,PsiFile psiFile,SonarIssue issue) {
HighlightSeverity severity = SonarToIjSeverityMapping.toHighlightSeverity(issue.getSeverity());
Annotation annotation;
if (issue.getLine() == null) {
annotation = createAnnotation(holder,issue.formattedMessage(),psiFile,severity);
annotation.setFileLevelAnnotation(true);
} else {
Optional<PsiElement> startElement = Finders.findFirstElementAtLine(psiFile,issue.getLine());
if (!startElement.isPresent()) {
// There is no AST element on this line. Maybe a tabulation issue on a blank line?
annotation = createAnnotation(
holder,
issue.formattedMessage(),
Finders.getLineRange(psiFile,issue.getLine()),
severity
);
} else
if (startElement.get().isValid()) {
TextRange lineRange = Finders.getLineRange(startElement.get());
annotation = createAnnotation(holder,issue.formattedMessage(),lineRange,severity);
} else {
annotation = null;
}
}
return Optional.ofNullable(annotation);
}
private static String createTooltip(SonarIssue issue) {
String myShortcutText;
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 = "";
}
@NonNls final String link = " <a "
+"href=\"#sonarissue/"+issue.getKey()+"\""
+(UIUtil.isUnderDarcula()
? " color=\"7AB4C9\" "
: "")
+">"+DaemonBundle.message("inspection.extended.description")
+"</a> "+myShortcutText;
return XmlStringUtil.wrapInHtml(XmlStringUtil.escapeString(issue.formattedMessage())+link);
}
private static Annotation createAnnotation(
AnnotationHolder holder,
String message,
PsiElement location,
HighlightSeverity severity
) {
if (HighlightSeverity.ERROR.equals(severity)) {
return holder.createErrorAnnotation(location.getTextRange(),message);
} else
if (HighlightSeverity.WEAK_WARNING.equals(severity)) {
return holder.createWeakWarningAnnotation(location.getTextRange(),message);
} else
if (HighlightSeverity.WARNING.equals(severity)) {
return holder.createWarningAnnotation(location.getTextRange(),message);
} else {
throw new IllegalArgumentException("Unhandled severity "+severity);
}
}
private static Annotation createAnnotation(
AnnotationHolder holder,
String message,
TextRange textRange,
HighlightSeverity severity
) {
if (HighlightSeverity.ERROR.equals(severity)) {
return holder.createErrorAnnotation(textRange,message);
} else
if (HighlightSeverity.WEAK_WARNING.equals(severity)) {
return holder.createWeakWarningAnnotation(textRange,message);
} else
if (HighlightSeverity.WARNING.equals(severity)) {
return holder.createWarningAnnotation(textRange,message);
} else {
throw new IllegalArgumentException("Unhandled severity "+severity);
}
}
}