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); } } }