Files
openide/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonRespondToChangesTest.java
Alexey Kudravtsev c83ce9390f tests: cleanup
GitOrigin-RevId: 4c0abf48343a2ad6c4ebfea78c20d66cd981c02b
2024-09-04 12:03:32 +00:00

2417 lines
98 KiB
Java

// 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.application.options.editor.CodeFoldingConfigurable;
import com.intellij.codeHighlighting.*;
import com.intellij.codeInsight.EditorInfo;
import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.daemon.impl.analysis.FileHighlightingSetting;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingSettingsPerFile;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixTestCase;
import com.intellij.codeInsight.folding.JavaCodeFoldingSettings;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.accessStaticViaInstance.AccessStaticViaInstance;
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection;
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspectionBase;
import com.intellij.codeInspection.ex.InspectionProfileImpl;
import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspectionBase;
import com.intellij.codeInspection.unusedSymbol.UnusedSymbolLocalInspection;
import com.intellij.codeInspection.varScopeCanBeNarrowed.FieldCanBeLocalInspection;
import com.intellij.concurrency.ConcurrentCollectionFactory;
import com.intellij.configurationStore.StorageUtilKt;
import com.intellij.configurationStore.StoreUtil;
import com.intellij.configurationStore.StoreUtilKt;
import com.intellij.diagnostic.ThreadDumper;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.ide.GeneralSettings;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.javaee.ExternalResourceManagerExImpl;
import com.intellij.lang.LanguageFilter;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.actionSystem.TypedAction;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
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.OpenFileDescriptor;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorProvider;
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
import com.intellij.openapi.fileTypes.PlainTextFileType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.ProperTextRange;
import com.intellij.openapi.util.Segment;
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.InspectionProfileManager;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.refactoring.inline.InlineRefactoringActionHandler;
import com.intellij.refactoring.rename.RenameProcessor;
import com.intellij.testFramework.*;
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl;
import com.intellij.util.*;
import com.intellij.util.concurrency.ThreadingAssertions;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.storage.HeavyProcessLatch;
import com.intellij.util.ref.GCWatcher;
import com.intellij.util.ui.UIUtil;
import com.intellij.xml.util.CheckDtdReferencesInspection;
import kotlin.Unit;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.concurrent.Future;
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 general daemon behaviour/interruptibility/restart during highlighting
*/
@SkipSlowTestLocally
@DaemonAnalyzerTestCase.CanChangeDocumentDuringHighlighting
public class DaemonRespondToChangesTest extends DaemonAnalyzerTestCase {
static final String BASE_PATH = "/codeInsight/daemonCodeAnalyzer/typing/";
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);
}
@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 boolean doTestLineMarkers() {
return true;
}
@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();
}
private static void typeInAlienEditor(@NotNull Editor alienEditor, char c) {
EditorActionManager.getInstance();
TypedAction action = TypedAction.getInstance();
DataContext dataContext = ((EditorEx)alienEditor).getDataContext();
action.actionPerformed(alienEditor, c, dataContext);
}
public void testHighlightersUpdate() throws Exception {
configureByFile(BASE_PATH + "HighlightersUpdate.java");
Document document = getDocument(getFile());
assertNotEmpty(highlightErrors());
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(document, HighlightSeverity.ERROR, getProject());
assertSize(1, errors);
TextRange dirty = myDaemonCodeAnalyzer.getFileStatusMap().getFileDirtyScope(document, getFile(), Pass.UPDATE_ALL);
assertNull(dirty);
type(' ');
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
dirty = myDaemonCodeAnalyzer.getFileStatusMap().getFileDirtyScope(document, getFile(), Pass.UPDATE_ALL);
assertNotNull(dirty);
}
public void testNoPsiEventsAltogether() throws Exception {
configureByFile(BASE_PATH + "HighlightersUpdate.java");
Document document = getDocument(getFile());
assertNotEmpty(highlightErrors());
type(' ');
backspace();
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
TextRange dirty = myDaemonCodeAnalyzer.getFileStatusMap().getFileDirtyScope(document, getFile(), Pass.UPDATE_ALL);
assertEquals(getFile().getTextRange(), dirty); // have to rehighlight whole file in case no PSI events have come
}
public void testRenameClass() {
configureByText(JavaFileType.INSTANCE, """
class AClass<caret> {
}
""");
Document document = getDocument(getFile());
assertEmpty(highlightErrors());
PsiClass psiClass = ((PsiJavaFile)getFile()).getClasses()[0];
new RenameProcessor(myProject, psiClass, "Class2", false, false).run();
TextRange dirty = myDaemonCodeAnalyzer.getFileStatusMap().getFileDirtyScope(document, getFile(), Pass.UPDATE_ALL);
assertEquals(getFile().getTextRange(), dirty);
assertEmpty(highlightErrors());
assertTrue(myDaemonCodeAnalyzer.isErrorAnalyzingFinished(getFile()));
}
public void testTypingSpace() {
configureByText(JavaFileType.INSTANCE, """
class AClass<caret> {
}
""");
Document document = getDocument(getFile());
assertEmpty(highlightErrors());
type(" ");
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
PsiElement elementAtCaret = myFile.findElementAt(myEditor.getCaretModel().getOffset());
assertTrue(elementAtCaret instanceof PsiWhiteSpace);
TextRange dirty = myDaemonCodeAnalyzer.getFileStatusMap().getFileDirtyScope(document, getFile(), Pass.UPDATE_ALL);
assertEquals(elementAtCaret.getTextRange(), dirty);
assertEmpty(highlightErrors());
assertTrue(myDaemonCodeAnalyzer.isErrorAnalyzingFinished(getFile()));
}
public void testTypingSpaceInsideError() {
configureByText(JavaFileType.INSTANCE, """
class AClass {
{
toString(0,<caret>0);
}
}
""");
assertOneElement(highlightErrors());
for (int i = 0; i < 100; i++) {
type(" ");
assertOneElement(highlightErrors());
}
}
public void testBackSpaceInsideError() {
configureByText(JavaFileType.INSTANCE, """
class E {
void fff() {
int i = <caret>
}
}
""");
assertOneElement(highlightErrors());
backspace();
assertOneElement(highlightErrors());
}
public void testUnusedFieldUpdate() {
configureByText(JavaFileType.INSTANCE, """
class Unused {
private int ffff;
void foo(int p) {
if (p==0) return;
<caret>
}
}
""");
Document document = getDocument(getFile());
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
assertSize(1, infos);
assertEquals("Private field 'ffff' is never used", infos.get(0).getDescription());
type(" foo(ffff++);");
assertEmpty(highlightErrors());
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(document, HighlightSeverity.WARNING, getProject());
assertSize(0, errors);
}
public void testUnusedMethodUpdate() {
configureByText(JavaFileType.INSTANCE, """
class X {
static void ffff() {}
public static void main(String[] args){
for (int i=0; i<1000;i++) {
System.out.println(i);
<caret>ffff();
}
}
}""");
enableInspectionTool(new UnusedDeclarationInspection(true));
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
assertEmpty(infos);
PlatformTestUtil.invokeNamedAction(IdeActions.ACTION_COMMENT_LINE);
infos = doHighlighting(HighlightSeverity.WARNING);
assertSize(1, infos);
assertEquals("Method 'ffff()' is never used", infos.get(0).getDescription());
}
public void testAssignedButUnreadFieldUpdate() throws Exception {
configureByFile(BASE_PATH + "AssignedButUnreadField.java");
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
assertSize(1, infos);
assertEquals("Private field 'text' is assigned but never accessed", infos.get(0).getDescription());
ctrlW();
WriteCommandAction.runWriteCommandAction(getProject(), () -> EditorModificationUtilEx.deleteSelectedText(getEditor()));
type(" text");
List<HighlightInfo> errors = doHighlighting(HighlightSeverity.WARNING);
assertEmpty(getFile().getText(), errors);
}
public void testDaemonIgnoresNonPhysicalEditor() throws Exception {
configureByText(JavaFileType.INSTANCE, """
class AClass<caret> {
}
""");
assertEmpty(highlightErrors());
EditorFactory editorFactory = EditorFactory.getInstance();
Document consoleDoc = editorFactory.createDocument("my console blah");
Editor consoleEditor = editorFactory.createEditor(consoleDoc);
try {
checkDaemonReaction(false, () -> caretRight(consoleEditor));
checkDaemonReaction(true, () -> typeInAlienEditor(consoleEditor, 'x'));
checkDaemonReaction(true, () -> LightPlatformCodeInsightTestCase.backspace(consoleEditor, getProject()));
//real editor
checkDaemonReaction(true, this::caretRight);
}
finally {
editorFactory.releaseEditor(consoleEditor);
}
}
public void testDaemonIgnoresConsoleActivities() throws Exception {
configureByText(JavaFileType.INSTANCE, """
class AClass<caret> {
}
""");
assertEmpty(highlightErrors());
ConsoleView consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(getProject()).getConsole();
consoleView.getComponent(); //create editor
consoleView.print("haha", ConsoleViewContentType.NORMAL_OUTPUT);
UIUtil.dispatchAllInvocationEvents();
try {
checkDaemonReaction(false, () -> {
consoleView.clear();
try {
Thread.sleep(300); // *&^ing alarm
}
catch (InterruptedException e) {
LOG.error(e);
}
UIUtil.dispatchAllInvocationEvents(); //flush
});
checkDaemonReaction(false, () -> {
consoleView.print("sss", ConsoleViewContentType.NORMAL_OUTPUT);
try {
Thread.sleep(300); // *&^ing alarm
}
catch (InterruptedException e) {
LOG.error(e);
}
UIUtil.dispatchAllInvocationEvents(); //flush
});
checkDaemonReaction(false, () -> {
consoleView.setOutputPaused(true);
try {
Thread.sleep(300); // *&^ing alarm
}
catch (InterruptedException e) {
LOG.error(e);
}
UIUtil.dispatchAllInvocationEvents(); //flush
});
}
finally {
Disposer.dispose(consoleView);
}
}
private void checkDaemonReaction(boolean mustCancelItself, @NotNull Runnable action) throws Exception {
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
highlightErrors();
myDaemonCodeAnalyzer.waitForTermination();
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
AtomicBoolean run = new AtomicBoolean();
Disposable disposable = Disposer.newDisposable();
AtomicReference<RuntimeException> stopDaemonReason = new AtomicReference<>();
StorageUtilKt.setDEBUG_LOG("");
getProject().getMessageBus().connect(disposable).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
new DaemonCodeAnalyzer.DaemonListener() {
@Override
public void daemonCancelEventOccurred(@NotNull String reason) {
RuntimeException e = new RuntimeException("Some bastard's restarted daemon: " + reason +
"\nStorage write log: ----------\n" +
StorageUtilKt.getDEBUG_LOG() + "\n--------------");
stopDaemonReason.compareAndSet(null, e);
}
});
try {
while (true) {
try {
int[] toIgnore = new int[0];
Runnable callbackWhileWaiting = () -> {
if (!run.getAndSet(true)) {
action.run();
}
};
myDaemonCodeAnalyzer.runPasses(getFile(), getDocument(getFile()), textEditor, toIgnore, true, callbackWhileWaiting);
break;
}
catch (ProcessCanceledException ignored) { }
}
if (mustCancelItself) {
assertNotNull(stopDaemonReason.get());
}
else {
if (stopDaemonReason.get() != null) throw stopDaemonReason.get();
}
}
finally {
StorageUtilKt.setDEBUG_LOG(null);
Disposer.dispose(disposable);
}
}
public void testChangeXmlIncludeLeadsToRehighlight() {
LanguageFilter[] extensions = XMLLanguage.INSTANCE.getLanguageExtensions();
for (LanguageFilter extension : extensions) {
XMLLanguage.INSTANCE.unregisterLanguageExtension(extension);
}
String location = getTestName(false) + ".xsd";
final String url = "http://myschema/";
ExternalResourceManagerExImpl.registerResourceTemporarily(url, location, getTestRootDisposable());
configureByFiles(null, BASE_PATH + getTestName(false) + ".xml", BASE_PATH + getTestName(false) + ".xsd");
assertEmpty(highlightErrors());
Editor[] allEditors = EditorFactory.getInstance().getAllEditors();
Editor schemaEditor = null;
for (Editor editor : allEditors) {
Document document = editor.getDocument();
PsiFile file = PsiDocumentManager.getInstance(getProject()).getPsiFile(document);
if (file == null) continue;
if (location.equals(file.getName())) {
schemaEditor = editor;
break;
}
}
delete(Objects.requireNonNull(schemaEditor));
assertNotEmpty(highlightErrors());
for (LanguageFilter extension : extensions) {
XMLLanguage.INSTANCE.registerLanguageExtension(extension);
}
}
public void testRehighlightInnerBlockAfterInline() throws Exception {
configureByFile(BASE_PATH + getTestName(false) + ".java");
HighlightInfo error = assertOneElement(highlightErrors());
assertEquals("Variable 'e' is already defined in the scope", error.getDescription());
PsiElement element = getFile().findElementAt(getEditor().getCaretModel().getOffset()).getParent();
DataContext dataContext = SimpleDataContext.getSimpleContext(CommonDataKeys.PSI_ELEMENT, element, ((EditorEx)getEditor()).getDataContext());
new InlineRefactoringActionHandler().invoke(getProject(), getEditor(), getFile(), dataContext);
assertEmpty(highlightErrors());
}
public void testRangeMarkersDoNotGetAddedOrRemovedWhenUserIsJustTypingInsideHighlightedRegionAndEspeciallyInsideInjectedFragmentsWhichAreColoredGreenAndUsersComplainEndlesslyThatEditorFlickersThere() {
configureByText(JavaFileType.INSTANCE, """
class S { int f() {
return <caret>hashCode();
}}""");
Collection<HighlightInfo> infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertSize(3, infos);
AtomicInteger count = new AtomicInteger();
MarkupModelEx modelEx = (MarkupModelEx)DocumentMarkupModel.forDocument(getDocument(getFile()), getProject(), true);
modelEx.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
@Override
public void afterAdded(@NotNull RangeHighlighterEx highlighter) {
count.incrementAndGet();
}
@Override
public void afterRemoved(@NotNull RangeHighlighterEx highlighter) {
count.incrementAndGet();
}
});
type(' ');
assertEmpty(highlightErrors());
assertEquals(0, count.get());
}
public void testTypeParametersMustNotBlinkWhenTypingInsideClass() {
configureByText(JavaFileType.INSTANCE, """
package x;
abstract class ToRun<TTTTTTTTTTTTTTT> implements Comparable<TTTTTTTTTTTTTTT> {
private ToRun<TTTTTTTTTTTTTTT> delegate;
<caret>
\s
}""");
assertEmpty(highlightErrors());
MarkupModelEx modelEx = (MarkupModelEx)DocumentMarkupModel.forDocument(getDocument(getFile()), getProject(), true);
modelEx.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
@Override
public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) {
if (highlighter.getTextRange().substring(highlighter.getDocument().getText()).equals("TTTTTTTTTTTTTTT")) {
throw new RuntimeException("Must not remove type parameter highlighter");
}
}
});
assertEmpty(highlightErrors());
type("//xxx");
assertEmpty(highlightErrors());
backspace();
assertEmpty(highlightErrors());
backspace();
assertEmpty(highlightErrors());
backspace();
assertEmpty(highlightErrors());
backspace();
backspace();
assertEmpty(highlightErrors());
}
public void testLocallyUsedFieldHighlighting() {
configureByText(JavaFileType.INSTANCE, """
class A {
String cons;
void foo() {
String local = null;
<selection>cons</selection>.substring(1); }
public static void main(String[] args) {
new A().foo();
}}""");
enableInspectionTool(new UnusedDeclarationInspection(true));
List<HighlightInfo> infos = doHighlighting(HighlightSeverity.WARNING);
assertSize(1, infos);
assertEquals("Variable 'local' is never used", infos.get(0).getDescription());
type("local");
infos = doHighlighting(HighlightSeverity.WARNING);
assertSize(1, infos);
assertEquals("Field 'cons' is never used", infos.get(0).getDescription());
}
public void testOverrideMethodsHighlightingPersistWhenTypeInsideMethodBody() {
@Language("JAVA")
String text = """
package x;\s
class ClassA {
static <T> void sayHello(Class<? extends T> msg) {}
}
class ClassB extends ClassA {
static <T extends String> void sayHello(Class<? extends T> msg) {<caret>
}
}
""";
configureByText(JavaFileType.INSTANCE, text);
assertOneElement(highlightErrors());
type("//my comment inside method body, so class modifier won't be visited");
assertOneElement(highlightErrors());
}
public void testWhenTypingOverWrongReferenceItsColorChangesToBlackAndOnlyAfterHighlightingFinishedItReturnsToRed() {
configureByText(JavaFileType.INSTANCE, """
class S { int f() {
return asfsdfsdfsd<caret>;
}}""");
HighlightInfo error = assertOneElement(highlightErrors());
assertSame(HighlightInfoType.WRONG_REF, error.type);
Document document = getDocument(getFile());
type("xxx");
List<HighlightInfo> infos = DaemonCodeAnalyzerImpl.getHighlights(document, HighlightInfoType.SYMBOL_TYPE_SEVERITY, getProject());
for (HighlightInfo info : infos) {
assertNotSame(HighlightInfoType.WRONG_REF, info.type);
}
HighlightInfo error2 = assertOneElement(highlightErrors());
assertSame(HighlightInfoType.WRONG_REF, error2.type);
}
public void testQuickFixRemainsAvailableAfterAnotherFixHasBeenAppliedInTheSameCodeBlockBefore() throws Exception {
configureByFile(BASE_PATH + "QuickFixes.java");
DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance();
boolean old = settings.isNextErrorActionGoesToErrorsFirst();
settings.setNextErrorActionGoesToErrorsFirst(true);
try {
Collection<HighlightInfo> errors = highlightErrors();
assertSize(3, errors);
new GotoNextErrorHandler(true).invoke(getProject(), getEditor(), getFile());
List<IntentionAction> fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
IntentionAction fix = assertContainsOneOf(fixes, "Delete catch for 'java.io.IOException'");
IntentionAction finalFix = fix;
WriteCommandAction.runWriteCommandAction(getProject(), () -> finalFix.invoke(getProject(), getEditor(), getFile()));
errors = highlightErrors();
assertSize(2, errors);
new GotoNextErrorHandler(true).invoke(getProject(), getEditor(), getFile());
fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
fix = assertContainsOneOf(fixes, "Delete catch for 'java.io.IOException'");
IntentionAction finalFix1 = fix;
WriteCommandAction.runWriteCommandAction(getProject(), () -> finalFix1.invoke(getProject(), getEditor(), getFile()));
assertOneElement(highlightErrors());
new GotoNextErrorHandler(true).invoke(getProject(), getEditor(), getFile());
fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
fix = assertContainsOneOf(fixes, "Delete catch for 'java.io.IOException'");
IntentionAction finalFix2 = fix;
WriteCommandAction.runWriteCommandAction(getProject(), () -> finalFix2.invoke(getProject(), getEditor(), getFile()));
assertEmpty(highlightErrors());
}
finally {
settings.setNextErrorActionGoesToErrorsFirst(old);
}
}
private static IntentionAction assertContainsOneOf(@NotNull Collection<? extends IntentionAction> collection, @NotNull String text) {
IntentionAction result = null;
for (IntentionAction action : collection) {
if (text.equals(action.getText())) {
if (result != null) {
fail("multiple " + " objects present in collection " + collection);
}
else {
result = action;
}
}
}
assertNotNull(" object not found in collection " + collection, result);
return result;
}
public void testRangeHighlightersDoNotGetStuckForever() {
configureByText(JavaFileType.INSTANCE, "class S { void ffffff() {fff<caret>fff();}}");
assertEmpty(highlightErrors());
MarkupModel markup = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
TextRange[] highlightersBefore = getHighlightersTextRange(markup);
type("%%%%");
assertNotEmpty(highlightErrors());
backspace();
backspace();
backspace();
backspace();
assertEmpty(highlightErrors());
TextRange[] highlightersAfter = getHighlightersTextRange(markup);
assertSize(highlightersBefore.length, highlightersAfter);
for (int i = 0; i < highlightersBefore.length; i++) {
TextRange before = highlightersBefore[i];
TextRange after = highlightersAfter[i];
assertEquals(before.getStartOffset(), after.getStartOffset());
assertEquals(before.getEndOffset(), after.getEndOffset());
}
}
private static TextRange @NotNull [] getHighlightersTextRange(@NotNull MarkupModel markup) {
RangeHighlighter[] highlighters = markup.getAllHighlighters();
TextRange[] result = new TextRange[highlighters.length];
for (int i = 0; i < highlighters.length; i++) {
result[i] = ProperTextRange.create(highlighters[i]);
}
return orderByHashCode(result); // markup.getAllHighlighters returns unordered array
}
private static <T extends Segment> T @NotNull [] orderByHashCode(T @NotNull [] highlighters) {
Arrays.sort(highlighters, (o1, o2) -> o2.hashCode() - o1.hashCode());
return highlighters;
}
public void testFileStatusMapDirtyPSICachingWorks() {
myDaemonCodeAnalyzer.setUpdateByTimerEnabled(false); // to prevent auto-start highlighting
UIUtil.dispatchAllInvocationEvents();
configureByText(JavaFileType.INSTANCE, "class <caret>S { int ffffff = 0;}");
UIUtil.dispatchAllInvocationEvents();
int[] creation = {0};
class Fac implements TextEditorHighlightingPassFactory {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
TextRange textRange = FileStatusMap.getDirtyTextRange(editor.getDocument(), file, Pass.UPDATE_ALL);
if (textRange == null) return null;
return new TestFileStatusMapDirtyCachingWorksPass(myProject);
}
final class TestFileStatusMapDirtyCachingWorksPass extends TextEditorHighlightingPass {
private TestFileStatusMapDirtyCachingWorksPass(Project project) {
super(project, getEditor().getDocument(), false);
creation[0]++;
}
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
}
@Override
public void doApplyInformationToEditor() {
}
}
}
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
registrar.registerTextEditorHighlightingPass(new Fac(), null, null, false, -1);
assertEmpty(highlightErrors());
assertEquals(1, creation[0]);
//cached
assertEmpty(highlightErrors());
assertEquals(1, creation[0]);
assertEmpty(highlightErrors());
assertEquals(1, creation[0]);
type(' ');
assertEmpty(highlightErrors());
assertEquals(2, creation[0]);
assertEmpty(highlightErrors());
assertEquals(2, creation[0]);
assertEmpty(highlightErrors());
assertEquals(2, creation[0]);
}
public void testFileStatusMapDirtyDocumentRangeWorks() {
configureByText(PlainTextFileType.INSTANCE, "class <caret>S { int ffffff = 0;}");
UIUtil.dispatchAllInvocationEvents();
Document document = myEditor.getDocument();
FileStatusMap fileStatusMap = myDaemonCodeAnalyzer.getFileStatusMap();
fileStatusMap.disposeDirtyDocumentRangeStorage(document);
assertEquals(TextRange.EMPTY_RANGE, fileStatusMap.getCompositeDocumentDirtyRange(document));
int offset = myEditor.getCaretModel().getOffset();
type(' ');
assertEquals(new TextRange(offset, offset+1), fileStatusMap.getCompositeDocumentDirtyRange(document));
WriteCommandAction.runWriteCommandAction(getProject(), () -> document.replaceString(10, 11, "xxx"));
assertEquals(new TextRange(offset, 13), fileStatusMap.getCompositeDocumentDirtyRange(document));
WriteCommandAction.runWriteCommandAction(getProject(), () -> document.setText(" "));
assertEquals(new TextRange(0, 2), fileStatusMap.getCompositeDocumentDirtyRange(document));
fileStatusMap.disposeDirtyDocumentRangeStorage(document);
assertEquals(new TextRange(0, 0), fileStatusMap.getCompositeDocumentDirtyRange(document));
WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(0,"x"));
assertEquals(new TextRange(0, 1), fileStatusMap.getCompositeDocumentDirtyRange(document));
WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(1,"x"));
assertEquals(new TextRange(0, 2), fileStatusMap.getCompositeDocumentDirtyRange(document));
WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(4,"x"));
assertEquals(new TextRange(0, 5), fileStatusMap.getCompositeDocumentDirtyRange(document));
}
public void testDefensivelyDirtyFlagDoesNotClearPrematurely() {
class Fac implements TextEditorHighlightingPassFactory {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
return null;
}
}
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
registrar.registerTextEditorHighlightingPass(new Fac(), null, null, false, -1);
configureByText(JavaFileType.INSTANCE, "@Deprecated<caret> class S { } ");
List<HighlightInfo> infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertSize(2, infos);
assertEquals("@Deprecated", infos.get(0).getText());
assertEquals("S", infos.get(1).getText());
backspace();
type('d');
List<HighlightInfo> after = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertEquals("@Deprecated", after.get(0).getText());
assertEquals("S", after.get(1).getText());
backspace();
type('d');
getEditor().getCaretModel().moveToOffset(getEditor().getDocument().getTextLength());
type(" ");
after = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertSize(2, after);
assertEquals("@Deprecated", after.get(0).getText());
assertEquals("S", after.get(1).getText());
}
public void testModificationInsideCodeBlockDoesNotAffectErrorMarkersOutside() {
configureByText(JavaFileType.INSTANCE, """
class SSSSS {
public static void suite() {
<caret>
new Runnable() {
public void run() {
}
};
}
""");
HighlightInfo error = assertOneElement(highlightErrors());
assertEquals("'}' expected", error.getDescription());
type("//comment");
HighlightInfo error2 = assertOneElement(highlightErrors());
assertEquals("'}' expected", error2.getDescription());
}
public void testErrorMarkerAtTheEndOfTheFile() {
CommandProcessor.getInstance().executeCommand(getProject(), () -> {
try {
configureByFile(BASE_PATH + "ErrorMarkAtEnd.java");
}
catch (Exception e) {
LOG.error(e);
}
}, "Cc", this);
EditorTestUtil.setEditorVisibleSizeInPixels(getEditor(), 1000, 1000);
assertEmpty(highlightErrors());
CommandProcessor.getInstance().executeCommand(getProject(), () -> {
Document document = getEditor().getDocument();
int offset = getEditor().getCaretModel().getOffset();
while (offset < document.getTextLength()) {
int i = StringUtil.indexOf(document.getText(), '}', offset, document.getTextLength());
if (i == -1) break;
getEditor().getCaretModel().moveToOffset(i);
delete(getEditor());
}
}, "My", this);
List<HighlightInfo> errs = highlightErrors();
assertSize(2, errs);
assertEquals("'}' expected", errs.get(0).getDescription());
undo();
assertEmpty(highlightErrors());
}
// todo - StoreUtil.saveDocumentsAndProjectsAndApp cannot save in EDT. If it is called in EDT,
// in this case, task is done under a modal progress, so, no idea how to fix the test, except executing it not in EDT (as it should be)
public void _testDaemonIgnoresFrameDeactivation() {
// return default value to avoid unnecessary save
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
String text = "class S { ArrayList<caret>XXX x;}";
configureByText(JavaFileType.INSTANCE, text);
assertNotEmpty(highlightErrors());
GeneralSettings settings = GeneralSettings.getInstance();
boolean frameSave = settings.isSaveOnFrameDeactivation();
settings.setSaveOnFrameDeactivation(true);
StoreUtilKt.runInAllowSaveMode(true, () -> {
try {
StoreUtil.saveDocumentsAndProjectsAndApp(false);
try {
checkDaemonReaction(false, () -> StoreUtil.saveDocumentsAndProjectsAndApp(false));
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
finally {
settings.setSaveOnFrameDeactivation(frameSave);
}
return Unit.INSTANCE;
});
}
public void testApplyLocalQuickFix() {
configureByText(JavaFileType.INSTANCE, "class X { static int sss; public int f() { return this.<caret>sss; }}");
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
List<HighlightInfo> warns = doHighlighting(HighlightSeverity.WARNING);
assertOneElement(warns);
Editor editor = getEditor();
List<HighlightInfo.IntentionActionDescriptor> actions =
ShowIntentionsPass.getAvailableFixes(editor, getFile(), -1, ((EditorEx)editor).getExpectedCaretOffset());
HighlightInfo.IntentionActionDescriptor descriptor = assertOneElement(actions);
CodeInsightTestFixtureImpl.invokeIntention(descriptor.getAction(), getFile(), getEditor());
assertEmpty(highlightErrors());
assertEmpty(ShowIntentionsPass.getAvailableFixes(editor, getFile(), -1, ((EditorEx)editor).getExpectedCaretOffset()));
}
public void testApplyErrorInTheMiddle() {
String text = "class <caret>X { " + """
{
// String x = "<zzzzzzzzzz/>";
}""".repeat(100) +
"\n}";
configureByText(JavaFileType.INSTANCE, text);
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
assertEmpty(highlightErrors());
type("//");
List<HighlightInfo> errors = highlightErrors();
assertSize(2, errors);
backspace();
backspace();
assertEmpty(highlightErrors());
}
public void testErrorInTheEndOutsideVisibleArea() {
String text = "<xml> \n" + StringUtil.repeatSymbol('\n', 1000) + "</xml>\nxxxxx<caret>";
configureByText(XmlFileType.INSTANCE, text);
ProperTextRange visibleRange = makeEditorWindowVisible(new Point(0, 1000), myEditor);
assertTrue(visibleRange.getStartOffset() > 0);
HighlightInfo info = assertOneElement(highlightErrors());
assertEquals("Top level element is not completed", info.getDescription());
type("xxx");
info = assertOneElement(highlightErrors());
assertEquals("Top level element is not completed", info.getDescription());
}
@NotNull
public static ProperTextRange makeEditorWindowVisible(@NotNull Point viewPosition, @NotNull Editor editor) {
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
int offset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(viewPosition));
VisibleHighlightingPassFactory.setVisibleRangeForHeadlessMode(editor, new ProperTextRange(offset, offset));
return editor.calculateVisibleRange();
}
static void makeWholeEditorWindowVisible(@NotNull EditorImpl editor) {
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
VisibleHighlightingPassFactory.setVisibleRangeForHeadlessMode(editor, new ProperTextRange(0, editor.getDocument().getTextLength()));
}
public void testEnterInCodeBlock() {
String text = """
class LQF {
int wwwwwwwwwwww;
public void main() {<caret>
wwwwwwwwwwww = 1;
}
}""";
configureByText(JavaFileType.INSTANCE, text);
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
List<HighlightInfo> infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertSize(4, infos);
type('\n');
infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertSize(4, infos);
deleteLine();
infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY);
assertSize(4, infos);
}
public void testTypingNearEmptyErrorElement() {
String text = """
class LQF {
public void main() {
int wwwwwwwwwwww = 1<caret>
}
}""";
configureByText(JavaFileType.INSTANCE, text);
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(1000, 1000);
assertOneElement(highlightErrors());
type(';');
assertEmpty(highlightErrors());
}
public void testCancelsItSelfOnTypingInAlienProject() throws Throwable {
String body = StringUtil.repeat("\"String field = null;\"\n", 1000);
configureByText(JavaFileType.INSTANCE, "class X{ void f() {" + body + "<caret>\n} }");
Project alienProject = PlatformTestUtil.loadAndOpenProject(createTempDirectory().toPath().resolve("alien.ipr"), getTestRootDisposable());
DaemonProgressIndicator.setDebug(true);
try {
Module alienModule = doCreateRealModuleIn("x", alienProject, getModuleType());
VirtualFile alienRoot = createTestProjectStructure(alienModule, null, true, getTempDir());
PsiDocumentManager.getInstance(alienProject).commitAllDocuments();
OpenFileDescriptor alienDescriptor = WriteAction.compute(() -> {
VirtualFile alienFile = alienRoot.createChildData(this, "AlienFile.java");
setFileText(alienFile, "class Alien { }");
return new OpenFileDescriptor(alienProject, alienFile);
});
FileEditorManager fe = FileEditorManager.getInstance(alienProject);
Editor alienEditor = Objects.requireNonNull(fe.openTextEditor(alienDescriptor, false));
((EditorImpl)alienEditor).setCaretActive();
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
PsiDocumentManager.getInstance(alienProject).commitAllDocuments();
// start daemon in the main project. should check for its cancel when typing in alien
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
AtomicBoolean checked = new AtomicBoolean();
Runnable callbackWhileWaiting = () -> {
if (checked.getAndSet(true)) return;
typeInAlienEditor(alienEditor, 'x');
};
myDaemonCodeAnalyzer.runPasses(getFile(), getEditor().getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, true, callbackWhileWaiting);
}
catch (ProcessCanceledException ignored) {
return;
}
fail("must throw PCE");
}
public void testPasteInAnonymousCodeBlock() {
configureByText(JavaFileType.INSTANCE, """
class X{ void f() { int x=0;x++;
Runnable r = new Runnable() { public void run() {
<caret>
}};
<selection>int y = x;</selection>
\s
} }""");
assertEmpty(highlightErrors());
PlatformTestUtil.invokeNamedAction(IdeActions.ACTION_EDITOR_COPY);
assertEquals("int y = x;", getEditor().getSelectionModel().getSelectedText());
getEditor().getSelectionModel().removeSelection();
PlatformTestUtil.invokeNamedAction(IdeActions.ACTION_EDITOR_PASTE);
assertOneElement(highlightErrors());
}
public void testPostHighlightingPassRunsOnEveryPsiModification() throws Exception {
@Language("JAVA")
String xText = "public class X { public static void ffffffffffffff(){} }";
PsiFile x = createFile("X.java", xText);
PsiFile use = createFile("Use.java", "public class Use { { <caret>X.ffffffffffffff(); } }");
configureByExistingFile(use.getVirtualFile());
enableDeadCodeInspection();
Editor xEditor = createEditor(x.getVirtualFile());
List<HighlightInfo> xInfos = filter(CodeInsightTestFixtureImpl.instantiateAndRun(x, xEditor, new int[0], false),
HighlightSeverity.WARNING);
HighlightInfo info = ContainerUtil.find(xInfos, xInfo -> xInfo.getDescription().equals("Method 'ffffffffffffff()' is never used"));
assertNull(xInfos.toString(), info);
Editor useEditor = myEditor;
List<HighlightInfo> useInfos = filter(CodeInsightTestFixtureImpl.instantiateAndRun(use, useEditor, new int[0], false), HighlightSeverity.ERROR);
assertEmpty(useInfos);
type('/');
type('/');
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
xInfos = filter(CodeInsightTestFixtureImpl.instantiateAndRun(x, xEditor, new int[0], false), HighlightSeverity.WARNING);
info = ContainerUtil.find(xInfos, xInfo -> xInfo.getDescription().equals("Method 'ffffffffffffff()' is never used"));
assertNotNull(xInfos.toString(), info);
}
private void enableDeadCodeInspection() {
InspectionProfile profile = InspectionProjectProfileManager.getInstance(myProject).getCurrentProfile();
HighlightDisplayKey myDeadCodeKey = HighlightDisplayKey.findOrRegister(UnusedDeclarationInspectionBase.SHORT_NAME,
UnusedDeclarationInspectionBase.getDisplayNameText(), UnusedDeclarationInspectionBase.SHORT_NAME);
UnusedDeclarationInspectionBase myDeadCodeInspection = new UnusedDeclarationInspectionBase(true);
enableInspectionTool(myDeadCodeInspection);
assert profile.isToolEnabled(myDeadCodeKey, myFile);
}
public void testErrorDisappearsRightAfterTypingInsideVisibleAreaWhileDaemonContinuesToChugAlong() {
String text = "class X{\nint xxx;\n{\nint i = <selection>null</selection><caret>;\n" + StringUtil.repeat("{ this.hashCode(); }\n\n\n", 10000) + "}}";
configureByText(JavaFileType.INSTANCE, text);
((EditorImpl)myEditor).getScrollPane().getViewport().setSize(100, 100);
DaemonCodeAnalyzerSettings.getInstance().setImportHintEnabled(true);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
((EditorImpl)myEditor).getScrollPane().getViewport().setViewPosition(new Point(0, 0));
((EditorImpl)myEditor).getScrollPane().getViewport().setExtentSize(new Dimension(100, 100000));
@NotNull Editor editor = getEditor();
ProperTextRange visibleRange = editor.calculateVisibleRange();
assertTrue(visibleRange.getLength() > 0);
Document document = myEditor.getDocument();
assertTrue(visibleRange.getLength() < document.getTextLength());
HighlightInfo info = assertOneElement(highlightErrors());
final String errorDescription = "Incompatible types. Found: 'null', required: 'int'";
assertEquals(errorDescription, info.getDescription());
MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, false);
AtomicBoolean errorRemoved = new AtomicBoolean();
model.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
@Override
public void afterRemoved(@NotNull RangeHighlighterEx highlighter) {
HighlightInfo info = HighlightInfo.fromRangeHighlighter(highlighter);
if (info == null) return;
String description = info.getDescription();
if (errorDescription.equals(description)) {
errorRemoved.set(true);
List<ProgressableTextEditorHighlightingPass> passes = myDaemonCodeAnalyzer.getPassesToShowProgressFor(document);
GeneralHighlightingPass ghp = null;
for (TextEditorHighlightingPass pass : passes) {
if (pass instanceof GeneralHighlightingPass && pass.getId() == Pass.UPDATE_ALL) {
assert ghp == null : ghp;
ghp = (GeneralHighlightingPass)pass;
}
}
assertNotNull(ghp);
boolean finished = ghp.isFinished();
assertFalse(finished);
}
}
});
type("1");
assertEmpty(highlightErrors());
assertTrue(errorRemoved.get());
}
public void testDaemonWorksForDefaultProjectSinceItIsNeededInSettingsDialogForSomeReason() {
assertNotNull(DaemonCodeAnalyzer.getInstance(ProjectManager.getInstance().getDefaultProject()));
}
public void testChangeEventsAreNotAlwaysGeneric() {
String body = """
class X {
<caret> @org.PPP
void dtoArrayDouble() {
}
}""";
configureByText(JavaFileType.INSTANCE, body);
makeEditorWindowVisible(new Point(), myEditor);
assertNotEmpty(highlightErrors());
type("//");
assertEmpty(highlightErrors());
backspace();
backspace();
assertNotEmpty(highlightErrors());
}
public void testInterruptOnTyping() throws Throwable {
@NonNls String filePath = "/psi/resolve/Thinlet.java";
configureByFile(filePath);
assertEmpty(highlightErrors());
myDaemonCodeAnalyzer.restart();
try {
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
PsiFile file = getFile();
Editor editor = getEditor();
Project project = file.getProject();
CodeInsightTestFixtureImpl.ensureIndexesUpToDate(project);
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(editor);
Runnable callbackWhileWaiting = () -> type(' ');
myDaemonCodeAnalyzer.runPasses(file, editor.getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, true, callbackWhileWaiting);
}
catch (ProcessCanceledException ignored) {
return;
}
fail("PCE must have been thrown");
}
public void testTypingInsideCodeBlockDoesntLeadToCatastrophicUnusedEverything_Stress() throws Throwable {
InspectionProfileImpl profile = InspectionProfileManager.getInstance(getProject()).getCurrentProfile();
profile.disableAllTools(getProject());
@NonNls String filePath = "/psi/resolve/Thinlet.java";
configureByFile(filePath);
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
PsiFile file = getFile();
Project project = file.getProject();
CodeInsightTestFixtureImpl.ensureIndexesUpToDate(project);
List<HighlightInfo> errors = doHighlighting(HighlightSeverity.ERROR);
assertEmpty(errors);
List<HighlightInfo> initialWarnings = doHighlighting(HighlightSeverity.WARNING);
assertEmpty(initialWarnings);
int N_BLOCKS = codeBlocks(file).size();
assertTrue("codeblocks :"+N_BLOCKS, N_BLOCKS > 1000);
Random random = new Random();
int N = 10;
// try with both serialized and not-serialized passes
myDaemonCodeAnalyzer.serializeCodeInsightPasses(false);
for (int i=0; i<N*2; i++) {
PsiCodeBlock block = codeBlocks(file).get(random.nextInt(N_BLOCKS));
getEditor().getCaretModel().moveToOffset(block.getLBrace().getTextOffset() + 1);
type("\n/*xxx*/");
List<HighlightInfo> warnings = doHighlighting(HighlightSeverity.WARNING);
if (!warnings.isEmpty()) {
System.out.println("\n-----\n"+getEditor().getDocument().getText()+"\n--------\n");
}
assertEmpty(warnings);
if (i == N) {
// repeat the same steps with serialized passes
myDaemonCodeAnalyzer.serializeCodeInsightPasses(true);
}
}
}
@NotNull
private List<PsiCodeBlock> codeBlocks(@NotNull PsiFile file) {
List<PsiCodeBlock> blocks = new ArrayList<>();
file.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitCodeBlock(@NotNull PsiCodeBlock block) {
blocks.add(block);
super.visitCodeBlock(block);
}
});
return blocks;
}
public void testCodeFoldingInSplittedWindowAppliesToAllEditors() throws Exception {
Set<Editor> applied = ConcurrentCollectionFactory.createConcurrentSet();
Set<Editor> collected = ConcurrentCollectionFactory.createConcurrentSet();
registerFakePass(applied, collected);
configureByText(PlainTextFileType.INSTANCE, "");
Editor editor1 = getEditor();
Editor editor2 = EditorFactory.getInstance().createEditor(editor1.getDocument(),getProject());
Disposer.register(getProject(), () -> EditorFactory.getInstance().releaseEditor(editor2));
TextEditor textEditor1 = new PsiAwareTextEditorProvider().getTextEditor(editor1);
TextEditor textEditor2 = new PsiAwareTextEditorProvider().getTextEditor(editor2);
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor1, new int[0], false, null);
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor2, new int[0], false, null);
List<HighlightInfo> errors = DaemonCodeAnalyzerImpl.getHighlights(editor1.getDocument(), null, myProject);
assertEmpty(errors);
assertEquals(collected, ContainerUtil.newHashSet(editor1, editor2));
assertEquals(applied, ContainerUtil.newHashSet(editor1, editor2));
}
public void testHighlightingInSplittedWindowFinishesEventually() throws Exception {
myDaemonCodeAnalyzer.serializeCodeInsightPasses(true); // reproduced only for serialized passes
try {
Collection<Editor> applied = ContainerUtil.createConcurrentList();
Collection<Editor> collected = ContainerUtil.createConcurrentList();
registerFakePass(applied, collected);
@Language("JAVA")
String text = "class X {" + "\n".repeat(1000) +
"}";
configureByText(JavaFileType.INSTANCE, text);
Editor editor1 = getEditor();
Editor editor2 = EditorFactory.getInstance().createEditor(editor1.getDocument(),getProject());
Disposer.register(getProject(), () -> EditorFactory.getInstance().releaseEditor(editor2));
TextEditor textEditor1 = new PsiAwareTextEditorProvider().getTextEditor(editor1);
TextEditor textEditor2 = new PsiAwareTextEditorProvider().getTextEditor(editor2);
setActiveEditors(editor1, editor2);
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor1, new int[0], false, null);
myDaemonCodeAnalyzer.runPasses(myFile, editor1.getDocument(), textEditor2, new int[0], false, null);
assertSameElements(collected, Arrays.asList(editor1, editor2));
assertSameElements(applied, Arrays.asList(editor1, editor2));
applied.clear();
collected.clear();
setActiveEditors(editor1, editor2);
type("/* xxx */");
waitForDaemon(myProject, myEditor.getDocument());
assertSameElements(collected, Arrays.asList(editor1, editor2));
assertSameElements(applied, Arrays.asList(editor1, editor2));
}
finally {
myDaemonCodeAnalyzer.serializeCodeInsightPasses(false);
}
}
private void registerFakePass(@NotNull Collection<? super Editor> applied, @NotNull Collection<? super Editor> collected) {
class Fac implements TextEditorHighlightingPassFactory {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
return new EditorBoundHighlightingPass(editor, file, false) {
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
collected.add(editor);
}
@Override
public void doApplyInformationToEditor() {
applied.add(editor);
}
};
}
}
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
registrar.registerTextEditorHighlightingPass(new Fac(), null, null, false, -1);
}
private volatile boolean runHeavyProcessing;
public void testDaemonDisablesItselfDuringHeavyProcessing() {
runWithReparseDelay(0, () -> {
runHeavyProcessing = false;
try {
Set<Editor> applied = Collections.synchronizedSet(new HashSet<>());
Set<Editor> collected = Collections.synchronizedSet(new HashSet<>());
registerFakePass(applied, collected);
configureByText(PlainTextFileType.INSTANCE, "");
Editor editor = getEditor();
EditorTracker editorTracker = EditorTracker.Companion.getInstance(myProject);
setActiveEditors(editor);
while (HeavyProcessLatch.INSTANCE.isRunning()) {
UIUtil.dispatchAllInvocationEvents();
}
type("xxx"); // restart daemon
assertTrue(editorTracker.getActiveEditors().contains(editor));
assertSame(editor, FileEditorManager.getInstance(myProject).getSelectedTextEditor());
// wait for the first pass to complete
long start = System.currentTimeMillis();
while (myDaemonCodeAnalyzer.isRunning() || !applied.contains(editor)) {
UIUtil.dispatchAllInvocationEvents();
if (System.currentTimeMillis() - start > 1000000) {
fail("Too long waiting for daemon");
}
}
runHeavyProcessing = true;
ApplicationManager.getApplication().executeOnPooledThread(() ->
HeavyProcessLatch.INSTANCE.performOperation(HeavyProcessLatch.Type.Processing, "my own heavy op", ()-> {
while (runHeavyProcessing) {
}
})
);
while (!HeavyProcessLatch.INSTANCE.isRunning()) {
UIUtil.dispatchAllInvocationEvents();
}
applied.clear();
collected.clear();
type("xxx"); // try to restart daemon
start = System.currentTimeMillis();
while (System.currentTimeMillis() < start + 5000) {
assertEmpty(applied); // it should not restart
assertEmpty(collected);
UIUtil.dispatchAllInvocationEvents();
}
}
finally {
runHeavyProcessing = false;
}
});
}
public void testDaemonDoesNotDisableItselfDuringVFSRefresh() {
runWithReparseDelay(0, () -> {
runHeavyProcessing = false;
try {
Set<Editor> applied = Collections.synchronizedSet(new HashSet<>());
Set<Editor> collected = Collections.synchronizedSet(new HashSet<>());
registerFakePass(applied, collected);
configureByText(PlainTextFileType.INSTANCE, "");
Editor editor = getEditor();
EditorTracker editorTracker = EditorTracker.Companion.getInstance(myProject);
setActiveEditors(editor);
while (HeavyProcessLatch.INSTANCE.isRunning()) {
UIUtil.dispatchAllInvocationEvents();
}
type("xxx"); // restart daemon
assertTrue(editorTracker.getActiveEditors().contains(editor));
assertSame(editor, FileEditorManager.getInstance(myProject).getSelectedTextEditor());
// wait for the first pass to complete
long start = System.currentTimeMillis();
while (myDaemonCodeAnalyzer.isRunning() || !applied.contains(editor)) {
UIUtil.dispatchAllInvocationEvents();
if (System.currentTimeMillis() - start > 1000000) {
fail("Too long waiting for daemon");
}
}
runHeavyProcessing = true;
Future<?> future = ApplicationManager.getApplication().executeOnPooledThread(() ->
HeavyProcessLatch.INSTANCE.performOperation(HeavyProcessLatch.Type.Syncing, "my own vfs refresh", () -> {
while (runHeavyProcessing) {
Thread.yield();
}
})
);
while (!HeavyProcessLatch.INSTANCE.isRunning()) {
UIUtil.dispatchAllInvocationEvents();
}
applied.clear();
collected.clear();
type("xxx"); // try to restart daemon
doHighlighting();
assertNotEmpty(applied); // it should restart
assertNotEmpty(collected);
runHeavyProcessing = false;
try {
future.get();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
finally {
runHeavyProcessing = false;
}
});
}
public void testDaemonMustDisableItselfDuringDocumentBulkModification() {
runWithReparseDelay(0, () -> {
configureByText(PlainTextFileType.INSTANCE, "");
Editor editor = getEditor();
Set<Editor> applied = Collections.synchronizedSet(new HashSet<>());
Set<Editor> collected = Collections.synchronizedSet(new HashSet<>());
DocumentUtil.executeInBulk(editor.getDocument(), () -> {
registerFakePass(applied, collected);
EditorTracker editorTracker = EditorTracker.Companion.getInstance(myProject);
setActiveEditors(editor);
assertTrue(editorTracker.getActiveEditors().contains(editor));
assertSame(editor, FileEditorManager.getInstance(myProject).getSelectedTextEditor());
applied.clear();
collected.clear();
myDaemonCodeAnalyzer.restart(); // try to restart daemon
long start = System.currentTimeMillis();
while (System.currentTimeMillis() < start + 5000) {
assertEmpty(applied); // it must not restart
assertEmpty(collected);
UIUtil.dispatchAllInvocationEvents();
}
});
applied.clear();
collected.clear();
myDaemonCodeAnalyzer.restart(); // try to restart daemon
long start = System.currentTimeMillis();
while (System.currentTimeMillis() < start + 5000 && applied.isEmpty()) {
UIUtil.dispatchAllInvocationEvents();
}
assertNotEmpty(applied); // it must restart outside bulk
assertNotEmpty(collected);
});
}
public void testModificationInsideCodeBlockDoesNotRehighlightWholeFile() {
configureByText(JavaFileType.INSTANCE, """
class X {
int f = "error";
int f() {
return 11<caret>;
}
}""");
HighlightInfo error = assertOneElement(highlightErrors());
assertEquals("Incompatible types. Found: 'java.lang.String', required: 'int'", error.getDescription());
error.getHighlighter().dispose();
assertEmpty(highlightErrors());
type("23");
assertEmpty(highlightErrors());
myEditor.getCaretModel().moveToOffset(0);
type("/* */");
HighlightInfo error2 = assertOneElement(highlightErrors());
assertEquals("Incompatible types. Found: 'java.lang.String', required: 'int'", error2.getDescription());
}
public void testModificationInExcludedFileDoesNotCauseRehighlight() {
@Language("JAVA")
String text = "class EEE { void f(){} }";
VirtualFile excluded = configureByText(JavaFileType.INSTANCE, text).getVirtualFile();
PsiTestUtil.addExcludedRoot(myModule, excluded.getParent());
configureByText(JavaFileType.INSTANCE, "class X { <caret> }");
assertEmpty(highlightErrors());
FileStatusMap me = DaemonCodeAnalyzerEx.getInstanceEx(getProject()).getFileStatusMap();
TextRange scope = me.getFileDirtyScope(getEditor().getDocument(), getFile(), Pass.UPDATE_ALL);
assertNull(scope);
WriteCommandAction.runWriteCommandAction(getProject(), () -> ((PsiJavaFile)PsiManager.getInstance(myProject).findFile(excluded)).getClasses()[0].getMethods()[0].delete());
UIUtil.dispatchAllInvocationEvents();
scope = me.getFileDirtyScope(getEditor().getDocument(), getFile(), Pass.UPDATE_ALL);
assertNull(scope);
}
public void testModificationInWorkspaceXmlDoesNotCauseRehighlight() {
configureByText(JavaFileType.INSTANCE, "class X { <caret> }");
StoreUtilKt.runInAllowSaveMode(true, () -> {
StoreUtil.saveDocumentsAndProjectsAndApp(true);
VirtualFile workspaceFile = Objects.requireNonNull(getProject().getWorkspaceFile());
PsiFile excluded = Objects.requireNonNull(PsiManager.getInstance(getProject()).findFile(workspaceFile));
assertEmpty(highlightErrors());
FileStatusMap me = DaemonCodeAnalyzerEx.getInstanceEx(getProject()).getFileStatusMap();
TextRange scope = me.getFileDirtyScope(getEditor().getDocument(), getFile(), Pass.UPDATE_ALL);
assertNull(scope);
WriteCommandAction.runWriteCommandAction(getProject(), () -> {
Document document = Objects.requireNonNull(PsiDocumentManager.getInstance(getProject()).getDocument(excluded));
document.insertString(0, "<!-- dsfsd -->");
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
});
UIUtil.dispatchAllInvocationEvents();
scope = me.getFileDirtyScope(getEditor().getDocument(), getFile(), Pass.UPDATE_ALL);
assertNull(scope);
return Unit.INSTANCE;
});
}
static void runWithReparseDelay(int reparseDelayMs, @NotNull Runnable task) {
DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance();
int oldDelay = settings.getAutoReparseDelay();
settings.setAutoReparseDelay(reparseDelayMs);
try {
task.run();
}
finally {
settings.setAutoReparseDelay(oldDelay);
}
}
public void testCodeFoldingPassRestartsOnRegionUnfolding() {
runWithReparseDelay(0, () -> {
@Language("JAVA")
String text = """
class Foo {
void m() {
}
}""";
configureByText(JavaFileType.INSTANCE, text);
EditorTestUtil.buildInitialFoldingsInBackground(myEditor);
waitForDaemon(myProject, myEditor.getDocument());
EditorTestUtil.executeAction(myEditor, IdeActions.ACTION_COLLAPSE_ALL_REGIONS);
waitForDaemon(myProject, myEditor.getDocument());
checkFoldingState("[FoldRegion +(25:33), placeholder='{}']");
WriteCommandAction.runWriteCommandAction(myProject, () -> myEditor.getDocument().insertString(0, "/*"));
waitForDaemon(myProject, myEditor.getDocument());
checkFoldingState("[FoldRegion -(0:37), placeholder='/.../', FoldRegion +(27:35), placeholder='{}']");
EditorTestUtil.executeAction(myEditor, IdeActions.ACTION_EXPAND_ALL_REGIONS);
waitForDaemon(myProject, myEditor.getDocument());
checkFoldingState("[FoldRegion -(0:37), placeholder='/.../']");
});
}
public void testChangingSettingsHasImmediateEffectOnOpenedEditor() {
runWithReparseDelay(0, () -> {
@Language("JAVA")
String text = """
class C {\s
void m() {
}\s
}""";
configureByText(JavaFileType.INSTANCE, text);
EditorTestUtil.buildInitialFoldingsInBackground(myEditor);
waitForDaemon(myProject, myEditor.getDocument());
checkFoldingState("[FoldRegion -(22:27), placeholder='{}']");
JavaCodeFoldingSettings settings = JavaCodeFoldingSettings.getInstance();
boolean savedValue = settings.isCollapseMethods();
try {
settings.setCollapseMethods(true);
CodeFoldingConfigurable.Util.applyCodeFoldingSettingsChanges();
waitForDaemon(myProject, myEditor.getDocument());
checkFoldingState("[FoldRegion +(22:27), placeholder='{}']");
}
finally {
settings.setCollapseMethods(savedValue);
}
});
}
private void checkFoldingState(String expected) {
assertEquals(expected, Arrays.toString(myEditor.getFoldingModel().getAllFoldRegions()));
}
static void waitForDaemon(@NotNull Project project, @NotNull Document document) {
ThreadingAssertions.assertEventDispatchThread();
long start = System.currentTimeMillis();
long deadline = start + 60_000;
while (!daemonIsWorkingOrPending(project, document)) {
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
if (System.currentTimeMillis() > deadline) {
fail("Too long waiting for daemon to start");
}
}
while (daemonIsWorkingOrPending(project, document)) {
if (System.currentTimeMillis() > deadline) {
DaemonRespondToChangesPerformanceTest.dumpThreadsToConsole();
fail("Too long waiting for daemon to finish ("+(System.currentTimeMillis()-start)+"ms already)");
}
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
}
}
static boolean daemonIsWorkingOrPending(@NotNull Project project, @NotNull Document document) {
DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(project);
return codeAnalyzer.isRunningOrPending() || PsiDocumentManager.getInstance(project).isUncommited(document);
}
public void testRehighlightInDebuggerExpressionFragment() {
PsiExpressionCodeFragment fragment = JavaCodeFragmentFactory.getInstance(getProject()).createExpressionCodeFragment("+ <caret>\"a\"", null,
PsiType.getJavaLangObject(getPsiManager(), GlobalSearchScope.allScope(getProject())), true);
myFile = fragment;
Document document = Objects.requireNonNull(PsiDocumentManager.getInstance(getProject()).getDocument(fragment));
myEditor = EditorFactory.getInstance().createEditor(document, getProject(), JavaFileType.INSTANCE, false);
ProperTextRange visibleRange = makeEditorWindowVisible(new Point(0, 0), myEditor);
assertEquals(document.getTextLength(), visibleRange.getLength());
try {
EditorInfo editorInfo = new EditorInfo(document.getText());
String newFileText = editorInfo.getNewFileText();
ApplicationManager.getApplication().runWriteAction(() -> {
if (!document.getText().equals(newFileText)) {
document.setText(newFileText);
}
editorInfo.applyToEditor(myEditor);
});
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
HighlightInfo error = assertOneElement(highlightErrors());
assertEquals("Operator '+' cannot be applied to 'java.lang.String'", error.getDescription());
type(" ");
HighlightInfo after = assertOneElement(highlightErrors());
assertEquals("Operator '+' cannot be applied to 'java.lang.String'", after.getDescription());
}
finally {
EditorFactory.getInstance().releaseEditor(myEditor);
}
}
public void testFileReload() throws Exception {
VirtualFile file = createFile("a.java", "").getVirtualFile();
Document document = getDocument(file);
assertNotNull(document);
FileStatusMap fileStatusMap = myDaemonCodeAnalyzer.getFileStatusMap();
WriteCommandAction.runWriteCommandAction(getProject(), () -> {
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
GCWatcher.tracking(documentManager.getCachedPsiFile(document)).ensureCollected();
assertNull(documentManager.getCachedPsiFile(document));
@Language("JAVA")
String text = "class X { void foo() {}}";
document.insertString(0, text);
documentManager.commitAllDocuments();
assertEquals(TextRange.from(0, document.getTextLength()), fileStatusMap.getFileDirtyScope(document, documentManager.getPsiFile(document), Pass.UPDATE_ALL));
FileContentUtilCore.reparseFiles(file);
assertEquals(TextRange.from(0, document.getTextLength()), fileStatusMap.getFileDirtyScope(document, documentManager.getPsiFile(document), Pass.UPDATE_ALL));
findClass("X").getMethods()[0].delete();
assertEquals(TextRange.from(0, document.getTextLength()), fileStatusMap.getFileDirtyScope(document, documentManager.getPsiFile(document), Pass.UPDATE_ALL));
});
}
public void testDumbAwareHighlightingPassesStartEvenInDumbMode() {
List<TextEditorHighlightingPassFactory> collected = Collections.synchronizedList(new ArrayList<>());
List<TextEditorHighlightingPassFactory> applied = Collections.synchronizedList(new ArrayList<>());
class DumbFac implements TextEditorHighlightingPassFactory, DumbAware {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
return new TestDumbAwareHighlightingPassesStartEvenInDumbModePass(editor, file);
}
class TestDumbAwareHighlightingPassesStartEvenInDumbModePass extends EditorBoundHighlightingPass implements DumbAware {
TestDumbAwareHighlightingPassesStartEvenInDumbModePass(Editor editor, PsiFile file) {
super(editor, file, false);
}
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
collected.add(DumbFac.this);
}
@Override
public void doApplyInformationToEditor() {
applied.add(DumbFac.this);
}
}
}
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
DumbFac dumbFac = new DumbFac();
registrar.registerTextEditorHighlightingPass(dumbFac, null, null, false, -1);
class SmartFac implements TextEditorHighlightingPassFactory {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
return new EditorBoundHighlightingPass(editor, file, false) {
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
collected.add(SmartFac.this);
}
@Override
public void doApplyInformationToEditor() {
applied.add(SmartFac.this);
}
};
}
}
SmartFac smartFac = new SmartFac();
registrar.registerTextEditorHighlightingPass(smartFac, null, null, false, -1);
configureByText(PlainTextFileType.INSTANCE, "");
doHighlighting();
assertSameElements(collected, dumbFac, smartFac);
assertSameElements(applied, dumbFac, smartFac);
collected.clear();
applied.clear();
myDaemonCodeAnalyzer.mustWaitForSmartMode(false, getTestRootDisposable());
DumbModeTestUtils.runInDumbModeSynchronously(myProject, () -> {
type(' ');
doHighlighting();
TextEditorHighlightingPassFactory f = assertOneElement(collected);
assertSame(dumbFac, f);
TextEditorHighlightingPassFactory f2 = assertOneElement(applied);
assertSame(dumbFac, f2);
});
}
public void testUncommittedByAccidentNonPhysicalDocumentMustNotHangDaemon() {
ThreadingAssertions.assertEventDispatchThread();
configureByText(JavaFileType.INSTANCE, "class X { void f() { <caret> } }");
assertEmpty(highlightErrors());
assertFalse(ApplicationManager.getApplication().isWriteAccessAllowed());
PsiFile original = getPsiManager().findFile(getTempDir().createVirtualFile("X.txt", ""));
assertNotNull(original);
assertTrue(original.getViewProvider().isEventSystemEnabled());
PsiFile copy = (PsiFile)original.copy();
assertFalse(copy.getViewProvider().isEventSystemEnabled());
Document document = copy.getViewProvider().getDocument();
assertNotNull(document);
String text = "class A{}";
document.setText(text);
assertFalse(PsiDocumentManager.getInstance(myProject).isCommitted(document));
assertTrue(PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments());
type("String i=0;");
waitForDaemon(myProject, myEditor.getDocument());
assertNotEmpty(DaemonCodeAnalyzerImpl.getHighlights(getEditor().getDocument(), HighlightSeverity.ERROR, getProject()));
assertEquals(text, document.getText()); // retain non-phys document until after highlighting
assertFalse(PsiDocumentManager.getInstance(myProject).isCommitted(document));
assertTrue(PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments());
}
public void testPutArgumentsOnSeparateLinesIntentionMustNotRemoveErrorHighlighting() {
configureByText(JavaFileType.INSTANCE, "class X{ static void foo(String s1, String s2, String s3) { foo(\"1\", 1.2, \"2\"<caret>); }}");
assertOneElement(highlightErrors());
List<IntentionAction> fixes = LightQuickFixTestCase.getAvailableActions(getEditor(), getFile());
IntentionAction intention = assertContainsOneOf(fixes, "Put arguments on separate lines");
assertNotNull(intention);
WriteCommandAction.runWriteCommandAction(getProject(), () -> intention.invoke(getProject(), getEditor(), getFile()));
assertOneElement(highlightErrors());
}
public void testHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsiveness() throws Throwable {
AtomicReference<Throwable> violation = new AtomicReference<>();
AtomicBoolean applied = new AtomicBoolean();
class MyCheckingConstructorTraceFac implements TextEditorHighlightingPassFactory {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
return new TestHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsivenessPass(myProject);
}
final class TestHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsivenessPass extends TextEditorHighlightingPass {
private TestHighlightingPassesAreInstantiatedOutsideEDTToImproveResponsivenessPass(Project project) {
super(project, getEditor().getDocument(), false);
if (ApplicationManager.getApplication().isDispatchThread()) {
violation.set(new Throwable());
}
}
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
}
@Override
public void doApplyInformationToEditor() {
applied.set(true);
}
}
}
TextEditorHighlightingPassRegistrar registrar = TextEditorHighlightingPassRegistrar.getInstance(getProject());
registrar.registerTextEditorHighlightingPass(new MyCheckingConstructorTraceFac(), null, null, false, -1);
configureByText(JavaFileType.INSTANCE, "class C{}");
assertEmpty(highlightErrors());
assertTrue(applied.get());
if (violation.get() != null) {
throw violation.get();
}
}
private static class EmptyPassFactory implements TextEditorHighlightingPassFactory {
@Override
public TextEditorHighlightingPass createHighlightingPass(@NotNull PsiFile file, @NotNull Editor editor) {
return new EmptyPass(file.getProject(), editor.getDocument());
}
static class EmptyPass extends TextEditorHighlightingPass {
private EmptyPass(Project project, @NotNull Document document) {
super(project, document, false);
}
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
}
@Override
public void doApplyInformationToEditor() {
}
}
}
public void testTextEditorHighlightingPassRegistrarMustNotAllowCyclesInPassDeclarationsOrCrazyPassIdsOmgMurphyLawStrikesAgain() {
configureByText(JavaFileType.INSTANCE, "class C{}");
TextEditorHighlightingPassRegistrarImpl registrar = (TextEditorHighlightingPassRegistrarImpl)TextEditorHighlightingPassRegistrar.getInstance(getProject());
int F2 = Pass.EXTERNAL_TOOLS;
int forcedId1 = 256;
int forcedId2 = 257;
int forcedId3 = 258;
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not intersect
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{F2}, false, -1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain forcedId
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain forcedId
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), null, new int[]{forcedId1}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain crazy ids
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{0}, new int[]{F2}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain crazy ids
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{-1}, new int[]{F2}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain crazy ids
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{0}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain crazy ids
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{-1}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain crazy ids
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{32134}, new int[]{F2}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () ->
// afterCompletionOf and afterStartingOf must not contain crazy ids
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{F2}, new int[]{982314}, false, forcedId1));
assertThrows(IllegalArgumentException.class, () -> {
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId2}, null, false, forcedId1);
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId2);
assertEmpty(highlightErrors());
});
// non-direct cycle
assertThrows(IllegalArgumentException.class, () -> {
registrar.reRegisterFactories(); // clear caches from incorrect factories above
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId2}, null, false, forcedId1);
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId3}, null, false, forcedId2);
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId3);
assertEmpty(highlightErrors());
});
registrar.reRegisterFactories(); // clear caches from incorrect factories above
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), null, null, false, forcedId1);
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId1}, null, false, forcedId3);
registrar.registerTextEditorHighlightingPass(new EmptyPassFactory(), new int[]{forcedId3}, null, false, forcedId2);
assertEmpty(highlightErrors());
}
public void testHighlightersMustDisappearWhenTheHighlightingIsSwitchedOff() {
@Language("JAVA")
String text = """
class X {
blah blah
)(@*$)(*%@$)
}""";
configureByText(JavaFileType.INSTANCE, text);
assertNotEmpty(highlightErrors());
HighlightingSettingsPerFile.getInstance(getProject()).setHighlightingSettingForRoot(getFile(), FileHighlightingSetting.SKIP_HIGHLIGHTING);
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"));
}
public void testTypingInsideCodeBlockCanAffectUnusedDeclarationInTheOtherClass() {
enableInspectionTool(new UnusedSymbolLocalInspection());
enableDeadCodeInspection();
configureByFiles(null, BASE_PATH+getTestName(true)+"/p2/A2222.java", BASE_PATH+getTestName(true)+"/p1/A1111.java");
assertEquals("A2222.java", getFile().getName());
HighlightInfo info = assertOneElement(doHighlighting(HighlightSeverity.WARNING));
assertEquals("Class 'A2222' is never used", info.getDescription());
Document document1111 = getFile().getParent().findFile("A1111.java").getFileDocument();
// uncomment (inside code block) the reference to A2222
WriteCommandAction.writeCommandAction(myProject).run(()->document1111.deleteString(document1111.getText().indexOf("//"), document1111.getText().indexOf("//")+2));
// now A2222 is no longer unused
assertEmpty(doHighlighting(HighlightSeverity.WARNING));
}
// test the other type of PSI change: child remove/child add
public void testTypingInsideCodeBlockCanAffectUnusedDeclarationInTheOtherClass2() {
enableInspectionTool(new UnusedSymbolLocalInspection());
enableDeadCodeInspection();
configureByFiles(null, BASE_PATH+getTestName(true)+"/p1/A1111.java", BASE_PATH+getTestName(true)+"/p2/A2222.java");
assertEquals("A1111.java", getFile().getName());
makeEditorWindowVisible(new Point(0, 1000), myEditor);
HighlightInfo info = assertOneElement(doHighlighting(HighlightSeverity.WARNING));
assertEquals("Method 'foo()' is never used", info.getDescription());
Document document2222 = getFile().getParent().findFile("A2222.java").getFileDocument();
// uncomment (inside code block) the reference to A1111
WriteCommandAction.writeCommandAction(myProject).run(()->document2222.deleteString(document2222.getText().indexOf("//"), document2222.getText().indexOf("//")+2));
// now foo() is no longer unused
assertEmpty(doHighlighting(HighlightSeverity.WARNING));
}
public void testTypingDoesNotLeaveInvalidPSIShitBehind() {
String text = """
class X {
void f() {
///
int s;<caret>
///
}
}""";
String bigText = text.replaceAll("///\n", "hashCode();\n".repeat(1000));
configureByText(JavaFileType.INSTANCE, bigText);
makeEditorWindowVisible(new Point(0, 1000), myEditor);
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
MarkupModel markupModel = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
for (int i=0; i<10; i++) {
type(" // TS");
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
assertEmpty(getErrorsFromMarkup(markupModel));
backspace();backspace();backspace();backspace();backspace();backspace();
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
assertEmpty(getErrorsFromMarkup(markupModel));
}
}
// highlights all //XXX, but very slow
public static class MyVerySlowAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
private static final AtomicBoolean wait = new AtomicBoolean();
private static final String SWEARING = "No swearing";
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PsiComment && element.getText().equals("//XXX")) {
while (wait.get()) {
Thread.yield();
}
holder.newAnnotation(HighlightSeverity.ERROR, SWEARING).range(element).create();
iDidIt();
}
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
}
static List<HighlightInfo> myHighlights(MarkupModel markupModel) {
return Arrays.stream(markupModel.getAllHighlighters())
.map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter))
.filter(Objects::nonNull)
.filter(info -> SWEARING.equals(info.getDescription())).toList();
}
static List<HighlightInfo> syntaxHighlights(MarkupModel markupModel, String description) {
List<HighlightInfo> errors = Arrays.stream(markupModel.getAllHighlighters())
.map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter))
.filter(Objects::nonNull)
.filter(info -> description.equals(info.getDescription())).toList();
return errors;
}
}
public void testInvalidPSIElementsCreatedByTypingNearThemMustBeRemovedImmediatelyMeaningLongBeforeTheHighlightingPassFinished() {
@Language("JAVA")
String text = """
class X {
void f() {
//XXX
int s-<caret>; // ';' expected
}
}""";
assertInvalidPSIElementHighlightingIsRemovedImmediatelyAfterRepairingChange(text, "';' expected", () -> backspace());
}
public void testInvalidPSIElementsCreatedByTypingNearThemMustBeRemovedImmediatelyMeaningLongBeforeTheHighlightingPassFinished2() {
@Language("JAVA")
String text = """
class X {
void f() {
//XXX
<caret> # // Unexpected token
}
}""";
assertInvalidPSIElementHighlightingIsRemovedImmediatelyAfterRepairingChange(text, "Unexpected token", () -> type("//"));
}
private void assertInvalidPSIElementHighlightingIsRemovedImmediatelyAfterRepairingChange(@Language("JAVA") String text,
String errorDescription,
Runnable repairingChange // the change which is supposed to fix the invalid PSI highlight
) {
configureByText(JavaFileType.INSTANCE, text);
makeEditorWindowVisible(new Point(0, 1000), myEditor);
MyVerySlowAnnotator.wait.set(false);
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyVerySlowAnnotator()}, ()->{
MarkupModel markupModel = DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
doHighlighting();
assertNotEmpty(MyVerySlowAnnotator.syntaxHighlights(markupModel, errorDescription));
assertNotEmpty(MyVerySlowAnnotator.myHighlights(markupModel));
MyVerySlowAnnotator.wait.set(true);
repairingChange.run(); //repair invalid psi
AtomicBoolean success = new AtomicBoolean();
// register very slow annotator and make sure the invalid PSI highlighting was removed before this annotator finished
TestTimeOut n = TestTimeOut.setTimeout(100, TimeUnit.SECONDS);
Runnable checkHighlighted = () -> {
UIUtil.dispatchAllInvocationEvents();
if (MyVerySlowAnnotator.syntaxHighlights(markupModel, errorDescription).isEmpty() && MyVerySlowAnnotator.wait.get()) {
// removed before highlighting is finished
MyVerySlowAnnotator.wait.set(false);
success.set(true);
}
if (n.isTimedOut()) {
MyVerySlowAnnotator.wait.set(false);
throw new RuntimeException(new TimeoutException(ThreadDumper.dumpThreadsToString()));
}
};
TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor());
try {
myDaemonCodeAnalyzer.runPasses(myFile, myEditor.getDocument(), textEditor, new int[0], false, checkHighlighted);
}
catch (Exception e) {
throw new RuntimeException(e);
}
assertEmpty(MyVerySlowAnnotator.syntaxHighlights(markupModel, errorDescription));
assertNotEmpty(MyVerySlowAnnotator.myHighlights(markupModel));
assertTrue(success.get());
});
}
@Language(value = "JAVA", prefix="class X { void foo() {\n", suffix = "\n}\n}")
String MANY_LAMBDAS_TEXT_TO_TYPE = """
if (i(()->{
i(()-> {
System.out.println("vFile = ");
});
})) {
i(new Runnable() {
@Override public void run() {
if (true==true) { return;
}
}
});
}
else {
// return this
}
""";
@Language("JAVA")
String MANY_LAMBDAS_INITIAL = """
class X {
void invokeLater(Runnable r) {}
boolean i(Runnable r) { return true;}
void foo() {
<caret>
}
}""";
public void testDaemonDoesRestartDuringMadMonkeyTyping/*Stress*/() {
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(MANY_LAMBDAS_INITIAL, MANY_LAMBDAS_TEXT_TO_TYPE, null);
}
@Language(value = "JAVA", prefix="class X { void foo() {\n", suffix = "\n}\n}")
String LONG_LINE_WITH_PARENS_TEXT_TO_TYPE = """
if (highlighter != null) highlighter += " text='" + StringUtil.first(getText(), 40, true) + "'";
""";
@Language("JAVA")
String LONG_LINE_WITH_PARENS_INITIAL_TEXT = """
class X {
static String getText() { return ""; }
static class StringUtil {
static String first(String t, int length, boolean b) { return t; }
}
String highlighter;
void foo() {
<caret>
}
}""";
public void testDaemonDoesRestartDuringMadMonkeyTyping2/*Stress*/() {
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(LONG_LINE_WITH_PARENS_INITIAL_TEXT, LONG_LINE_WITH_PARENS_TEXT_TO_TYPE, null);
}
public void testDaemonDoesNotLeaveObsoleteErrorElementHighlightsBehind/*Stress*/() {
Random random = new Random();
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(MANY_LAMBDAS_INITIAL, MANY_LAMBDAS_TEXT_TO_TYPE, () -> TimeoutUtil.sleep(random.nextInt(10)));
}
public void testDaemonDoesNotLeaveObsoleteErrorElementHighlightsBehind2/*Stress*/() {
Random random = new Random();
assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(LONG_LINE_WITH_PARENS_INITIAL_TEXT, LONG_LINE_WITH_PARENS_TEXT_TO_TYPE, () -> TimeoutUtil.sleep(random.nextInt(10)));
}
// start typing in the empty java file char by char
// after each typing, wait for the daemon to start and immediately proceed to type the next char
// thus making daemon interrupt itself constantly, in hope for multiple highlighting sessions overlappings to manifest themselves more quickly.
// after all typings are over, wait for final highlighting to complete and check that no errors are left in the markup
private void assertDaemonRestartsAndLeavesNoErrorElementsInTheEnd(String initialText, String textToType, Runnable afterWaitForDaemon) {
// run expensive consistency checks on each typing
HighlightInfoUpdaterImpl.ASSERT_INVARIANTS = true; Disposer.register(getTestRootDisposable(), () -> HighlightInfoUpdaterImpl.ASSERT_INVARIANTS = false);
String finalText = initialText.replace("<caret>", textToType);
configureByText(JavaFileType.INSTANCE, finalText);
HighlightInfoUpdaterImpl updater = (HighlightInfoUpdaterImpl)HighlightInfoUpdater.getInstance(getProject());
assertEmpty(doHighlighting(HighlightSeverity.ERROR));
runWithReparseDelay(0, () -> {
for (int i=0; i<10; i++) {
//System.out.println("i = " + i);
PassExecutorService.LOG.debug("i = " + i);
WriteCommandAction.runWriteCommandAction(getProject(), () -> myEditor.getDocument().setText(" "));
doHighlighting(); // reset various optimizations e.g. FileStatusMap.getCompositeDocumentDirtyRange
MarkupModel markupModel = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
for (int c = 0; c < finalText.length(); c++) {
PassExecutorService.LOG.debug("c = " + c);
//System.out.println(" c = " + c);
int o=c;
//updater.assertNoDuplicates(myFile, getErrorsFromMarkup(markupModel), "errors from markup ");
WriteCommandAction.runWriteCommandAction(getProject(), () -> {
assertFalse(myDaemonCodeAnalyzer.isRunning());
type(finalText.charAt(o));
assertFalse(myDaemonCodeAnalyzer.isAllAnalysisFinished(myFile));
});
//updater.assertNoDuplicates(myFile, getErrorsFromMarkup(markupModel), "errors from markup ");
TestTimeOut t = TestTimeOut.setTimeout(30, TimeUnit.SECONDS);
myDaemonCodeAnalyzer.restart(myFile);
List<HighlightInfo> errorsFromMarkup = getErrorsFromMarkup(markupModel);
//updater.assertNoDuplicates(myFile, errorsFromMarkup, "errors from markup ");
//((HighlightInfoUpdaterImpl)HighlightInfoUpdater.getInstance(getProject())).assertMarkupDataConsistent(myFile);
PassExecutorService.LOG.debug(" errorsfrommarkup:\n" + StringUtil.join(ContainerUtil.sorted(errorsFromMarkup, Segment.BY_START_OFFSET_THEN_END_OFFSET), "\n") + "\n-----\n");
while (!myDaemonCodeAnalyzer.isRunning() && !myDaemonCodeAnalyzer.isAllAnalysisFinished(myFile)/*in case the highlighting has already finished miraculously by now*/) {
Thread.yield();
UIUtil.dispatchAllInvocationEvents();
if (t.timedOut()) throw new RuntimeException(new TimeoutException());
}
if (afterWaitForDaemon != null) {
afterWaitForDaemon.run();
}
}
// some chars might be inserted by TypeHandlers
while (!myEditor.getDocument().getText().substring(myEditor.getCaretModel().getOffset()).isEmpty()) {
delete(myEditor);
}
LOG.debug("All typing completed. " +
"\neditor text:-----------\n"+myEditor.getDocument().getText()+"\n-------\n"+
"errors in markup: " + StringUtil.join(getErrorsFromMarkup(markupModel), "\n") + "\n-----\n");
waitForDaemon(getProject(), myEditor.getDocument());
assertEmpty(myEditor.getDocument().getText(), getErrorsFromMarkup(markupModel));
}
});
}
private static @NotNull List<HighlightInfo> getErrorsFromMarkup(MarkupModel model) {
return Arrays.stream(model.getAllHighlighters())
.map(m -> HighlightInfo.fromRangeHighlighter(m))
.filter(Objects::nonNull)
.filter(h -> h.getSeverity() == HighlightSeverity.ERROR)
.toList();
}
public void testMultiplePSIInvalidationsMustDelayTheirHighlightersRemovalForShortTimeToAvoidFlickering() {
//IJPL-160136 Blinking highlighting on refactoring TS code
@Language("JAVA")
String text = """
class X {
int xxx;
void foo() {
for (int i=0; i<xxx+1; i++) {
if (i == xxx) return xxx;
}
}
public int hashCode() {
return xxx;
}
}""";
DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyXXXIdentifierAnnotator()}, ()->{
configureByText(JavaFileType.INSTANCE, text);
makeEditorWindowVisible(new Point(0, 1000), myEditor);
MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true);
doHighlighting();
assertSize(5, MyXXXIdentifierAnnotator.myHighlights(markupModel));
List<String> events = Collections.synchronizedList(new ArrayList<>());
markupModel.addMarkupModelListener(getTestRootDisposable(), new MarkupModelListener() {
@Override
public void afterAdded(@NotNull RangeHighlighterEx highlighter) {
events.add("added " + highlighter);
}
@Override
public void afterRemoved(@NotNull RangeHighlighterEx highlighter) {
events.add("removed " + highlighter);
}
});
// invalidate all xxx (leaving the text the same), check that these highlighters are recycled
List<PsiIdentifier> identifiers = new ArrayList<>();
getFile().accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitIdentifier(@NotNull PsiIdentifier identifier) {
super.visitIdentifier(identifier);
if (identifier.getText().equals("xxx")) identifiers.add(identifier);
}
});
for (PsiIdentifier identifier : identifiers) {
WriteCommandAction.writeCommandAction(getProject()).run(() -> identifier.replace(PsiElementFactory.getInstance(getProject()).createIdentifier("xxx")));
assertFalse(identifier.isValid());
}
doHighlighting();
assertEmpty(events);
});
}
// highlight all identifiers with text "xxx"
public static class MyXXXIdentifierAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator {
private static final String MSG = "xxx?";
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PsiIdentifier && element.getText().equals("xxx")) {
holder.newAnnotation(HighlightSeverity.ERROR, MSG).range(element).create();
iDidIt();
}
LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt());
}
static List<HighlightInfo> myHighlights(MarkupModel markupModel) {
return Arrays.stream(markupModel.getAllHighlighters())
.map(highlighter -> HighlightInfo.fromRangeHighlighter(highlighter))
.filter(Objects::nonNull)
.filter(info -> MSG.equals(info.getDescription())).toList();
}
}
}