// 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.DaemonAnalyzerTestCase; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInspection.*; import com.intellij.codeInspection.accessStaticViaInstance.AccessStaticViaInstance; import com.intellij.codeInspection.dataFlow.ConstantValueInspection; import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection; import com.intellij.codeInspection.ex.Tools; import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspectionBase; import com.intellij.codeInspection.miscGenerics.RawUseOfParameterizedTypeInspection; import com.intellij.codeInspection.varScopeCanBeNarrowed.FieldCanBeLocalInspection; import com.intellij.debugger.DebugException; import com.intellij.diagnostic.ThreadDumper; import com.intellij.ide.highlighter.JavaFileType; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.command.undo.UndoManager; import com.intellij.openapi.diagnostic.DefaultLogger; import com.intellij.openapi.editor.*; 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; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; import com.intellij.openapi.fileTypes.PlainTextFileType; import com.intellij.openapi.project.Project; 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.profile.codeInspection.ProjectInspectionProfileManager; import com.intellij.psi.*; import com.intellij.testFramework.PlatformTestUtil; import com.intellij.testFramework.SkipSlowTestLocally; import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl; import com.intellij.util.ArrayUtilRt; import com.intellij.util.TestTimeOut; import com.intellij.util.TimeoutUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import com.intellij.xml.util.CheckDtdReferencesInspection; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.awt.geom.Point2D; import java.io.File; import java.io.IOException; import java.util.List; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * tests {@link LocalInspectionTool} behaviour during highlighting */ @SkipSlowTestLocally @DaemonAnalyzerTestCase.CanChangeDocumentDuringHighlighting public class DaemonInspectionsRespondToChangesTest extends DaemonAnalyzerTestCase { private DaemonCodeAnalyzerImpl myDaemonCodeAnalyzer; @Override protected void setUp() throws Exception { super.setUp(); enableInspectionTool(new UnusedDeclarationInspection()); myDaemonCodeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(getProject()); UndoManager.getInstance(myProject); myDaemonCodeAnalyzer.setUpdateByTimerEnabled(true); DaemonProgressIndicator.setDebug(true); PlatformTestUtil.assumeEnoughParallelism(); } @Override protected void tearDown() throws Exception { try { if (myEditor != null) { Document document = myEditor.getDocument(); FileDocumentManager.getInstance().reloadFromDisk(document); } Project project = getProject(); if (project != null) { doPostponedFormatting(project); } } catch (Throwable e) { addSuppressedException(e); } finally { myDaemonCodeAnalyzer = null; super.tearDown(); } } @Override protected Sdk getTestProjectJdk() { //noinspection removal return JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk(); } @Override protected @NotNull LanguageLevel getProjectLanguageLevel() { return LanguageLevel.JDK_11; } @Override protected void configureByExistingFile(@NotNull VirtualFile virtualFile) { super.configureByExistingFile(virtualFile); setActiveEditors(getEditor()); } @Override protected VirtualFile configureByFiles(@Nullable File rawProjectRoot, VirtualFile @NotNull ... vFiles) throws IOException { VirtualFile file = super.configureByFiles(rawProjectRoot, vFiles); setActiveEditors(getEditor()); return file; } private void setActiveEditors(Editor @NotNull ... editors) { EditorTracker.Companion.getInstance(myProject).setActiveEditors(Arrays.asList(editors)); } @Override protected LocalInspectionTool[] configureLocalInspectionTools() { return new LocalInspectionTool[] { new FieldCanBeLocalInspection(), new RequiredAttributesInspectionBase(), new CheckDtdReferencesInspection(), new AccessStaticViaInstance(), }; } @Override protected void setUpProject() throws Exception { super.setUpProject(); // treat listeners added there as not leaks EditorMouseHoverPopupManager.getInstance(); } public void testWholeFileInspection() throws Exception { configureByFile(DaemonRespondToChangesTest.BASE_PATH + "FieldCanBeLocal.java"); List infos = doHighlighting(HighlightSeverity.WARNING); assertSize(1, infos); assertEquals("Field can be converted to a local variable", infos.get(0).getDescription()); ctrlW(); WriteCommandAction.runWriteCommandAction(getProject(), () -> EditorModificationUtilEx.deleteSelectedText(getEditor())); type("xxxx"); infos = doHighlighting(HighlightSeverity.WARNING); assertEmpty(infos); ctrlW(); WriteCommandAction.runWriteCommandAction(getProject(), () -> EditorModificationUtilEx.deleteSelectedText(getEditor())); type("0"); infos = doHighlighting(HighlightSeverity.WARNING); assertSize(1, infos); assertEquals("Field can be converted to a local variable", infos.get(0).getDescription()); } private abstract static class MyTrackingInspection extends MyInspectionBase { private final List visited = Collections.synchronizedList(new ArrayList<>()); @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new PsiElementVisitor() { @Override public void visitFile(@NotNull PsiFile file) { TimeoutUtil.sleep(1000); // make it run longer than LIP super.visitFile(file); } @Override public void visitElement(@NotNull PsiElement element) { visited.add(element); } }; } } static abstract class MyInspectionBase extends LocalInspectionTool { @Nls @NotNull @Override public String getGroupDisplayName() { return getClass().getName(); } @Nls @NotNull @Override public String getDisplayName() { return getGroupDisplayName(); } @NotNull @Override public String getShortName() { return getGroupDisplayName(); } } private static class MyWholeInspection extends MyTrackingInspection { @Override public boolean runForWholeFile() { return true; } } public void testWholeFileInspectionRestartedOnAllElements() { MyTrackingInspection tool = registerInspection(new MyWholeInspection()); configureByText(JavaFileType.INSTANCE, "class X { void f() { } }"); List infos = doHighlighting(HighlightSeverity.WARNING); assertEmpty(infos); int visitedCount = new HashSet<>(tool.visited).size(); tool.visited.clear(); type(" "); // white space modification infos = doHighlighting(HighlightSeverity.WARNING); assertEmpty(infos); int countAfter = new HashSet<>(tool.visited).size(); assertTrue("visitedCount = "+visitedCount+"; countAfter="+countAfter, countAfter >= visitedCount); } private @NotNull T registerInspection(@NotNull T tool) { enableInspectionTool(tool); return tool; } public void testWholeFileInspectionRestartedEvenIfThereWasAModificationInsideCodeBlockInOtherFile() throws Exception { MyTrackingInspection tool = registerInspection(new MyWholeInspection()); PsiFile file = configureByText(JavaFileType.INSTANCE, "class X { void f() { } }"); PsiFile otherFile = createFile(myModule, file.getContainingDirectory().getVirtualFile(), "otherFile.txt", "xxx"); List infos = doHighlighting(HighlightSeverity.WARNING); assertEmpty(infos); int visitedCount = tool.visited.size(); assertTrue(tool.visited.toString(), visitedCount > 0); tool.visited.clear(); Document otherDocument = Objects.requireNonNull(PsiDocumentManager.getInstance(getProject()).getDocument(otherFile)); WriteCommandAction.runWriteCommandAction(getProject(), () -> otherDocument.setText("zzz")); infos = doHighlighting(HighlightSeverity.WARNING); assertEmpty(infos); int countAfter = tool.visited.size(); assertTrue(tool.visited.toString(), countAfter > 0); tool.visited.clear(); //ensure started on another file configureByExistingFile(otherFile.getVirtualFile()); infos = doHighlighting(HighlightSeverity.WARNING); assertEmpty(infos); int countAfter2 = tool.visited.size(); assertTrue(tool.visited.toString(), countAfter2 > 0); } public void testInspectionIsRestartedOnPsiCacheDrop() { MyTrackingInspection tool = registerInspection(new MyTrackingInspection(){}); configureByText(JavaFileType.INSTANCE, "class X { void f() { } }"); DaemonRespondToChangesTest.waitForDaemon(myProject, myEditor.getDocument()); tool.visited.clear(); getPsiManager().dropPsiCaches(); DaemonRespondToChangesTest.waitForDaemon(myProject, myEditor.getDocument()); assertNotEmpty(tool.visited); } public void testLIPGetAllParentsAfterCodeBlockModification() { @Language("JAVA") String text = """ class LQF { int f; public void me() { // } }"""; configureByText(JavaFileType.INSTANCE, text); List visitedElements = Collections.synchronizedList(new ArrayList<>()); class MyVisitor extends PsiElementVisitor { @Override public void visitElement(@NotNull PsiElement element) { visitedElements.add(element); } } LocalInspectionTool tool = new MyInspectionBase() { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new MyVisitor(); } }; disposeOnTearDown(() -> disableInspectionTool(tool.getShortName())); enableInspectionTool(tool); assertEmpty(highlightErrors()); List allPsi = CollectHighlightsUtil.getElementsInRange(myFile, 0, myFile.getTextLength()); assertEquals(new HashSet<>(allPsi), new HashSet<>(visitedElements)); // inside code block modification visitedElements.clear(); backspace(); backspace(); assertEmpty(highlightErrors()); PsiMethod method = ((PsiJavaFile)myFile).getClasses()[0].getMethods()[0]; List methodAndParents = CollectHighlightsUtil.getElementsInRange(myFile, method.getTextRange().getStartOffset(), method.getTextRange().getEndOffset(), true); assertEquals(new HashSet<>(methodAndParents), new HashSet<>(visitedElements)); } public void testDumbQuickFixIsNoLongerVisibleAfterApplied() { registerInspection(new FindElseBranchInspection()); @Language("JAVA") String text = "class X { void f() { if (this == null) {} else return; } }"; configureByText(JavaFileType.INSTANCE, text); WriteCommandAction.runWriteCommandAction(getProject(), () -> myEditor.getDocument().setText(text)); getEditor().getCaretModel().moveToOffset(getFile().getText().indexOf("if (") + 1); assertEmpty(doHighlighting(HighlightSeverity.ERROR)); List fixes = findStupidFixes(); IntentionAction fix = assertOneElement(fixes); fix.invoke(getProject(), getEditor(), getFile()); fixes = findStupidFixes(); assertEmpty(fixes); assertEmpty(doHighlighting(HighlightSeverity.ERROR)); fixes = findStupidFixes(); assertEmpty(fixes); } private @NotNull List findStupidFixes() { return ContainerUtil.filter(CodeInsightTestFixtureImpl.getAvailableIntentions(getEditor(), getFile()), f -> f.getFamilyName() .equals(new FindElseBranchInspection.StupidQuickFixWhichDoesntCheckItsOwnApplicability().getFamilyName())); } private static class FindElseBranchInspection extends LocalInspectionTool { @Nls @NotNull @Override public String getGroupDisplayName() { return "danuna"; } @Nls @NotNull @Override public String getDisplayName() { return getGroupDisplayName(); } @NotNull @Override public String getShortName() { return getGroupDisplayName(); } @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitIfStatement(@NotNull PsiIfStatement statement) { if (statement.getElseBranch() != null) { PsiKeyword keyword = (PsiKeyword)statement.getChildren()[0]; holder.registerProblem(keyword, "Dododo", new StupidQuickFixWhichDoesntCheckItsOwnApplicability()); } } }; } private static class StupidQuickFixWhichDoesntCheckItsOwnApplicability implements LocalQuickFix { @Nls @NotNull @Override public String getName() { return "danu"; } @Nls @NotNull @Override public String getFamilyName() { return getName(); } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { WriteCommandAction.runWriteCommandAction(project, () -> ((PsiIfStatement)descriptor.getPsiElement().getParent()).getElseBranch().delete()); } } } private static final AtomicInteger toSleepMs = new AtomicInteger(0); private static final String SWEARING = "No swearing"; private void checkSwearingHighlightIsVisibleImmediately() throws Exception { @Language("JAVA") String text = """ class X /* */ { int foo(Object param) {//XXX return 0; }/* */ } """; configureByText(JavaFileType.INSTANCE, text); ((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000); @NotNull Editor editor = getEditor(); assertEquals(getFile().getTextRange(), editor.calculateVisibleRange()); toSleepMs.set(1_000_000); MarkupModel markupModel = DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); TestTimeOut n = TestTimeOut.setTimeout(100, TimeUnit.SECONDS); AtomicInteger called = new AtomicInteger(); Runnable checkHighlighted = () -> { called.incrementAndGet(); UIUtil.dispatchAllInvocationEvents(); long highlighted = Arrays.stream(markupModel.getAllHighlighters()) .map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter)) .filter(Objects::nonNull) .filter(info -> SWEARING.equals(info.getDescription())) .count(); if (highlighted != 0) { toSleepMs.set(0); 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); List 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 void testAddInspectionProblemToProblemHolderEntailsCreatingCorrespondingRangeHighlighterMoreOrLessImmediately() throws Exception { registerInspection(new MySwearingInspection()); checkSwearingHighlightIsVisibleImmediately(); } // produces an error problem for "XXX" comment very fast, and then very slowly inspects the rest of the file private static class MySwearingInspection extends LocalInspectionTool { @Nls @NotNull @Override public String getGroupDisplayName() { return "sweardanuna"; } @Nls @NotNull @Override public String getDisplayName() { return getGroupDisplayName(); } @NotNull @Override public String getShortName() { return getGroupDisplayName(); } @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitComment(@NotNull PsiComment element) { if (element.getText().equals("//XXX")) { holder.registerProblem(element, SWEARING); } } @Override public void visitFile(@NotNull PsiFile file) { // 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); } } }; } } public void testLocalInspectionPassMustRunFastOrFertileInspectionsFirstToReduceLatency() { @Language("JAVA") String text = """ class LQF { int f; public void me() { // } void foo// (){}}"""; 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 AtomicReference diagnosticText = new AtomicReference<>("1st run"); AtomicInteger stallMs = new AtomicInteger(); // highlight fields, stall every other element LocalInspectionTool tool = new MyInspectionBase() { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitField(@NotNull PsiField field) { holder.registerProblem(field.getNameIdentifier(), diagnosticText.get()); } @Override public void visitElement(@NotNull PsiElement element) { // stall every other element to exacerbate latency problems if the order is wrong TimeoutUtil.sleep(stallMs.get()); } }; } }; disposeOnTearDown(() -> disableInspectionTool(tool.getShortName())); for (Tools tools : ProjectInspectionProfileManager.getInstance(getProject()).getCurrentProfile().getAllEnabledInspectionTools(getProject())) { disableInspectionTool(tools.getTool().getShortName()); } enableInspectionTool(tool); List infos = doHighlighting(HighlightSeverity.WARNING); HighlightInfo i = assertOneElement(infos); assertEquals(diagnosticText.get(), i.getDescription()); diagnosticText.set("Aha, field, finally!"); stallMs.set(10000); backspace(); backspace(); type("blah"); DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way // now when the LIP 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"); } } PsiField field = ((PsiJavaFile)getFile()).getClasses()[0].getFields()[0]; TextRange range = field.getNameIdentifier().getTextRange(); MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); while (DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) { if (System.currentTimeMillis() > deadline) { DaemonRespondToChangesPerformanceTest.dumpThreadsToConsole(); fail("Too long waiting for daemon to finish"); } PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); boolean found = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, range.getStartOffset(), range.getEndOffset(), info -> !diagnosticText.get().equals(info.getDescription())); if (found) { break; } } } private static volatile TextRange expectedVisibleRange; private static class MyVisibleRangeInspection extends MyTrackingInspection { @Override public void inspectionStarted(@NotNull LocalInspectionToolSession session, boolean isOnTheFly) { TextRange priorityRange = session.getPriorityRange(); assertEquals(expectedVisibleRange, priorityRange); expectedVisibleRange = null; } } private static final String wordToAnnotate = "annotate_here"; public void testLocalInspectionMustReceiveCorrectVisibleRangeViaItsHighlightingSession() { registerInspection(new MyVisibleRangeInspection()); String text = "blah blah\n".repeat(1000) + "" + wordToAnnotate + "\n" + "balh blah\n".repeat(1000); configureByText(PlainTextFileType.INSTANCE, text); EditorImpl editor = (EditorImpl)getEditor(); editor.getScrollPane().getViewport().setSize(1000, 1000); DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true); editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); Point2D caretVisualPoint = editor.offsetToPoint2D(editor.getCaretModel().getOffset()); editor.getScrollPane().getViewport().setViewPosition(new Point((int)caretVisualPoint.getX(), (int)caretVisualPoint.getY())); editor.getScrollPane().getViewport().setExtentSize(new Dimension(100, editor.getPreferredHeight() - (int)caretVisualPoint.getY())); ProperTextRange visibleRange = editor.calculateVisibleRange(); assertTrue(visibleRange.toString(), visibleRange.getStartOffset() > 0); myDaemonCodeAnalyzer.restart(); expectedVisibleRange = visibleRange; doHighlighting(); assertNull(expectedVisibleRange); // check the inspection was run DaemonRespondToChangesTest.makeWholeEditorWindowVisible(editor); myDaemonCodeAnalyzer.restart(); expectedVisibleRange = new TextRange(0, editor.getDocument().getTextLength()); doHighlighting(); assertNull(expectedVisibleRange); // check the inspection was run } // add file-level "blah" for each identifier containing "XXX" private static class MyFileLevelInspection extends MyInspectionBase { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitIdentifier(@NotNull PsiIdentifier identifier) { if (identifier.getText().contains("XXX")) { holder.registerProblem(identifier.getContainingFile(),"blah", ProblemHighlightType.WARNING); } } }; } } public void testFileLevelHighlightingDoesNotDuplicateOnTypingInsideSmallRange() { registerInspection(new MyFileLevelInspection()); @Language("JAVA") String text = """ class X { void foo() { int XXX; } }"""; configureByText(JavaFileType.INSTANCE, text); assertEmpty(highlightErrors()); assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); type('2'); assertEmpty(highlightErrors()); assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); } public void testFileLevelHighlightingDoesNotDuplicateOnTypingOrOnFileCloseReopen() { registerInspection(new MyFileLevelInspection()); @Language("JAVA") String text = """ class X { int XXX; }"""; PsiFile psiFile = configureByText(JavaFileType.INSTANCE, text); assertEmpty(highlightErrors()); assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); FileEditorManager.getInstance(myProject).closeFile(psiFile.getVirtualFile()); for (int i=0; i<100; i++) { configureByExistingFile(psiFile.getVirtualFile()); getEditor().getCaretModel().moveToOffset(getEditor().getDocument().getText().indexOf("XXX") + "XXX".length()); UIUtil.dispatchAllInvocationEvents(); type('2'); assertEmpty(highlightErrors()); UIUtil.dispatchAllInvocationEvents(); assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); UIUtil.dispatchAllInvocationEvents(); backspace(); assertEmpty(highlightErrors()); UIUtil.dispatchAllInvocationEvents(); assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); LOG.debug("i = " + i); FileEditorManager.getInstance(myProject).closeFile(psiFile.getVirtualFile()); } } public void testFileLevelWithEverChangingDescriptionMustUpdateOnTyping() { // add file-level "blah" if there are identifiers containing "xxx" class XXXIdentifierFileLevelInspection extends MyInspectionBase { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitIdentifier(@NotNull PsiIdentifier identifier) { if (identifier.getText().contains("xxx")) { holder.registerProblem(identifier.getContainingFile(),"xxx: "+identifier.getText(), ProblemHighlightType.WARNING); } } }; } } registerInspection(new XXXIdentifierFileLevelInspection()); @Language("JAVA") String text = """ class X { void foo() { int xxx; } }"""; configureByText(JavaFileType.INSTANCE, text); assertEmpty(highlightErrors()); HighlightInfo info = assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); assertEquals("xxx: xxx", info.getDescription()); type('2'); assertEmpty(highlightErrors()); info = assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); assertEquals("xxx: xxx2", info.getDescription()); type('y'); assertEmpty(highlightErrors()); info = assertOneElement(myDaemonCodeAnalyzer.getFileLevelHighlights(getProject(), getFile())); assertEquals("xxx: xxx2y", info.getDescription()); } public void testInspectionMustRemoveItsObsoleteHighlightsImmediatelyAfterFinished() { @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() SeverityRegistrar.getSeverityRegistrar(getProject()); //preload inspection profile AtomicReference fieldWarningText = new AtomicReference<>("1st run"); AtomicInteger stallMs = new AtomicInteger(0); AtomicBoolean slowToolFinished = new AtomicBoolean(); LocalInspectionTool slowTool = new MyInspectionBase() { @Override public void inspectionFinished(@NotNull LocalInspectionToolSession session, @NotNull ProblemsHolder problemsHolder) { // invoke later because we are checking this flag in EDT below, and // we do not want a race between contextFinishedCallback.accept(context); in inspection thread // and querying markup model in EDT ApplicationManager.getApplication().invokeLater(() -> { //System.out.println("slow finished "); slowToolFinished.set(true); }); } @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitField(@NotNull PsiField field) { //System.out.println("slow visit field "+field + Thread.currentThread()); holder.registerProblem(field.getNameIdentifier(), fieldWarningText.get()); } @Override public void visitElement(@NotNull PsiElement element) { //System.out.println("slow visit "+element + Thread.currentThread()); // stall every other element to exacerbate latency problems if the order is wrong TimeoutUtil.sleep(stallMs.get()); } }; } }; AtomicBoolean fastToolFinished = new AtomicBoolean(); // highlights all "xxx" comments, only when there are no comments after it String fastToolText = "blah"; LocalInspectionTool fastTool = new MyInspectionBase() { @Override public void inspectionFinished(@NotNull LocalInspectionToolSession session, @NotNull ProblemsHolder problemsHolder) { // invoke later because we are checking this flag in EDT below, and // we do not want a race between contextFinishedCallback.accept(context); in inspection thread // and querying markup model in EDT //System.out.println("fast about to finished "); ApplicationManager.getApplication().invokeLater(() -> { //System.out.println("fast finished "); fastToolFinished.set(true); }); } @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitComment(@NotNull PsiComment comment) { //System.out.println("fast visit comment "+comment + Thread.currentThread()); if (comment.getText().contains("xxx") && !comment.getContainingFile().getText().substring(comment.getTextOffset()+2).contains("//")) { holder.registerProblem(comment, fastToolText, ProblemHighlightType.WARNING); } } @Override public void visitElement(@NotNull PsiElement element) { //System.out.println("fast visit "+element + Thread.currentThread()); } }; } }; disposeOnTearDown(() -> disableInspectionTool(fastTool.getShortName())); disposeOnTearDown(() -> disableInspectionTool(slowTool.getShortName())); for (Tools tools : ProjectInspectionProfileManager.getInstance(getProject()).getCurrentProfile().getAllEnabledInspectionTools(getProject())) { disableInspectionTool(tools.getTool().getShortName()); } enableInspectionTools(fastTool, slowTool); // both inspections should produce their results List infos = doHighlighting(HighlightSeverity.WARNING); assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(fieldWarningText.get()))); assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(fastToolText))); fieldWarningText.set("Aha, field, finally!"); stallMs.set(100); type("// another comment"); //System.out.println("-------------"); fastToolFinished.set(false); slowToolFinished.set(false); DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way // now when the LIP 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"); } } MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); 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 (fastToolFinished.get() && !slowToolFinished.get()) { fastToolFinishedFaster = true; boolean fastFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0, myEditor.getDocument().getTextLength(), info -> !fastToolText.equals(info.getDescription())); if (fastFound) { fail("Inspection 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 { stallMs.set(0); } } public void testModificationInsideCommentDoesNotAffectNearbyInspectionWarning() { enableInspectionTool(new ConstantValueInspection()); @Language("JAVA") String text = """ class AClass { public int foo() { // if (this == null) return 0; return 1; } } """; configureByText(JavaFileType.INSTANCE, text); assertEmpty(highlightErrors()); List infos = doHighlighting(HighlightSeverity.WARNING); HighlightInfo error = ContainerUtil.find(infos, e->e.getDescription().contains("always 'false'")); assertNotNull(infos.toString(), error); type("d"); List infos2 = doHighlighting(HighlightSeverity.WARNING); HighlightInfo error2 = ContainerUtil.find(infos2, e->e.getDescription().contains("always 'false'")); assertNotNull(infos2.toString(), error2); } private static class MyException extends RuntimeException { MyException() { super("MyPreciousException"); } } public void testThrowingExceptionFromInspectionMustPropagateUpToTheLogger() { DefaultLogger.disableStderrDumping(getTestRootDisposable()); MyInspectionBase throwExceptionInspection = new MyInspectionBase() { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new PsiElementVisitor() { @Override public void visitFile(@NotNull PsiFile file) { throw new MyException(); } }; } }; enableInspectionTool(throwExceptionInspection); @Language("JAVA") String text = """ class AClass { } """; configureByText(JavaFileType.INSTANCE, text); assertThrows(Throwable.class, new MyException().getMessage(), () -> highlightErrors()); } public void testInspectionMustRemoveItsObsoleteHighlightsImmediatelyAfterVisitingPSIElementTheSecondTimeAndFailingToGenerateTheSameWarningAgain() { @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() SeverityRegistrar.getSeverityRegistrar(getProject()); //preload inspection profile String fieldWarningText = "1st run"; AtomicBoolean fieldIdentifierVisited = new AtomicBoolean(); AtomicBoolean fieldHighlightsUpdated = new AtomicBoolean(); AtomicBoolean fieldToolMustWarn = new AtomicBoolean(true); // highlight each field on visitField() if said so, stall on every other element LocalInspectionTool fieldTool = new MyInspectionBase() { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @Override public void visitIdentifier(@NotNull PsiIdentifier identifier) { if (identifier.getParent() instanceof PsiField) { if (fieldToolMustWarn.get()) { holder.registerProblem(identifier, fieldWarningText); } fieldIdentifierVisited.set(true); } } @Override public void visitElement(@NotNull PsiElement element) { if (fieldIdentifierVisited.get()) { // after visitIdentifier() has completed, before the next visit() method is called, the highlights must be updated fieldHighlightsUpdated.set(true); } } }; } }; enableInspectionTools(fieldTool); // inspections should produce their results List infos = doHighlighting(HighlightSeverity.WARNING); assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(fieldWarningText))); assertTrue(fieldIdentifierVisited.get()); assertTrue(fieldHighlightsUpdated.get()); fieldIdentifierVisited.set(false); fieldHighlightsUpdated.set(false); fieldToolMustWarn.set(false); type("// another comment\nvoid anotherMethod(){}"); DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way 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"); } } // now when the LIP restarted, we should observe the range highlighter for the inspection to disappear as soon as visitIdentifier() method is finished MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); while (DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) { if (System.currentTimeMillis() > deadline) { fail("Too long waiting for daemon to finish\n"+ThreadDumper.dumpThreadsToString()); } PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); if (fieldHighlightsUpdated.get()) { boolean found = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0, myEditor.getDocument().getTextLength(), info -> !fieldWarningText.equals(info.getDescription())); if (found) { fail("Inspection must have its obsolete highlights removed as soon as its visitIdentifier() is finished, but got:" + StringUtil.join(model.getAllHighlighters(), Object::toString, "\n ") + "; thread dump:\n" + ThreadDumper.dumpThreadsToString()); } } } assertTrue(fieldIdentifierVisited.get()); assertTrue(fieldHighlightsUpdated.get()); } public void testRedundantWarningMustNotBlinkOnTypingInsideParameterModifierList() { DefaultLogger.disableStderrDumping(getTestRootDisposable()); enableInspectionTools(new RedundantSuppressInspection(), new RawUseOfParameterizedTypeInspection()); @Language("JAVA") String text = """ abstract class Converter { abstract T fromString(String value); } class Base { protected Base(String accessor, /*caret goes here*/Class converter) { System.out.println(accessor + converter); } } """; configureByText(JavaFileType.INSTANCE, text); List infos = doHighlighting(HighlightSeverity.WARNING); assertTrue(infos.toString(), ContainerUtil.exists(infos, info-> new RawUseOfParameterizedTypeInspection().getShortName() .equals(info.getInspectionToolId()))); getEditor().getCaretModel().moveToOffset(getEditor().getDocument().getText().indexOf("Class highlighters = List.of(DocumentMarkupModel.forDocument(getDocument(myFile), myProject, true).getAllHighlighters()); assertFalse(ContainerUtil.exists(highlighters, r -> HighlightInfo.fromRangeHighlighter(r) != null && getShortName().equals(HighlightInfo.fromRangeHighlighter(r).getInspectionToolId()))); holder.registerProblem(identifier, "XXX", ProblemHighlightType.WARNING); } } }; } }; enableInspectionTools(myTool); @Language("JAVA") String text = """ class Base { int xxx; } """; configureByText(JavaFileType.INSTANCE, text); List infos = doHighlighting(HighlightSeverity.WARNING); assertOneElement(ContainerUtil.filter(infos, info-> myTool.getShortName().equals(info.getInspectionToolId()))); getEditor().getCaretModel().moveToOffset(getEditor().getDocument().getText().indexOf("int xxx;")); type("// ") ; assertEmpty(doHighlighting(HighlightSeverity.WARNING)); } }