IDEA-321013 Outdated HighlightInfo's should be removed as soon as the relevant annotator or inspection finishes

Rework annotators engine to make annotators run in parallel, each on all relevant PSI elements in their own order (makes fast annotators complete faster to allow them to remove outdated highlighters faster).
For that, for each annotator (in parallel):
- create its own AnnotationHolder
- rearrange its PSI elements in "time to first diagnostic in previous run" order, to reduce latency.
- run annotator on these PSI elements sequentially
- as soon as annotator produces info/fails to produce the same info from the previous run, update the corresponding range highlighters
Pleas note, there's no more contract "Do not call annotators for parent PSI if some (maybe completely unrelated) annotator/highlight visitor produced error for some PSI element".
Fix highlighting tests, the majority of which relied on annotator order, or implicit contract above.
Fix a bunch of annotators which tried to double-visit some PSI elements to fight the contract above.

GitOrigin-RevId: 74f727fc6d3be3f500cdbb0f26e7d0daf1ffe7ff
This commit is contained in:
Alexey Kudravtsev
2024-02-12 18:52:52 +00:00
committed by intellij-monorepo-bot
parent d336c981c2
commit 394e5c640a
39 changed files with 761 additions and 245 deletions
@@ -22,6 +22,7 @@ import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorMouseHoverPopupManager;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.markup.MarkupModel;
@@ -36,6 +37,7 @@ import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
import com.intellij.openapi.util.ProperTextRange;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
@@ -80,6 +82,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
@Override
protected void tearDown() throws Exception {
MyRecordingAnnotator.clearAll();
try {
if (myEditor != null) {
Document document = myEditor.getDocument();
@@ -169,7 +172,6 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
}
public void testAddRemoveHighlighterRaceInIncorrectAnnotatorsWhichUseFileRecursiveVisit() {
//System.out.println("i = " + i);
useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new MyRecordingAnnotator[]{new MyIncorrectlyRecursiveAnnotator()}, () -> {
@Language("JAVA")
String text1 = """
@@ -188,7 +190,6 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
assertEquals("XXX", assertOneElement(doHighlighting(HighlightSeverity.WARNING)).getDescription());
for (int i = 0; i < 100; i++) {
//System.out.println("i = " + i);
myDaemonCodeAnalyzer.restart();
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
assertEquals("XXX", assertOneElement(infos).getDescription());
@@ -295,6 +296,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
TimeoutUtil.sleep(100);
}
}
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
}
}
public static class MyFastAnnotator extends MyRecordingAnnotator {
@@ -306,6 +308,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
holder.newAnnotation(HighlightSeverity.ERROR, SWEARING).range(element).create();
iDidIt();
}
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
}
}
@@ -316,6 +319,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
holder.newAnnotation(HighlightSeverity.INFORMATION, "comment").create();
iDidIt();
}
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
}
}
@@ -356,7 +360,9 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
MarkupModel markupModel = DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
TestTimeOut n = TestTimeOut.setTimeout(100, TimeUnit.SECONDS);
AtomicInteger called = new AtomicInteger();
AtomicBoolean success = new AtomicBoolean();
Runnable checkHighlighted = () -> {
if (success.get()) return;
called.incrementAndGet();
UIUtil.dispatchAllInvocationEvents();
long highlighted = Arrays.stream(markupModel.getAllHighlighters())
@@ -366,26 +372,25 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
.count();
if (highlighted != 0) {
toSleepMs.set(0);
throw new DebugException(); // sorry for that, had to differentiate from failure
success.set(true);
//throw new DebugException(); // sorry for that, had to differentiate from failure
}
if (n.timedOut()) {
toSleepMs.set(0);
throw new RuntimeException(new TimeoutException(ThreadDumper.dumpThreadsToString()));
}
};
try {
CodeInsightTestFixtureImpl.ensureIndexesUpToDate(getProject());
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
long start = System.currentTimeMillis();
myDaemonCodeAnalyzer.runPasses(getFile(), getEditor().getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, false, checkHighlighted);
CodeInsightTestFixtureImpl.ensureIndexesUpToDate(getProject());
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
long start = System.currentTimeMillis();
myDaemonCodeAnalyzer.runPasses(getFile(), getEditor().getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, false, checkHighlighted);
if (!success.get()) {
List<RangeHighlighter> errors = ContainerUtil.filter(markupModel.getAllHighlighters(), highlighter -> HighlightInfo.fromRangeHighlighter(highlighter) != null && HighlightInfo.fromRangeHighlighter(highlighter).getSeverity() == HighlightSeverity.ERROR);
long elapsed = System.currentTimeMillis() - start;
fail("should have been interrupted. toSleepMs: " + toSleepMs + "; highlights: " + errors + "; called: " + called+"; highlighted in "+elapsed+"ms");
}
catch (DebugException ignored) {
}
}
public static class MyNewBuilderAnnotator extends MyRecordingAnnotator {
@@ -393,21 +398,21 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PsiComment && element.getText().equals("//XXX")) {
holder.newAnnotation(HighlightSeverity.ERROR, MyFastAnnotator.SWEARING).create();
iDidIt();
}
else if (didIDoIt()) {
// sleep after creating annotation to emulate a very big annotator which does a great amount of work after registering annotation
// use this contrived form to be able to bail out immediately by modifying toSleepMs in the other thread
while (toSleepMs.addAndGet(-100) > 0) {
TimeoutUtil.sleep(100);
}
iDidIt();
}
}
}
public void testAddAnnotationViaBuilderEntailsCreatingCorrespondingRangeHighlighterImmediately() {
PlatformTestUtil.assumeEnoughParallelism();
useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new MyRecordingAnnotator[]{new MyNewBuilderAnnotator()},
this::checkSwearingHighlightIsVisibleImmediately);
useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new MyRecordingAnnotator[]{new MyNewBuilderAnnotator()}, this::checkSwearingHighlightIsVisibleImmediately);
}
private static final AtomicBoolean annotated = new AtomicBoolean();
@@ -436,7 +441,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
}
}
public void test_SerializeCodeInsightPasses_SecretSettingDoesWork() {
public void _test_SerializeCodeInsightPasses_SecretSettingDoesWork() {
PlatformTestUtil.assumeEnoughParallelism();
TextEditorHighlightingPassRegistrarImpl registrar =
@@ -538,24 +543,17 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
}
private void checkFirstAnnotation() {
AtomicReference<Throwable> reported = new AtomicReference<>();
AtomicReference<DaemonCodeAnalyzer.DaemonListener.AnnotatorStatistics> firstStatistics = new AtomicReference<>();
getProject().getMessageBus().connect(getTestRootDisposable()).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
new DaemonCodeAnalyzer.DaemonListener() {
@Override
public void daemonAnnotatorStatisticsGenerated(@NotNull AnnotationSession session,
@NotNull Collection<? extends AnnotatorStatistics> statistics,
@NotNull PsiFile file) {
AnnotatorStatistics stat = assertOneElement(ContainerUtil.filter(statistics, stat1 -> stat1.annotator instanceof MyInfoAnnotator));
Throwable old = reported.getAndSet(new Throwable());
assertNull(old==null? null: ExceptionUtil.getMessage(old), old);
assertEquals("Annotation(message='comment', severity='INFORMATION', toolTip='<html>comment</html>')", stat.firstAnnotation.toString());
assertSame(stat.firstAnnotation, stat.lastAnnotation);
assertTrue(stat.annotatorStartStamp > 0);
assertTrue(stat.firstAnnotationStamp >= stat.annotatorStartStamp);
assertTrue(stat.lastAnnotationStamp >= stat.firstAnnotationStamp);
assertTrue(stat.annotatorFinishStamp >= stat.lastAnnotationStamp);
}
});
new DaemonCodeAnalyzer.DaemonListener() {
@Override
public void daemonAnnotatorStatisticsGenerated(@NotNull AnnotationSession session,
@NotNull Collection<? extends AnnotatorStatistics> statistics,
@NotNull PsiFile file) {
AnnotatorStatistics stat = assertOneElement(ContainerUtil.filter(statistics, stat1 -> stat1.annotator instanceof MyInfoAnnotator));
firstStatistics.compareAndExchange(null, stat);
}
});
@Language("JAVA")
String text = """
@@ -566,7 +564,15 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
configureByText(JavaFileType.INSTANCE, text);
doHighlighting();
assertNotNull(reported.get());
DaemonCodeAnalyzer.DaemonListener.AnnotatorStatistics stat = firstStatistics.get();
assertNotNull(stat);
assertEquals("Annotation(message='comment', severity='INFORMATION', toolTip='<html>comment</html>')", stat.firstAnnotation.toString());
assertSame(stat.firstAnnotation, stat.lastAnnotation);
assertTrue(stat.annotatorStartStamp > 0);
assertTrue(stat.firstAnnotationStamp >= stat.annotatorStartStamp);
assertTrue(stat.lastAnnotationStamp >= stat.firstAnnotationStamp);
assertTrue(stat.annotatorFinishStamp >= stat.lastAnnotationStamp);
}
private static final String wordToAnnotate = "annotate_here";
@@ -642,4 +648,215 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase
expectedVisibleRange = new TextRange(0, editor.getDocument().getTextLength());
useAnnotatorsIn(PlainTextLanguage.INSTANCE, new MyRecordingAnnotator[]{new CheckVisibleRangeAnnotator()}, ()-> assertEmpty(doHighlighting()));
}
// highlight each field, stall every other element
static class MyFieldSlowAnnotator extends MyRecordingAnnotator {
static final AtomicReference<String> fieldWarningText = new AtomicReference<>();
static final AtomicInteger stallMs = new AtomicInteger();
static final AtomicBoolean finished = new AtomicBoolean();
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PsiField) {
holder.newAnnotation(HighlightSeverity.WARNING, fieldWarningText.get()).range(element).create();
iDidIt();
}
else if (element instanceof PsiFile) {
finished.set(true);
}
else {
// stall every other element to exacerbate latency problems if the order is wrong
TimeoutUtil.sleep(stallMs.get());
}
}
}
// highlights all "xxx" comments, but only when there are no comments after it
static class MyCommentFastAnnotator extends MyRecordingAnnotator {
static final AtomicBoolean finished = new AtomicBoolean();
static final String fastToolText = "blah.MyCommentFastAnnotator";
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PsiComment) {
if (element.getText().contains("xxx") && !element.getContainingFile().getText().substring(element.getTextOffset()+2).contains("//")) {
holder.newAnnotation(HighlightSeverity.WARNING, fastToolText).range(element).create();
iDidIt();
}
}
else if (element instanceof PsiFile) {
finished.set(true);
iDidIt();
}
}
}
public void testAnnotatorMustRemoveItsObsoleteHighlightsImmediatelyAfterFinished() {
@Language("JAVA")
String text = """
class LQF {
// xxx
int f;<caret>
}""";
configureByText(JavaFileType.INSTANCE, text);
DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way
UIUtil.markAsFocused(getEditor().getContentComponent(), true); // to make ShowIntentionPass call its collectInformation()
SeverityRegistrar.getSeverityRegistrar(getProject()); //preload inspection profile
MyFieldSlowAnnotator.fieldWarningText.set("1st run");
MyFieldSlowAnnotator.finished.set(false);
MyFieldSlowAnnotator.stallMs.set(0);
MyCommentFastAnnotator.finished.set(false);
Map<com.intellij.lang.Language, MyRecordingAnnotator[]> annotatorsByLanguage = new HashMap<>();
annotatorsByLanguage.put(JavaLanguage.INSTANCE, new MyRecordingAnnotator[]{new MyFieldSlowAnnotator(), new MyCommentFastAnnotator()});
MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
// both annos should produce their results
useAnnotatorsIn(annotatorsByLanguage, () -> {
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(MyFieldSlowAnnotator.fieldWarningText.get())));
assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(MyCommentFastAnnotator.fastToolText)));
RangeHighlighter[] markers = model.getAllHighlighters();
assertTrue(Arrays.toString(markers), ContainerUtil.exists(markers, i -> HighlightInfo.fromRangeHighlighter(i) != null && MyFieldSlowAnnotator.fieldWarningText.get().equals(HighlightInfo.fromRangeHighlighter(i).getDescription())));
assertTrue(Arrays.toString(markers), ContainerUtil.exists(markers, i -> HighlightInfo.fromRangeHighlighter(i) != null && MyCommentFastAnnotator.fastToolText.equals(HighlightInfo.fromRangeHighlighter(i).getDescription())));
});
MyFieldSlowAnnotator.fieldWarningText.set("Aha, field, finally!");
MyFieldSlowAnnotator.stallMs.set(100);
// type another comment which will cause the warning about the first comment (by MyCommentFastAnnotator) to disappear
// and check that as soon as MyCommentFastAnnotator is finished, it removed its own obsolete warnings, whereas MyFieldSlowAnnotator continues to run
type("// another comment");
MyCommentFastAnnotator.finished.set(false);
MyFieldSlowAnnotator.finished.set(false);
DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way
useAnnotatorsIn(annotatorsByLanguage, () -> {
// now when the highlighting is restarted, we should get back our inspection result very fast, despite very slow processing of every other element
long deadline = System.currentTimeMillis() + 10_000;
while (!DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) {
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
if (System.currentTimeMillis() > deadline) {
fail("Too long waiting for daemon to start");
}
}
try {
boolean fastToolFinishedFaster = false;
while (DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) {
if (System.currentTimeMillis() > deadline) {
fail("Too long waiting for daemon to finish\n" + ThreadDumper.dumpThreadsToString());
}
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
if (MyCommentFastAnnotator.finished.get() && !MyFieldSlowAnnotator.finished.get()) {
boolean fastToolWarningFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0,
myEditor.getDocument().getTextLength(),
info -> !MyCommentFastAnnotator.fastToolText.equals(info.getDescription()));
fastToolFinishedFaster = true;
if (fastToolWarningFound) {
fail("Annotator must have removed its own obsolete highlights as soon as it's finished, but got:" +
StringUtil.join(model.getAllHighlighters(), Object::toString, "\n ") + "; thread dump:\n" + ThreadDumper.dumpThreadsToString());
}
}
}
assertTrue("Fast inspection must have finished faster than the slow one, but it didn't", fastToolFinishedFaster);
}
finally {
MyFieldSlowAnnotator.stallMs.set(0);
}
});
}
static class MyComment1Annotator extends MyRecordingAnnotator {
static final AtomicBoolean stall1 = new AtomicBoolean();
static final String comment1Text = "comment1Text";
public MyComment1Annotator() {
}
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
while (stall1.get()) {
Thread.yield();
//ProgressManager.checkCanceled();
}
if (element instanceof PsiComment) {
if (element.getText().contains("xxx")) {
holder.newAnnotation(HighlightSeverity.WARNING, comment1Text).range(element).create();
iDidIt();
//stall1.set(true); // stall right after producing annotation
}
}
}
}
static class MyComment2Annotator extends MyRecordingAnnotator {
static final AtomicBoolean stall2 = new AtomicBoolean();
static final String comment2Text = "comment2Text";
public MyComment2Annotator() {
}
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
while (stall2.get()) {
Thread.yield();
//ProgressManager.checkCanceled();
}
if (element instanceof PsiComment) {
if (element.getText().contains("xxx")) {
holder.newAnnotation(HighlightSeverity.WARNING, comment2Text).range(element).create();
iDidIt();
//stall2.set(true); // stall right after producing annotation
}
}
}
}
public void testAnnotatorsMustNotWaitForEachOther() {
PlatformTestUtil.assumeEnoughParallelism();
@Language("JAVA")
String text = """
class LQF {
// xxx
int f;
}""";
configureByText(JavaFileType.INSTANCE, text);
DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way
UIUtil.markAsFocused(getEditor().getContentComponent(), true); // to make ShowIntentionPass call its collectInformation()
Map<com.intellij.lang.Language, MyRecordingAnnotator @NotNull []> annotatorsByLanguage = new HashMap<>();
annotatorsByLanguage.put(JavaLanguage.INSTANCE, new MyRecordingAnnotator[]{new MyComment1Annotator(), new MyComment2Annotator()});
MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
// both annos should produce their results
myDaemonCodeAnalyzer.restart();
DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way
useAnnotatorsIn(annotatorsByLanguage, () -> {
long deadline = System.currentTimeMillis() + 20_000;
while (!DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) {
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
if (System.currentTimeMillis() > deadline) {
fail("Too long waiting for daemon to start");
}
}
boolean tool1AnnoFound = false;
boolean tool2AnnoFound = false;
while (!tool1AnnoFound || !tool2AnnoFound) {
if (System.currentTimeMillis() > deadline) {
fail("Too long waiting for daemon to finish\n" + ThreadDumper.dumpThreadsToString());
}
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
tool1AnnoFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0,
myEditor.getDocument().getTextLength(),
info -> !MyComment1Annotator.comment1Text.equals(info.getDescription()));
tool2AnnoFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0,
myEditor.getDocument().getTextLength(),
info -> !MyComment2Annotator.comment2Text.equals(info.getDescription()));
}
MyComment1Annotator.stall1.set(false);
MyComment2Annotator.stall2.set(false);
while (DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) {
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
if (System.currentTimeMillis() > deadline+1_000) {
fail("Too long waiting for daemon to finish; stall1="+MyComment1Annotator.stall1+"; stall2="+MyComment2Annotator.stall2 +
"\n" + ThreadDumper.dumpThreadsToString());
}
Thread.yield();
}
});
}
}
@@ -2632,4 +2632,17 @@ public class DaemonRespondToChangesTest extends DaemonAnalyzerTestCase {
assertEmpty(highlightErrors());
}
public void testTypingErrorElementMustHighlightIt() {
ThreadingAssertions.assertEventDispatchThread();
configureByText(JavaFileType.INSTANCE, "class X { void f() { } }<caret>");
assertEmpty(highlightErrors());
makeEditorWindowVisible(new Point(0, 1000), myEditor);
type("/");
waitForDaemon(myProject, myEditor.getDocument());
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(getEditor().getDocument(), HighlightSeverity.ERROR, getProject());
assertNotEmpty(errors);
assertTrue(errors.toString().contains("'class' or 'interface' expected"));
}
}
@@ -48,6 +48,7 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
@@ -70,15 +71,14 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
List<HighlightInfo> fileLevel =
((DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(getProject())).getFileLevelHighlights(getProject(), getFile());
HighlightInfo info = assertOneElement(fileLevel);
assertEquals("top level", info.getDescription());
assertTrue(MyFileLevelAnnotator.isMy(info));
type("\n\n");
assertEmpty(highlightErrors());
fileLevel =
((DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(getProject())).getFileLevelHighlights(getProject(), getFile());
info = assertOneElement(fileLevel);
assertEquals("top level", info.getDescription());
assertTrue(MyFileLevelAnnotator.isMy(info));
type("//xxx"); //disable top level annotation
List<HighlightInfo> warnings = doHighlighting(HighlightSeverity.WARNING);
assertEmpty(warnings);
@@ -121,6 +121,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
iDidIt();
}
}
static boolean isMy(HighlightInfo info) {
return HighlightSeverity.WARNING.equals(info.getSeverity()) && "top level".equals(info.getDescription());
}
}
public void testAnnotatorMustNotSpecifyCrazyRangeForCreatedAnnotation() {
@@ -464,23 +467,25 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyStupidRepetitiveAnnotator()}, () -> runMyAnnotators());
}
public void testAnnotatorTryingToHighlightWarningAndErrorToTheSameElementMustFilterOutWarning() {
public void testDifferentAnnotatorsTryingToHighlightWarningAndErrorToTheSameElementMustNotInterfere() {
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyErrorAnnotator(), new MyWarningAnnotator()}, () -> {
configureFromFileText("My.java", "class My {}");
((EditorEx)getEditor()).getScrollPane().getViewport().setSize(new Dimension(1000,1000)); // whole file fit onscreen
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
HighlightInfo info = assertOneElement(infos);
MyErrorAnnotator.assertMy(info);
List<HighlightInfo> infos = ContainerUtil.sorted(doHighlighting(HighlightSeverity.WARNING), Comparator.comparing(HighlightInfo::getSeverity));
assertEquals(2, infos.size());
assertTrue(MyWarningAnnotator.isMy(infos.get(0)));
assertTrue(MyErrorAnnotator.isMy(infos.get(1)));
});
}
public void testAnnotatorTryingToHighlightInformationAndErrorToTheSameElementMustFilterOutInformation() {
public void testDifferentAnnotatorsTryingToHighlightInformationAndErrorToTheSameElementMustNotInterfere() {
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyErrorAnnotator(), new MyInfoAnnotator()}, () -> {
configureFromFileText("My.java", "class My {}");
((EditorEx)getEditor()).getScrollPane().getViewport().setSize(new Dimension(1000,1000)); // whole file fit onscreen
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.INFORMATION);
HighlightInfo info = assertOneElement(infos);
MyErrorAnnotator.assertMy(info);
List<HighlightInfo> infos = ContainerUtil.sorted(doHighlighting(HighlightSeverity.INFORMATION), Comparator.comparing(HighlightInfo::getSeverity));
assertEquals(2, infos.size());
assertTrue(MyInfoAnnotator.isMy(infos.get(0)));
assertTrue(MyErrorAnnotator.isMy(infos.get(1)));
});
}
@@ -489,8 +494,8 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
configureFromFileText("My.java", "class My {}");
((EditorEx)getEditor()).getScrollPane().getViewport().setSize(new Dimension(1000,1000)); // whole file fit onscreen
List<HighlightInfo> infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightSeverity.ERROR) && info.getDescription().equals("error2")));
assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightInfoType.SYMBOL_TYPE_SEVERITY) && info.getDescription().equals("symbol2")));
assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightSeverity.ERROR) && MyErrorAnnotator.isMy(info)));
assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightInfoType.SYMBOL_TYPE_SEVERITY) && MySymbolAnnotator.isMy(info)));
});
}
@@ -502,9 +507,8 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
iDidIt();
}
}
static void assertMy(HighlightInfo info) {
assertEquals(HighlightSeverity.ERROR, info.getSeverity());
assertEquals("error2", info.getDescription());
static boolean isMy(HighlightInfo info) {
return HighlightSeverity.ERROR.equals(info.getSeverity()) && "error2".equals(info.getDescription());
}
}
public static class MyWarningAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
@@ -515,6 +519,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
iDidIt();
}
}
static boolean isMy(HighlightInfo info) {
return HighlightSeverity.WARNING.equals(info.getSeverity()) && "warn2".equals(info.getDescription());
}
}
public static class MyInfoAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
@Override
@@ -524,6 +531,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
iDidIt();
}
}
static boolean isMy(HighlightInfo info) {
return HighlightSeverity.INFORMATION.equals(info.getSeverity()) && "info2".equals(info.getDescription());
}
}
public static class MySymbolAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
@Override
@@ -533,6 +543,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
iDidIt();
}
}
static boolean isMy(HighlightInfo info) {
return HighlightInfoType.SYMBOL_TYPE_SEVERITY.equals(info.getSeverity()) && "symbol2".equals(info.getDescription());
}
}
/**
@@ -563,7 +576,7 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase
private static volatile boolean FIX_ENABLED;
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element.getText().equals("hello")) {
if (element.getText().equals("hello") && !(element instanceof PsiFile)) {
holder.newAnnotation(HighlightSeverity.ERROR, "i hate it")
.newFix(new DeleteElementFix(element) {
@Override
@@ -196,11 +196,16 @@ public class AnnotationHolderImpl extends SmartList<Annotation> implements Annot
@Override
public @NotNull AnnotationBuilder newAnnotation(@NotNull HighlightSeverity severity, @NotNull @Nls String message) {
return new B(this, severity, message, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator));
return createBuilder(severity, message, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator));
}
@Override
public @NotNull AnnotationBuilder newSilentAnnotation(@NotNull HighlightSeverity severity) {
return new B(this, severity, null, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator));
return createBuilder(severity, null, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator));
}
@NotNull
protected B createBuilder(@NotNull HighlightSeverity severity, @Nls String message, PsiElement currentElement, Object currentAnnotator) {
return new B(this, severity, message, currentElement, currentAnnotator);
}
PsiElement myCurrentElement;
@@ -0,0 +1,262 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl;
import com.intellij.codeInsight.daemon.AnnotatorStatisticsCollector;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder;
import com.intellij.codeInspection.ex.GlobalInspectionContextBase;
import com.intellij.concurrency.JobLauncher;
import com.intellij.diagnostic.PluginException;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageAnnotators;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.Annotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ProperTextRange;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.PairProcessor;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.CollectionFactory;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashingStrategy;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
final class AnnotatorRunner {
private static final Logger LOG = Logger.getInstance(AnnotatorRunner.class);
private static final Annotator TOMB_STONE = (__, __1) -> { };
private final Project myProject;
private final PsiFile myPsiFile;
private final HighlightInfoHolder myHighlightInfoHolder;
private final HighlightingSession myHighlightingSession;
private final DumbService myDumbService;
private final boolean myBatchMode;
private boolean myDumb;
private final AnnotatorStatisticsCollector myAnnotatorStatisticsCollector = new AnnotatorStatisticsCollector();
private final List<HighlightInfo> results = Collections.synchronizedList(new ArrayList<>());
AnnotatorRunner(@NotNull PsiFile psiFile, boolean batchMode, @NotNull HighlightInfoHolder holder, @NotNull HighlightingSession highlightingSession) {
myProject = psiFile.getProject();
myPsiFile = psiFile;
myHighlightInfoHolder = holder;
myHighlightingSession = highlightingSession;
myDumbService = DumbService.getInstance(myProject);
myBatchMode = batchMode;
}
boolean runAnnotatorsAsync(@NotNull List<? extends PsiElement> inside, @NotNull List<? extends PsiElement> outside, @NotNull BooleanSupplier runnable) {
ApplicationManager.getApplication().assertIsNonDispatchThread();
DaemonProgressIndicator indicator = GlobalInspectionContextBase.assertUnderDaemonProgress();
myDumb = myDumbService.isDumb();
// TODO move inside Divider to calc only once
List<PsiElement> insideThenOutside = ContainerUtil.concat(inside, outside);
Map<Annotator, Set<Language>> supportedLanguages = calcSupportedLanguages(insideThenOutside);
PairProcessor<Annotator, JobLauncher.QueueController<? super Annotator>> processor = (annotator, __) ->
ApplicationManagerEx.getApplicationEx().tryRunReadAction(() -> runAnnotator(annotator, insideThenOutside, supportedLanguages));
boolean result = JobLauncher.getInstance().procInOrderAsync(indicator, supportedLanguages.size(), processor, addToQueue -> {
for (Annotator annotator : supportedLanguages.keySet()) {
addToQueue.enqueue(annotator);
}
addToQueue.finish();
return runnable.getAsBoolean();
});
myAnnotatorStatisticsCollector.reportAnalysisFinished(myProject, myHighlightInfoHolder.getAnnotationSession(), myPsiFile);
return result;
}
@NotNull
private Map<Annotator, Set<Language>> calcSupportedLanguages(@NotNull List<PsiElement> elements) {
Map<Annotator, Set<Language>> map = CollectionFactory.createCustomHashingStrategyMap(new HashingStrategy<>() {
@Override
public int hashCode(Annotator object) {
return object.getClass().hashCode();
}
@Override
public boolean equals(Annotator o1, Annotator o2) {
return o1 == null || o2 == null ? o1==o2 : o1.getClass().equals(o2.getClass());
}
});
Set<Language> languages = new HashSet<>();
for (PsiElement element : elements) {
Language language = element.getLanguage();
addDialects(language, languages);
}
for (Language language : languages) {
List<Annotator> templates = LanguageAnnotators.INSTANCE.allForLanguageOrAny(language);
for (Annotator template : templates) {
Set<Language> supportedLanguages = map.get(template);
if (supportedLanguages == null) {
supportedLanguages = new HashSet<>();
map.put(cloneTemplate(template), supportedLanguages);
}
supportedLanguages.add(language);
}
}
return map;
}
private static void addDialects(@NotNull Language language, @NotNull Set<? super Language> outProcessedLanguages) {
if (outProcessedLanguages.add(language)) {
Collection<Language> dialects = language.getTransitiveDialects();
outProcessedLanguages.addAll(dialects);
}
}
private void runAnnotator(@NotNull Annotator annotator,
@NotNull List<? extends PsiElement> insideThenOutside,
@NotNull Map<Annotator, Set<Language>> supportedLanguages) {
Set<Language> supported = supportedLanguages.get(annotator);
if (supported.isEmpty()) {
return;
}
AtomicReference<PsiElement> currentElement = new AtomicReference<>();
// create AnnotationHolderImpl for each Annotator to make it immutable thread-safe converter to the corresponding HighlightInfo
AnnotationHolderImpl annotationHolder = new AnnotationHolderImpl(myHighlightInfoHolder.getAnnotationSession(), myBatchMode) {
@Override
public boolean add(Annotation annotation) {
myAnnotatorStatisticsCollector.reportAnnotationProduced(annotator, annotation);
super.add(annotation);
return true;
}
@Override
protected @NotNull B createBuilder(@NotNull HighlightSeverity severity, @Nls String message, PsiElement __, Object ___) {
return super.createBuilder(severity, message, currentElement.get(), annotator);
}
};
HighlightersRecycler emptyElementRecycler = new HighlightersRecycler(); // no need to call incinerate/release because it's always empty
for (PsiElement element : insideThenOutside) {
if (!supported.contains(element.getLanguage())) {
continue;
}
if (myDumb && !DumbService.isDumbAware(annotator)) {
continue;
}
ProgressManager.checkCanceled();
currentElement.set(element);
int sizeBefore = annotationHolder.size();
annotator.annotate(element, annotationHolder);
int sizeAfter = annotationHolder.size();
List<HighlightInfo> newInfos;
if (sizeBefore == sizeAfter) {
newInfos = List.of();
}
else {
newInfos = new ArrayList<>(sizeAfter - sizeBefore);
for (int i = sizeBefore; i < sizeAfter; i++) {
Annotation annotation = annotationHolder.get(i);
HighlightInfo info = HighlightInfo.fromAnnotation(annotator.getClass(), annotation, myBatchMode);
info.setGroup(-1); // prevent DefaultHighlightProcessor from removing this info, we want to control it ourselves via `psiElementVisited` below
addConvertedToHostInfo(info, newInfos);
if (LOG.isDebugEnabled()) {
LOG.debug("runAnnotator annotation="+annotation+" -> "+newInfos);
}
}
results.addAll(newInfos);
}
Document hostDocument = myPsiFile.getFileDocument();
if (hostDocument instanceof DocumentWindow w) hostDocument = w.getDelegate();
HighlightInfoUpdater.getInstance(myProject).psiElementVisited(annotator.getClass(), element, newInfos, hostDocument, myPsiFile, myProject,
emptyElementRecycler, myHighlightingSession);
}
}
private static void addPatchedInfos(@NotNull HighlightInfo info,
@NotNull PsiFile injectedPsi,
@NotNull DocumentWindow documentWindow,
@NotNull InjectedLanguageManager injectedLanguageManager,
@NotNull Consumer<? super HighlightInfo> outInfos) {
ProperTextRange infoRange = new ProperTextRange(info.startOffset, info.endOffset);
List<TextRange> editables = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, infoRange);
for (TextRange editable : editables) {
TextRange hostRange = documentWindow.injectedToHost(editable);
boolean isAfterEndOfLine = info.isAfterEndOfLine();
if (isAfterEndOfLine) {
// convert injected afterEndOfLine to either host's afterEndOfLine or not-afterEndOfLine highlight of the injected fragment boundary
int hostEndOffset = hostRange.getEndOffset();
int lineNumber = documentWindow.getDelegate().getLineNumber(hostEndOffset);
int hostLineEndOffset = documentWindow.getDelegate().getLineEndOffset(lineNumber);
if (hostEndOffset < hostLineEndOffset) {
// convert to non-afterEndOfLine
isAfterEndOfLine = false;
hostRange = new ProperTextRange(hostRange.getStartOffset(), hostEndOffset+1);
}
}
HighlightInfo patched =
new HighlightInfo(info.forcedTextAttributes, info.forcedTextAttributesKey, info.type,
hostRange.getStartOffset(), hostRange.getEndOffset(),
info.getDescription(), info.getToolTip(), info.getSeverity(), isAfterEndOfLine, null,
false, 0, info.getProblemGroup(), info.toolId, info.getGutterIconRenderer(), info.getGroup(), info.unresolvedReference);
patched.setHint(info.hasHint());
info.findRegisteredQuickFix((descriptor, quickfixTextRange) -> {
List<TextRange> editableQF = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, quickfixTextRange);
for (TextRange editableRange : editableQF) {
TextRange hostEditableRange = documentWindow.injectedToHost(editableRange);
patched.registerFix(descriptor.getAction(), descriptor.myOptions, descriptor.getDisplayName(), hostEditableRange, descriptor.myKey);
}
return null;
});
patched.markFromInjection();
outInfos.accept(patched);
}
}
private void addConvertedToHostInfo(@NotNull HighlightInfo info, @NotNull List<? super HighlightInfo> newInfos) {
Document document = myPsiFile.getFileDocument();
if (document instanceof DocumentWindow window) {
addPatchedInfos(info, myPsiFile, window, InjectedLanguageManager.getInstance(myProject), patched -> newInfos.add(patched));
}
else {
newInfos.add(info);
}
}
private @NotNull List<Annotator> cloneTemplates(@NotNull Collection<? extends Annotator> templates) {
List<Annotator> result = new ArrayList<>(templates.size());
for (Annotator template : templates) {
Annotator annotator = cloneTemplate(template);
if (annotator == null) continue;
result.add(annotator);
myAnnotatorStatisticsCollector.reportNewAnnotatorCreated(annotator);
}
return result;
}
private static Annotator cloneTemplate(@NotNull Annotator template) {
Annotator annotator;
try {
annotator = ReflectionUtil.newInstance(template.getClass());
}
catch (Exception e) {
LOG.error(PluginException.createByClass(e, template.getClass()));
return null;
}
return annotator;
}
@NotNull
List<HighlightInfo> getResults() {
return results;
}
}
@@ -126,7 +126,7 @@ public final class BackgroundUpdateHighlightersUtil {
Set<HighlightInfo> infoSet = new HashSet<>(filteredInfos);
Processor<HighlightInfo> processor = info -> {
if (info.getGroup() == group) {
if (info.getGroup() == group && !info.isFromAnnotator()) { // ignore annotators, they are applied via HighlightInfoUpdater
RangeHighlighterEx highlighter = info.getHighlighter();
int hiStart = highlighter.getStartOffset();
int hiEnd = highlighter.getEndOffset();
@@ -165,7 +165,7 @@ public final class BackgroundUpdateHighlightersUtil {
if (UpdateHighlightersUtil.isWarningCoveredByError(info, severityRegistrar, overlappingIntervals)) {
return true;
}
if (info.getStartOffset() < priorityRange.getStartOffset() || info.getEndOffset() > priorityRange.getEndOffset()) {
if ((info.getStartOffset() < priorityRange.getStartOffset() || info.getEndOffset() > priorityRange.getEndOffset()) && !info.isFromAnnotator()) {
// have to create RangeHighlighter later, to avoid exposing them to the markup model immediately,
// thus messing the HighlightInfo.getStartOffset() leading to "sweep generator supplied infos in a wrong order" exception
infosToCreateHighlightersFor.add(info);
@@ -242,6 +242,10 @@ public final class BackgroundUpdateHighlightersUtil {
List<HighlightInfo> fileLevelHighlights = new ArrayList<>();
List<HighlightInfo> infosToCreateHighlightersFor = new ArrayList<>(filteredInfos.size());
SweepProcessor.sweep(generator, (__, info, atStart, overlappingIntervals) -> {
//if (info.isFromAnnotator()) {
// // annotator infos are handled by HighlightInfoUpdater separately
// return true;
//}
if (!atStart) {
return true;
}
@@ -293,7 +297,7 @@ public final class BackgroundUpdateHighlightersUtil {
infoStartOffset = Math.min(infoStartOffset, infoEndOffset);
}
if (infoEndOffset == infoStartOffset && !info.isAfterEndOfLine()) {
if (infoEndOffset == docLength) return; // empty highlighter beyond file boundaries
if (infoEndOffset == docLength) return; // empty highlighter beyond file boundaries
infoEndOffset++; //show something in case of empty HighlightInfo
}
@@ -5,14 +5,7 @@ import com.intellij.codeInsight.daemon.AnnotatorStatisticsCollector;
import com.intellij.codeInsight.daemon.impl.analysis.ErrorQuickFixProvider;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder;
import com.intellij.codeInsight.highlighting.HighlightErrorFilter;
import com.intellij.diagnostic.PluginException;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageAnnotators;
import com.intellij.lang.LanguageUtil;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
@@ -22,41 +15,27 @@ import com.intellij.psi.FileViewProvider;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.ConcurrentFactoryMap;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware {
private static final Logger LOG = Logger.getInstance(DefaultHighlightVisitor.class);
private AnnotationHolderImpl myAnnotationHolder;
private final Map<Language, List<Annotator>> myAnnotators = ConcurrentFactoryMap.createMap(language -> createAnnotators(language));
private final Project myProject;
private final boolean myHighlightErrorElements;
private final boolean myRunAnnotators;
private final DumbService myDumbService;
private HighlightInfoHolder myHolder;
private final boolean myBatchMode;
private boolean myDumb;
private final AnnotatorStatisticsCollector myAnnotatorStatisticsCollector = new AnnotatorStatisticsCollector();
@SuppressWarnings("UnusedDeclaration")
DefaultHighlightVisitor(@NotNull Project project) {
this(project, true, true, false);
this(project, true, false);
}
DefaultHighlightVisitor(@NotNull Project project,
boolean highlightErrorElements,
boolean runAnnotators,
boolean batchMode) {
myProject = project;
myHighlightErrorElements = highlightErrorElements;
myRunAnnotators = runAnnotators;
myDumbService = DumbService.getInstance(project);
myBatchMode = batchMode;
}
@@ -70,40 +49,19 @@ final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware {
boolean updateWholeFile,
@NotNull HighlightInfoHolder holder,
@NotNull Runnable action) {
myDumb = myDumbService.isDumb();
myHolder = holder;
myAnnotationHolder = new AnnotationHolderImpl(holder.getAnnotationSession(), myBatchMode) {
@Override
void queueToUpdateIncrementally() {
if (!isEmpty()) {
myAnnotatorStatisticsCollector.reportAnnotationProduced(myCurrentAnnotator, get(0));
for (int i = 0; i < size(); i++) {
Annotation annotation = get(i);
holder.add(HighlightInfo.fromAnnotation(myCurrentAnnotator.getClass(), annotation, myBatchMode));
}
clear();
}
}
};
try {
action.run();
myAnnotationHolder.assertAllAnnotationsCreated();
}
finally {
myAnnotators.clear();
myHolder = null;
myAnnotationHolder = null;
myAnnotatorStatisticsCollector.reportAnalysisFinished(myProject, holder.getAnnotationSession(), file);
}
return true;
}
@Override
public void visit(@NotNull PsiElement element) {
if (myRunAnnotators) {
runAnnotators(element);
}
if (element instanceof PsiErrorElement && myHighlightErrorElements) {
visitErrorElement((PsiErrorElement)element);
}
@@ -112,26 +70,7 @@ final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware {
@SuppressWarnings("CloneDoesntCallSuperClone")
@Override
public @NotNull HighlightVisitor clone() {
return new DefaultHighlightVisitor(myProject, myHighlightErrorElements, myRunAnnotators, myBatchMode);
}
private void runAnnotators(@NotNull PsiElement element) {
List<Annotator> annotators = myAnnotators.get(element.getLanguage());
if (!annotators.isEmpty()) {
AnnotationHolderImpl holder = myAnnotationHolder;
holder.myCurrentElement = element;
for (Annotator annotator : annotators) {
if (!myDumb || DumbService.isDumbAware(annotator)) {
ProgressManager.checkCanceled();
holder.myCurrentAnnotator = annotator;
annotator.annotate(element, holder);
// assume that annotator is done messing with just created annotations after its annotate() method completed,
// so we can start applying them incrementally at last
// (but not sooner, thanks to awfully racey Annotation.setXXX() API)
holder.queueToUpdateIncrementally();
}
}
}
return new DefaultHighlightVisitor(myProject, myHighlightErrorElements, myBatchMode);
}
private void visitErrorElement(@NotNull PsiErrorElement element) {
@@ -159,27 +98,6 @@ final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware {
return info;
}
private @NotNull List<Annotator> cloneTemplates(@NotNull Collection<? extends Annotator> templates) {
List<Annotator> result = new ArrayList<>(templates.size());
for (Annotator template : templates) {
Annotator annotator;
try {
annotator = ReflectionUtil.newInstance(template.getClass());
}
catch (Exception e) {
LOG.error(PluginException.createByClass(e, template.getClass()));
continue;
}
result.add(annotator);
myAnnotatorStatisticsCollector.reportNewAnnotatorCreated(annotator);
}
return result;
}
private @NotNull List<Annotator> createAnnotators(@NotNull Language language) {
return cloneTemplates(LanguageAnnotators.INSTANCE.allForLanguageOrAny(language));
}
private static @NotNull HighlightInfo.Builder createErrorElementInfoWithoutFixes(@NotNull PsiErrorElement element) {
TextRange range = element.getTextRange();
String errorDescription = element.getErrorDescription();
@@ -56,6 +56,7 @@ import org.jetbrains.annotations.TestOnly;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -75,12 +76,14 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP
final @NotNull ProperTextRange myPriorityRange;
final List<HighlightInfo> myHighlights = new ArrayList<>();
final List<HighlightInfo> myAnnotatorHighlights = Collections.synchronizedList(new ArrayList<>()); // have to store them separately to avoid double call setToEditor()
protected volatile boolean myHasErrorElement;
private volatile boolean myHasErrorSeverity;
private volatile boolean myErrorFound;
final EditorColorsScheme myGlobalScheme;
private volatile @NotNull Supplier<? extends @NotNull List<HighlightVisitor>> myHighlightVisitorProducer = this::cloneHighlightVisitors;
private boolean myRunAnnotators = true;
GeneralHighlightingPass(@NotNull PsiFile file,
@NotNull Document document,
@@ -286,35 +289,47 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP
int chunkSize = Math.max(1, (elements1.size()+elements2.size()) / 100); // one percent precision is enough
boolean success = analyzeByVisitors(visitors, holder, 0, () -> {
LongStack nestedRange = new LongStack();
Stack<List<HighlightInfo>> nestedInfos = new Stack<>();
BooleanSupplier runnable = () -> {
boolean success = analyzeByVisitors(visitors, holder, 0, () -> {
LongStack nestedRange = new LongStack();
Stack<List<HighlightInfo>> nestedInfos = new Stack<>();
try (var ignored = HIGHLIGHTING_PERFORMANCE_ASSERT.runPass()) {
runVisitors(elements1, ranges1, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors,
try (var ignored = HIGHLIGHTING_PERFORMANCE_ASSERT.runPass()) {
runVisitors(elements1, ranges1, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors,
nestedRange, nestedInfos);
}
boolean priorityIntersectionHasElements = myPriorityRange.intersectsStrict(myRestrictRange);
if ((!elements1.isEmpty() || !insideResult.isEmpty()) || priorityIntersectionHasElements) { // do not apply when there were no elements to highlight
myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), insideResult, myPriorityRange, myRestrictRange, getId());
}
runVisitors(elements2, ranges2, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors,
nestedRange, nestedInfos);
});
// there can be extra highlights generated in PostHighlightVisitor
List<HighlightInfo> postInfos;
synchronized (holder) {
postInfos = new ArrayList<>(holder.size());
for (int j = 0; j < holder.size(); j++) {
HighlightInfo info = holder.get(j);
postInfos.add(info);
insideResult.add(info);
}
}
boolean priorityIntersectionHasElements = myPriorityRange.intersectsStrict(myRestrictRange);
if ((!elements1.isEmpty() || !insideResult.isEmpty()) || priorityIntersectionHasElements) { // do not apply when there were no elements to highlight
myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), insideResult, myPriorityRange, myRestrictRange, getId());
}
runVisitors(elements2, ranges2, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors,
nestedRange, nestedInfos);
});
// there can be extra highlights generated in PostHighlightVisitor
List<HighlightInfo> postInfos;
synchronized (holder) {
postInfos = new ArrayList<>(holder.size());
for (int j = 0; j < holder.size(); j++) {
HighlightInfo info = holder.get(j);
postInfos.add(info);
insideResult.add(info);
}
myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(),
postInfos, getFile().getTextRange(), getFile().getTextRange(), POST_UPDATE_ALL);
return success;
};
AnnotatorRunner annotatorRunner = myRunAnnotators ? new AnnotatorRunner(getFile(), false, holder, myHighlightingSession) : null;
boolean result;
if (annotatorRunner == null) {
result = runnable.getAsBoolean();
}
myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(),
postInfos, getFile().getTextRange(), getFile().getTextRange(), POST_UPDATE_ALL);
return success;
else {
result = annotatorRunner.runAnnotatorsAsync(elements1, elements2, runnable);
outsideResult.addAll(annotatorRunner.getResults());
}
return result;
}
private boolean analyzeByVisitors(HighlightVisitor @NotNull [] visitors,
@@ -399,7 +414,7 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP
// if this highlight info range is contained inside the current element range we are visiting
// that means we can clear this highlight as soon as visitors won't produce any highlights during visiting the same range next time.
// We also know that we can remove a syntax error element.
info.setVisitingTextRange(myFile, myDocument, elementRange);
info.setVisitingTextRange(getFile(), myDocument, elementRange);
infosForThisRange.add(info);
}
holder.clear();
@@ -487,7 +502,8 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP
synchronized (this) {
added = super.add(info);
}
if (info != null && added) {
// annotator infos are handled by HighlightInfoUpdater separately
if (info != null && added/* && !info.isFromAnnotator()*/) {
queueInfoToUpdateIncrementally(info, info.getGroup() == 0 ? Pass.UPDATE_ALL : info.getGroup());
}
return added;
@@ -617,4 +633,7 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP
private static @Nls String getPresentableNameText() {
return AnalysisBundle.message("pass.syntax");
}
void setRunAnnotators(boolean run) {
myRunAnnotators = run;
}
}
@@ -13,10 +13,7 @@ import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.codeInspection.ex.LocalInspectionToolWrapper;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.ExternalAnnotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.lang.annotation.ProblemGroup;
import com.intellij.lang.annotation.*;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.openapi.diagnostic.Logger;
@@ -322,7 +319,7 @@ public class HighlightInfo implements Segment {
}
public @Nullable @NonNls String getInspectionToolId() {
return toolId instanceof String ? (String)toolId : null;
return toolId instanceof String inspectionToolShortName ? inspectionToolShortName : null;
}
private boolean isFlagSet(@FlagConstant byte mask) {
@@ -1074,4 +1071,7 @@ public class HighlightInfo implements Segment {
void setUnresolvedReferenceQuickFixesComputed() {
setFlag(UNRESOLVED_REFERENCE_QUICK_FIXES_COMPUTED_MASK, true);
}
boolean isFromAnnotator() {
return toolId instanceof Class<?> c && Annotator.class.isAssignableFrom(c);
}
}
@@ -26,6 +26,7 @@ import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedFileViewProvider;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.SearchScope;
@@ -181,16 +182,15 @@ final class HighlightInfoUpdater {
@NotNull HighlightersRecycler invalidElementRecycler,
@NotNull HighlightingSession session) {
List<? extends HighlightInfo> oldInfos = getInfosForVisitedPsi(psiFile, toolId, visitedPsiElement);
synchronized (oldInfos) {
if (LOG.isDebugEnabled()) {
LOG.debug("psiElementVisited: " + visitedPsiElement+ " in "+psiFile+" injected in "+InjectedLanguageManager.getInstance(project).injectedToHost(psiFile, psiFile.getTextRange())+
"; tool:" + toolId + "; infos:" + newInfos+ "; oldInfos:" + oldInfos + "; document:" + hostDocument);
}
if (!oldInfos.isEmpty() || !newInfos.isEmpty()) {
MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(hostDocument, project, true);
setHighlightersInRange(newInfos, oldInfos, markup, session, invalidElementRecycler);
}
if (LOG.isDebugEnabled()) {
LOG.debug("psiElementVisited: " + visitedPsiElement+ " in "+psiFile+
(psiFile.getViewProvider() instanceof InjectedFileViewProvider ?
" injected in " + InjectedLanguageManager.getInstance(project).injectedToHost(psiFile, psiFile.getTextRange()) : "") +
"; tool:" + toolId + "; infos:" + newInfos + "; oldInfos:" + oldInfos + "; document:" + hostDocument);
}
if (!oldInfos.isEmpty() || !newInfos.isEmpty()) {
MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(hostDocument, project, true);
setHighlightersInRange(newInfos, oldInfos, markup, session, invalidElementRecycler);
}
// store back only after markup model changes are applied to avoid PCE thrown in the middle leaving corrupted data behind
putInfosForVisitedPsi(psiFile, toolId, visitedPsiElement, newInfos);
@@ -116,8 +116,9 @@ public final class HighlightVisitorBasedInspection extends GlobalSimpleInspectio
for (GeneralHighlightingPass gpass : gpasses) {
gpass.setHighlightVisitorProducer(() -> {
gpass.incVisitorUsageCount(1);
return List.of(new DefaultHighlightVisitor(project, highlightErrorElements, runAnnotators, true));
return List.of(new DefaultHighlightVisitor(project, highlightErrorElements, true));
});
gpass.setRunAnnotators(runAnnotators);
}
}
@@ -6,6 +6,7 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.util.PairProcessor;
import com.intellij.util.Processor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -89,4 +90,18 @@ public abstract class JobLauncher {
* Use {@link com.intellij.openapi.application.Application#executeOnPooledThread(Runnable)} instead
*/
public abstract @NotNull Job<Void> submitToJobThread(final @NotNull Runnable action, @Nullable Consumer<? super Future<?>> onDoneCallback);
@ApiStatus.Internal
public <T> boolean procInOrderAsync(@NotNull ProgressIndicator progress,
int maxQueueSize,
@NotNull PairProcessor<? super T, ? super QueueController<? super T>> thingProcessor,
@NotNull Processor<? super QueueController<? super T>> otherActions) throws ProcessCanceledException {
return false;
}
public interface QueueController<T> {
void enqueue(T element);
void dropEverythingAndPanic();
void finish();
}
}
@@ -3,7 +3,9 @@ package com.intellij.core;
import com.intellij.concurrency.Job;
import com.intellij.concurrency.JobLauncher;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.util.PairProcessor;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -34,4 +36,5 @@ public class CoreJobLauncher extends JobLauncher {
}
return Job.nullJob();
}
}
@@ -3,6 +3,7 @@ package com.intellij.openapi.editor.impl;
import com.intellij.codeInsight.daemon.GutterMark;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
@@ -12,6 +13,7 @@ import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.util.BitUtil;
import com.intellij.util.Consumer;
import org.intellij.lang.annotations.MagicConstant;
@@ -79,6 +81,13 @@ sealed class RangeHighlighterImpl extends RangeMarkerImpl implements RangeHighli
myModel = model;
registerInTree(start, end, greedyToLeft, greedyToRight, layer);
if (LOG.isDebugEnabled()) {
LOG.debug("create: " + this);
if (getTextRange().equals(new TextRange(16, 22))) {
//new RuntimeException().printStackTrace();
int i = 0;
}
}
}
private boolean isFlagSet(@Flag byte mask) {
@@ -443,8 +452,16 @@ sealed class RangeHighlighterImpl extends RangeMarkerImpl implements RangeHighli
getMarkupModel().removeHighlighter(this);
}
private static final Logger LOG = Logger.getInstance(RangeHighlighterImpl.class);
@Override
public void dispose() {
if (LOG.isDebugEnabled()) {
if (getTextRange().equals(new TextRange(16, 22))) {
//new RuntimeException().printStackTrace();
int i = 0;
}
LOG.debug("dispose: "+this);
}
super.dispose();
GutterIconRenderer renderer = getGutterIconRenderer();
if (renderer instanceof Disposable disposableRenderer) {
@@ -528,13 +528,16 @@ public final class DaemonCodeAnalyzerImpl extends DaemonCodeAnalyzerEx
}
catch (Throwable e) {
Throwable unwrapped = ExceptionUtilRt.unwrapException(e, ExecutionException.class);
LOG.debug("doRunPasses() thrown " + ExceptionUtil.getThrowableText(unwrapped));
if (progress.isCanceled() && progress.isRunning()) {
unwrapped.addSuppressed(new RuntimeException("Daemon progress was canceled unexpectedly: " + progress));
}
ExceptionUtil.rethrow(unwrapped);
}
finally {
progress.cancel();
if (!progress.isCanceled()) {
((DaemonProgressIndicator)progress).cancel("Cancel after highlighting. threads:\n"+ThreadDumper.dumpThreadsToString());
}
waitForTermination();
}
return null;
@@ -44,11 +44,11 @@ public final class DefaultHighlightInfoProcessor extends HighlightInfoProcessor
Document document = session.getDocument();
long modificationStamp = document.getModificationStamp();
TextRange priorityIntersection = priorityRange.intersection(restrictRange);
TextEditorHighlightingPass showAutoImportPass = editor == null ? null : getOrCreateShowAutoImportPass(editor, psiFile, session.getProgressIndicator());
MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true);
if (priorityIntersection != null) {
MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true);
BackgroundUpdateHighlightersUtil.setHighlightersInRange(priorityIntersection, new ArrayList<HighlightInfo>(infos), markupModel, groupId, session);
}
TextEditorHighlightingPass showAutoImportPass = editor == null ? null : getOrCreateShowAutoImportPass(editor, psiFile, session.getProgressIndicator());
ApplicationManager.getApplication().invokeLater(() -> {
if (editor != null && !editor.isDisposed() && modificationStamp == document.getModificationStamp()) {
// usability: show auto import popup as soon as possible
@@ -132,7 +132,7 @@ public final class DefaultHighlightInfoProcessor extends HighlightInfoProcessor
@NotNull HighlightingSession session) {
List<RangeHighlighterEx> toRemove = new ArrayList<>();
DaemonCodeAnalyzerEx.processHighlights(document, project, null, TextRangeScalarUtil.startOffset(range), TextRangeScalarUtil.endOffset(range), existing -> {
if (existing.getGroup() == Pass.UPDATE_ALL && TextRangeScalarUtil.startOffset(range) == existing.getVisitingTextRange().getStartOffset() && TextRangeScalarUtil.endOffset(range) == existing.getVisitingTextRange().getEndOffset()) {
if (existing.getGroup() == Pass.UPDATE_ALL && !existing.isFromAnnotator() && TextRangeScalarUtil.startOffset(range) == existing.getVisitingTextRange().getStartOffset() && TextRangeScalarUtil.endOffset(range) == existing.getVisitingTextRange().getEndOffset()) {
if (infos != null) {
for (HighlightInfo created : infos) {
if (existing.equalsByActualOffset(created)) return true;
@@ -57,6 +57,13 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass {
myReducedRanges = reducedRanges;
}
@Override
public @NotNull List<HighlightInfo> getInfos() {
synchronized (myHighlights) {
return ContainerUtil.concat(myHighlights, myAnnotatorHighlights);
}
}
@Override
protected @NotNull String getPresentableName() {
return IdeBundle.message("highlighting.pass.injected.presentable.name");
@@ -92,10 +99,10 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass {
}));
synchronized (myHighlights) {
// all infos for the "injected fragment for the host which is inside" are indeed inside
// all infos for the "injected fragment for the host which is inside" are inside indeed,
// but some infos for the "injected fragment for the host which is outside" can be still inside
if (resultOutside.isEmpty()) {
// apply only result (by default apply command) and only within inside
// apply only the result inside
myHighlights.addAll(resultInside);
myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), myHighlights, myRestrictRange, myRestrictRange, getId());
}
@@ -230,16 +237,23 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass {
}
HighlightInfoHolder holder = createInfoHolder(injectedPsi, documentWindow, injectedLanguageManager, outInfos);
runHighlightVisitorsForInjected(injectedPsi, holder);
highlightInjectedSyntax(injectedPsi, places, outInfos);
List<PsiElement> elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength());
if (!isDumbMode()) {
List<HighlightInfo> todos = new ArrayList<>();
highlightTodos(injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), myPriorityRange, todos, todos);
for (HighlightInfo info : todos) {
addPatchedInfos(info, injectedPsi, documentWindow, injectedLanguageManager, outInfos);
AnnotatorRunner annotatorRunner = new AnnotatorRunner(injectedPsi, false, holder, myHighlightingSession);
annotatorRunner.runAnnotatorsAsync(elements, List.of(), () -> {
runHighlightVisitorsForInjected(injectedPsi, holder, elements);
highlightInjectedSyntax(injectedPsi, places, outInfos);
if (!isDumbMode()) {
List<HighlightInfo> todos = new ArrayList<>();
highlightTodos(injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), myPriorityRange, todos, todos);
for (HighlightInfo info : todos) {
addPatchedInfos(info, injectedPsi, documentWindow, injectedLanguageManager, outInfos);
}
}
}
return true;
});
myAnnotatorHighlights.addAll(annotatorRunner.getResults());
}
private static void addPatchedInfos(@NotNull HighlightInfo info,
@@ -285,10 +299,9 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass {
}
}
private void runHighlightVisitorsForInjected(@NotNull PsiFile injectedPsi, @NotNull HighlightInfoHolder holder) {
private void runHighlightVisitorsForInjected(@NotNull PsiFile injectedPsi, @NotNull HighlightInfoHolder holder, List<PsiElement> elements) {
HighlightVisitor[] filtered = getHighlightVisitors(injectedPsi);
try {
List<PsiElement> elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength());
for (HighlightVisitor visitor : filtered) {
visitor.analyze(injectedPsi, true, holder, () -> {
for (PsiElement element : elements) {
@@ -179,7 +179,7 @@ class InspectionRunner {
});
return true;
};
if (!((JobLauncherImpl)JobLauncher.getInstance()).procInOrderAsync(new SensitiveProgressWrapper(myProgress), initSize, contextProcessor, addToQueue -> {
if (!JobLauncher.getInstance().procInOrderAsync(new SensitiveProgressWrapper(myProgress), initSize, contextProcessor, addToQueue -> {
// have to do all this even for empty elements, to perform correct cleanup/inspectionFinished
if (init.isEmpty()) {
addToQueue.finish();
@@ -189,15 +189,16 @@ class InspectionRunner {
addToQueue.enqueue(context);
}
}
reportIdsOfInspectionsReportedAnyProblemToFUS(init);
if (myInspectInjected && InjectionUtils.shouldInspectInjectedFiles(myPsiFile)) {
// we don't run whole-file tools on injected fragments
List<LocalInspectionToolWrapper> localTools = ContainerUtil.filter(toolWrappers, t -> !t.runForWholeFile());
inspectInjectedPsi(session, localTools, injectedContexts, applyIncrementallyCallback,
contextFinishedCallback, enabledToolsPredicate, addToInjectedQueue ->
return inspectInjectedPsi(session, localTools, injectedContexts, applyIncrementallyCallback,
contextFinishedCallback, enabledToolsPredicate, addToInjectedQueue ->
getInjectedWithHosts(ContainerUtil.concat(restrictedInside, restrictedOutside), addToInjectedQueue));
}
reportIdsOfInspectionsReportedAnyProblemToFUS(init);
return true;
})) {
throw new ProcessCanceledException();
}
@@ -418,7 +419,7 @@ class InspectionRunner {
@NotNull ApplyIncrementallyCallback addDescriptorIncrementallyCallback,
@NotNull Consumer<? super InspectionContext> contextFinishedCallback,
@Nullable Condition<? super LocalInspectionToolWrapper> enabledToolsPredicate,
@NotNull Consumer<? super JobLauncherImpl.QueueController<? super Pair<PsiFile, PsiElement>>> otherActions) {
@NotNull Processor<? super JobLauncherImpl.QueueController<? super Pair<PsiFile, PsiElement>>> otherActions) {
PairProcessor<? super Pair<PsiFile, PsiElement>, JobLauncherImpl.QueueController<? super Pair<PsiFile, PsiElement>>> injectedProcessor = (pair,__) -> {
executeInImpatientReadAction(() -> {
PsiFile injectedPsi = pair.getFirst();
@@ -495,8 +496,8 @@ class InspectionRunner {
}
}
private void getInjectedWithHosts(@NotNull List<? extends PsiElement> elements,
@NotNull JobLauncherImpl.QueueController<? super Pair<PsiFile, PsiElement>> addToQueue) {
private boolean getInjectedWithHosts(@NotNull List<? extends PsiElement> elements,
@NotNull JobLauncherImpl.QueueController<? super Pair<PsiFile, PsiElement>> addToQueue) {
Map<PsiFile, PsiElement> injectedToHost = createInjectedFileMap();
Project project = myPsiFile.getProject();
for (PsiElement element : elements) {
@@ -510,6 +511,7 @@ class InspectionRunner {
});
}
addToQueue.finish(); // no more injections
return true;
}
interface ApplyIncrementallyCallback {
@@ -22,7 +22,10 @@ import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -381,12 +384,6 @@ public final class JobLauncherImpl extends JobLauncher {
return (T)TOMBSTONE;
}
public interface QueueController<T> {
void enqueue(T element);
void dropEverythingAndPanic();
void finish();
}
/**
* Method for producing some elements (to process via {@code thingProcessor}), and scheduling their processing along with ongoing calculations.
* It has three parts:
@@ -402,11 +399,12 @@ public final class JobLauncherImpl extends JobLauncher {
* Guarantees all tasks are completed in the method end, or runtime exception is thrown, in which case some elements might be in-flight.
* @return true if all processors returned true
*/
@Override
@ApiStatus.Internal
public <T> boolean procInOrderAsync(@NotNull ProgressIndicator progress,
int maxQueueSize,
@NotNull PairProcessor<? super T, ? super QueueController<? super T>> thingProcessor,
@NotNull Consumer<? super QueueController<? super T>> otherActions) throws ProcessCanceledException {
@NotNull Processor<? super QueueController<? super T>> otherActions) throws ProcessCanceledException {
progress.checkCanceled(); // do not start up expensive threads if there's no need to
// optimization: if we know the max number of elements in the queue, use the cheaper ABQ
BlockingQueue<T> things = maxQueueSize < Integer.MAX_VALUE ?
@@ -536,7 +534,9 @@ public final class JobLauncherImpl extends JobLauncher {
try {
// execute other actions while we are processing enqueued elements
otherActions.accept(addToQueue);
if (!otherActions.process(addToQueue)) {
futureResult.set(false);
}
}
catch (Exception e) {
// in case of exception in normal flow, terminate background tasks
@@ -21,6 +21,7 @@ import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.extensions.DefaultPluginDescriptor;
import com.intellij.openapi.fileTypes.PlainTextFileType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.ExtensionTestUtil;
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
import org.jetbrains.annotations.NotNull;
@@ -117,7 +118,9 @@ public class DocumentMarkupModelTest extends BasePlatformTestCase {
public static class TestAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
holder.newSilentAnnotation(HighlightSeverity.INFORMATION).create();
if (element instanceof PsiFile) {
holder.newSilentAnnotation(HighlightSeverity.INFORMATION).create();
}
}
}
}
@@ -262,7 +262,6 @@ public final class GroovyAnnotator extends GroovyElementVisitor {
@Override
public void visitVariableDeclaration(@NotNull GrVariableDeclaration variableDeclaration) {
checkDuplicateModifiers(myHolder, variableDeclaration.getModifierList(), null);
if (variableDeclaration.isTuple()) {
final GrModifierList list = variableDeclaration.getModifierList();
@@ -385,7 +384,10 @@ public final class GroovyAnnotator extends GroovyElementVisitor {
}
checkTypeDefinition(myHolder, typeDefinition);
checkImplementedMethodsOfClass(myHolder, typeDefinition);
// enum constants are not handled here because their getTextOffset() is crazy - outside their own getTextRange()
if (!(typeDefinition instanceof PsiEnumConstantInitializer)) {
checkImplementedMethodsOfClass(myHolder, typeDefinition);
}
checkConstructors(myHolder, typeDefinition);
checkAnnotationCollector(myHolder, typeDefinition);
@@ -993,6 +995,8 @@ public final class GroovyAnnotator extends GroovyElementVisitor {
}
else {
checkVariableModifiers(myHolder, declaration);
GrVariable[] variables = declaration.getVariables();
checkDuplicateModifiers(myHolder, modifierList, variables.length == 0 ? null : variables[0]);
}
}
else if (parent instanceof GrClassInitializer) {
@@ -1731,7 +1735,7 @@ public final class GroovyAnnotator extends GroovyElementVisitor {
final TextRange range = GrHighlightUtil.getClassHeaderTextRange(typeDefinition);
String message = GroovyBundle.message("method.is.not.implemented", notImplementedMethodName);
AnnotationBuilder builder =
AnnotationBuilder builder =
holder.newAnnotation(HighlightSeverity.ERROR, message)
.range(range);
registerImplementsMethodsFix(typeDefinition, abstractMethod, builder, message, range).create();
@@ -1944,8 +1948,9 @@ public final class GroovyAnnotator extends GroovyElementVisitor {
}
}
private static void checkDuplicateModifiers(AnnotationHolder holder, @NotNull GrModifierList list, PsiMember member) {
private static void checkDuplicateModifiers(AnnotationHolder holder, @NotNull GrModifierList list, PsiElement member) {
final PsiElement[] modifiers = list.getModifiers();
if (modifiers.length <= 1) return;
Set<String> set = new HashSet<>(modifiers.length);
for (PsiElement modifier : modifiers) {
if (modifier instanceof GrAnnotation) continue;
@@ -1953,10 +1958,12 @@ public final class GroovyAnnotator extends GroovyElementVisitor {
if (set.contains(name)) {
String message = GroovyBundle.message("duplicate.modifier", name);
AnnotationBuilder builder =
holder.newAnnotation(HighlightSeverity.ERROR, message).range(list);
holder.newAnnotation(HighlightSeverity.ERROR, message).range(modifier);
GrModifierFix fix = member instanceof PsiMember ? new GrModifierFix((PsiMember)member, name, false, false, GrModifierFix.MODIFIER_LIST) :
member instanceof GrVariable ? new GrModifierFix((GrVariable)member, name, false, GrModifierFix.MODIFIER_LIST) :
null;
if (member != null) {
builder = registerLocalFix(builder, new GrModifierFix(member, name, false, false, GrModifierFix.MODIFIER_LIST), list, message,
ProblemHighlightType.ERROR, list.getTextRange());
builder = registerLocalFix(builder, fix, list, message, ProblemHighlightType.ERROR, list.getTextRange());
}
builder.create();
}
@@ -1274,7 +1274,7 @@ class B extends A {
void testUnresolvedQualifierHighlighting() {
doTestHighlighting('''\
<error descr="Cannot resolve symbol 'Abc'">Abc</error>.Cde abc
<error descr="Cannot resolve symbol 'Abc'">Abc</error>.<error descr="Cannot resolve symbol 'Cde'">Cde</error> abc
''')
}
@@ -1422,7 +1422,7 @@ class X{}
@A('ab')
class Y{}
<error descr="Duplicate modifier 'public'">public public</error> class Z {}
public <error descr="Duplicate modifier 'public'">public</error> class Z {}
''')
}
@@ -1,7 +1,7 @@
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
def x=new ActionListener() {
def x=new <error descr="Method 'foo' is not implemented">ActionListener</error>() {
def actionPerformed(ActionEvent e) {
}
@@ -22,7 +22,7 @@ enum E {
String bar(x = 2)
}
def a = new Runnable() {
def a = new <error descr="Method 'foo' is not implemented">Runnable</error>() {
void run() {}
<error descr="Anonymous class cannot have abstract method">abstract</error> foo(x = 5);
}
+1 -1
View File
@@ -20,7 +20,7 @@ class Combinations {
}
class Duplicates {
<error descr="Duplicate modifier 'public'">public public</error> a
public <error descr="Duplicate modifier 'public'">public</error> a
}
interface I {
@@ -17,4 +17,4 @@ import groovy.transform.Field
<error descr="Illegal combination of modifiers">@Field protected public</error> c
<error descr="Illegal combination of modifiers 'volatile' and 'final'">@Field volatile final</error> g
<error descr="Duplicate modifier 'public'">@Field public public</error> d
@Field public <error descr="Duplicate modifier 'public'">public</error> d
@@ -1 +1 @@
<error descr="Duplicate modifier 'def'">def final def</error> foo = 44
def final <error descr="Duplicate modifier 'def'">def</error> foo = 44
@@ -12,11 +12,11 @@ class JSR308BaseClass<T> {}
interface JSR308Interface1<T> {}
interface JSR308Interface2<T extends <error descr="Type annotations are not supported in current version">@JSR308</error> CharSequence> {}
class JSR308Class extends @JSR308 JSR308BaseClass<<error descr="Type annotations are not supported in current version">@JSR308</error> List> implements @JSR308 JSR308Interface1<<error descr="Type annotations are not supported in current version">@JSR308</error> String>, @JSR308 JSR308Interface2<<error descr="Type annotations are not supported in current version">@JSR308</error> String> {
class JSR308Class extends <error descr="Type annotations are not supported in current version">@JSR308</error> JSR308BaseClass<<error descr="Type annotations are not supported in current version">@JSR308</error> List> implements <error descr="Type annotations are not supported in current version">@JSR308</error> JSR308Interface1<<error descr="Type annotations are not supported in current version">@JSR308</error> String>, <error descr="Type annotations are not supported in current version">@JSR308</error> JSR308Interface2<<error descr="Type annotations are not supported in current version">@JSR308</error> String> {
@JSR308 private String name;
@JSR308 List<<error descr="Type annotations are not supported in current version">@JSR308</error> String> test(@JSR308 List<@JSR308 ? extends <error descr="Type annotations are not supported in current version">@JSR308</error> Object> list) throws <error descr="Type annotations are not supported in current version">@JSR308</error> IOException, <error descr="Type annotations are not supported in current version">@JSR308</error> java.sql.SQLException {
@JSR308 List<<error descr="Type annotations are not supported in current version">@JSR308</error> String> localVar = new @JSR308 ArrayList<<error descr="Type annotations are not supported in current version">@JSR308</error> String>();
@JSR308 List<<error descr="Type annotations are not supported in current version">@JSR308</error> String> localVar = new <error descr="Type annotations are not supported in current version">@JSR308</error> ArrayList<<error descr="Type annotations are not supported in current version">@JSR308</error> String>();
try {
for (e in list) {
@@ -27,9 +27,9 @@ class JSR308Class extends @JSR308 JSR308BaseClass<<error descr="Type annotations
}
String <error descr="Type annotations are not supported in current version">@JSR308</error> [] strs = new String @JSR308 [] <error descr="Array initializers are not supported in current version">{ 'a' }</error>
String <error descr="Type annotations are not supported in current version">@JSR308</error> [] @JSR308 [] strs2 = new String @JSR308 [] @JSR308 [] { new String[] <error descr="Array initializers are not supported in current version">{'a', 'b'}</error> }
String <error descr="Type annotations are not supported in current version">@JSR308</error> [] <error descr="Type annotations are not supported in current version">@JSR308</error> [] strs2 = new String @JSR308 [] @JSR308 [] <error descr="Array initializers are not supported in current version">{ new String[] <error descr="Array initializers are not supported in current version">{'a', 'b'}</error> }</error>
String [][] <error descr="Type annotations are not supported in current version">@JSR308</error> [] strs3 = new String [1][2] @JSR308 []
String [] <error descr="Type annotations are not supported in current version">@JSR308</error> [] @JSR308 [] @JSR308 [] strs4 = new String [1] @JSR308 [2] @JSR308 [] @JSR308 []
String [] <error descr="Type annotations are not supported in current version">@JSR308</error> [] <error descr="Type annotations are not supported in current version">@JSR308</error> [] <error descr="Type annotations are not supported in current version">@JSR308</error> [] strs4 = new String [1] @JSR308 [2] @JSR308 [] @JSR308 []
localVar.add(strs[0])
localVar.add(strs2[0][1])
@@ -9,6 +9,6 @@
<fx:script>
</fx:script>
<fx:<error descr="Cannot resolve symbol 'fx:id'">id</error>/>
<Label fx:id="label" GridPane.rowIndex="1" <error descr="Attribute fx:script is not allowed here">fx:script</error>=""/>
<Label fx:id="label" GridPane.rowIndex="1" <error descr="Property 'fx:script' is read-only"><error descr="Attribute fx:script is not allowed here">fx:script</error>=""</error>/>
</children>
</GridPane>
@@ -8,5 +8,5 @@
<fx:script>
</fx:script>
<fx:<error descr="Cannot resolve symbol 'fx:id'">id</error>/>
<Label fx:id="label" GridPane.rowIndex="1" <error descr="Attribute fx:script is not allowed here">fx:script</error>=""/>
<Label fx:id="label" GridPane.rowIndex="1" <error descr="Property 'fx:script' is read-only"><error descr="Attribute fx:script is not allowed here">fx:script</error>=""</error>/>
</GridPane>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" <error descr="Attribute unknownAttr is not allowed here">unknownAttr</error>="val" xmlns:fx="http://javafx.com/fxml">
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" <error descr="Property 'unknownAttr' is read-only"><error descr="Attribute unknownAttr is not allowed here">unknownAttr</error>="val"</error> xmlns:fx="http://javafx.com/fxml">
<children>
<AnchorPane id="anchorPane2" prefHeight="300.0" AnchorPane.bottomAnchor="200.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
@@ -1,6 +1,6 @@
<?import javafx.scene.layout.AnchorPane?>
<?import java.lang.String?>
<AnchorPane id="AnchorPane" xmlns:fx="http://javafx.com/fxml" stylesheets="<error descr="Cannot resolve file 'mystyle.css'">mystyle.css</error>" <error descr="Attribute backgroundFills is not allowed here">backgroundFills</error>="">
<AnchorPane id="AnchorPane" xmlns:fx="http://javafx.com/fxml" stylesheets="<error descr="Cannot resolve file 'mystyle.css'">mystyle.css</error>" <error descr="Property 'backgroundFills' is read-only"><error descr="Attribute backgroundFills is not allowed here">backgroundFills</error>=""</error>>
<stylesheets>
<String fx:value="<error descr="Cannot resolve file 'mystyle.css'">mystyle.css</error>"/>
</stylesheets>
@@ -1,2 +1,2 @@
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml"
style="-fx-background-color: yellow;" prefHeight="500" <error descr="Attribute unknownProperty is not allowed here">unknownProperty</error>=""/>
style="-fx-background-color: yellow;" prefHeight="500" <error descr="Property 'unknownProperty' is read-only"><error descr="Attribute unknownProperty is not allowed here">unknownProperty</error>=""</error>/>
@@ -4,7 +4,7 @@
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<children>
<AnchorPane AnchorPane.leftAnchor="0.0" <error descr="Attribute AnchorPane.leftAnchor1 is not allowed here">AnchorPane.<error descr="Cannot resolve symbol 'leftAnchor1'">leftAnchor1</error></error>="0.0">
<AnchorPane AnchorPane.leftAnchor="0.0" <error descr="Property 'AnchorPane.leftAnchor1' is read-only"><error descr="Attribute AnchorPane.leftAnchor1 is not allowed here">AnchorPane.<error descr="Cannot resolve symbol 'leftAnchor1'">leftAnchor1</error></error>="0.0"</error>>
<AnchorPane.bottomAnchor>200</AnchorPane.bottomAnchor>
<<error descr="Element AnchorPane1.bottomAnchor is not allowed here">AnchorPane1.bottomAnchor</error>>200</<error descr="Element AnchorPane1.bottomAnchor is not allowed here">AnchorPane1.bottomAnchor</error>>
<GridPane.rowIndex>0</GridPane.rowIndex>
@@ -1,6 +1,6 @@
package test;
public class ExtendClassWithDefaultImplementationComplext {
class ExtendClassWithDefaultImplementationComplext {
<error descr="Class 'Test1' must either be declared abstract or implement abstract method 'a()' in 'A'">public static class Test1 implements A</error> {
}
@@ -131,6 +131,7 @@ public final class XPathAnnotator extends XPath2ElementVisitor implements Annota
public void visitElement(@NotNull PsiElement element) {
final IElementType elementType = element.getNode().getElementType();
if (elementType != XPathTokenTypes.STAR && XPath2TokenTypes.KEYWORDS.contains(elementType)) {
if (element.getPrevSibling() == null) return;
final PsiElement leaf = PsiTreeUtil.prevLeaf(element);
PsiElement number;
if (leaf != null && (number = leaf.getParent()) instanceof XPathNumber && ((XPathNumber)number).getXPathVersion() == XPathVersion.V2) {
@@ -16,5 +16,5 @@ key-case-1:
<error descr="Invalid block mapping key indent">subKey2: text 2</error>
key-case-2:
subKey1: text 1
subKey2: <error descr="Invalid child element in a block mapping">text 2
subKey3: text 3</error>
<error descr="Invalid block mapping key indent">subKey2: <error descr="It is forbidden to specify block composed value at the same line as key">subKey2: text 2</error></error>
subKey3: text 3
@@ -1,3 +1,3 @@
<error descr="Python version 3.9 does not support match statements">match</error> 42:
<error descr="Python version 3.9 does not support match statements"><info descr="null">match</info></error> 42:
<info descr="null">case</info> 42:
pass
@@ -1 +1 @@
<error descr="Python version 3.11 does not support type alias statements"><info descr="null">type</info> myType[T] = list[T]</error>
<error descr="Python version 3.11 does not support type alias statements"><info descr="null">type</info> myType[T] = <info descr="PY.BUILTIN_NAME">list</info>[T]</error>