Skip to content

Commit

Permalink
SLI-881 Group files in the same analysis when opening multiple files (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nquinquenel authored Oct 1, 2024
1 parent 980369b commit a5fbea7
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.sonarlint.intellij.fs.EditorFileChangeListener;
import org.sonarlint.intellij.promotion.PromotionProvider;
import org.sonarlint.intellij.trigger.EditorChangeTrigger;
import org.sonarlint.intellij.trigger.EditorOpenTrigger;

import static org.sonarlint.intellij.common.util.SonarLintUtils.getService;
import static org.sonarlint.intellij.util.ThreadUtilsKt.runOnPooledThread;
Expand All @@ -42,6 +43,7 @@ public void runActivity(@NotNull Project project) {
runOnPooledThread(project, () -> {
getService(EditorFileChangeListener.class).startListening();
getService(project, EditorChangeTrigger.class).onProjectOpened();
getService(project, EditorOpenTrigger.class).onProjectOpened();
getService(BackendService.class).projectOpened(project);
getService(project, SecurityHotspotsRefreshTrigger.class).subscribeToTriggeringEvents();
getService(project, PromotionProvider.class).subscribeToTriggeringEvents();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,19 @@
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.concurrent.ThreadSafe;
import org.sonarlint.intellij.analysis.AnalysisSubmitter;
import org.sonarlint.intellij.analysis.Cancelable;
import org.sonarlint.intellij.messages.AnalysisListener;
import org.sonarlint.intellij.util.SonarLintAppUtils;

import static org.sonarlint.intellij.common.util.SonarLintUtils.getService;
import static org.sonarlint.intellij.config.Settings.getGlobalSettings;
import static org.sonarlint.intellij.util.ThreadUtilsKt.runOnPooledThread;

@ThreadSafe
@Service(Service.Level.PROJECT)
public final class EditorChangeTrigger implements DocumentListener, Disposable {
private static final int DEFAULT_TIMER_MS = 2000;

// entries in this map mean that the file is "dirty"
private final ConcurrentHashMap<VirtualFile, Long> eventMap = new ConcurrentHashMap<>();
Expand All @@ -55,7 +48,7 @@ public final class EditorChangeTrigger implements DocumentListener, Disposable {

public EditorChangeTrigger(Project project) {
myProject = project;
watcher = new EventWatcher();
watcher = new EventWatcher(myProject, "change", eventMap, TriggerType.EDITOR_CHANGE, 2000);
}

public void onProjectOpened() {
Expand Down Expand Up @@ -105,71 +98,6 @@ Map<VirtualFile, Long> getEvents() {
return Collections.unmodifiableMap(eventMap);
}

private class EventWatcher extends Thread {

private boolean stop = false;
private Cancelable task;

EventWatcher() {
this.setDaemon(true);
this.setName("sonarlint-auto-trigger-" + myProject.getName());
}

public void stopWatcher() {
stop = true;
this.interrupt();
}

@Override
public void run() {
while (!stop) {
checkTimers();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// continue until stop flag is set
}
}
}

private void triggerFiles(List<VirtualFile> files) {
if (getGlobalSettings().isAutoTrigger()) {
var openFilesToAnalyze = SonarLintAppUtils.retainOpenFiles(myProject, files);
if (!openFilesToAnalyze.isEmpty()) {
if (task != null) {
task.cancel();
task = null;
return;
}
files.forEach(eventMap::remove);
task = getService(myProject, AnalysisSubmitter.class).autoAnalyzeFiles(openFilesToAnalyze, TriggerType.EDITOR_CHANGE);
}
}
}

private void checkTimers() {
var now = System.currentTimeMillis();

var it = eventMap.entrySet().iterator();
var filesToTrigger = new ArrayList<VirtualFile>();
while (it.hasNext()) {
var event = it.next();
if (!event.getKey().isValid()) {
it.remove();
continue;
}
// don't trigger if file currently has errors?
// filter files opened in the editor
// use some heuristics based on analysis time or average pauses? Or make it configurable?
if (event.getValue() + DEFAULT_TIMER_MS < now) {
filesToTrigger.add(event.getKey());
}
}
runOnPooledThread(myProject, () -> triggerFiles(filesToTrigger));
}

}

@Override
public void dispose() {
eventMap.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,59 @@
*/
package org.sonarlint.intellij.trigger;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.concurrent.ThreadSafe;
import org.jetbrains.annotations.NotNull;
import org.sonarlint.intellij.analysis.AnalysisSubmitter;
import org.sonarlint.intellij.core.BackendService;
import org.sonarlint.intellij.fs.VirtualFileEvent;
import org.sonarlint.intellij.messages.AnalysisListener;
import org.sonarlint.intellij.util.SonarLintAppUtils;
import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileEvent;

import static org.sonarlint.intellij.common.util.SonarLintUtils.getService;
import static org.sonarlint.intellij.util.ThreadUtilsKt.runOnPooledThread;

public class EditorOpenTrigger implements FileEditorManagerListener {
@ThreadSafe
@Service(Service.Level.PROJECT)
public final class EditorOpenTrigger implements FileEditorManagerListener, Disposable {

// entries in this map mean that the file is "dirty"
private final ConcurrentHashMap<VirtualFile, Long> eventMap = new ConcurrentHashMap<>();
private final EventWatcher watcher;
private final Project myProject;

public EditorOpenTrigger(Project project) {
myProject = project;
watcher = new EventWatcher(myProject, "open", eventMap, TriggerType.EDITOR_OPEN, 1000);
}

public void onProjectOpened() {
myProject.getMessageBus()
.connect()
.subscribe(AnalysisListener.TOPIC, new AnalysisListener.Adapter() {
@Override
public void started(Collection<VirtualFile> files, TriggerType trigger) {
removeFiles(files);
}
});
watcher.start();
myProject.getMessageBus()
.connect()
.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this);
}

private void removeFiles(Collection<VirtualFile> files) {
files.forEach(eventMap::remove);
}

@Override
public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
Expand All @@ -43,7 +80,16 @@ public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile f
if (module != null) {
getService(BackendService.class).updateFileSystem(Map.of(module, List.of(new VirtualFileEvent(ModuleFileEvent.Type.CREATED, file))));
}
getService(source.getProject(), AnalysisSubmitter.class).autoAnalyzeFile(file, TriggerType.EDITOR_OPEN);
if (source.getProject().equals(myProject)) {
eventMap.put(file, System.currentTimeMillis());
}
});
}

@Override
public void dispose() {
eventMap.clear();
watcher.stopWatcher();
}

}
100 changes: 100 additions & 0 deletions src/main/java/org/sonarlint/intellij/trigger/EventWatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* SonarLint for IntelliJ IDEA
* Copyright (C) 2015-2024 SonarSource
* [email protected]
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonarlint.intellij.trigger

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import java.util.concurrent.ConcurrentHashMap
import org.sonarlint.intellij.analysis.AnalysisSubmitter
import org.sonarlint.intellij.analysis.Cancelable
import org.sonarlint.intellij.common.util.SonarLintUtils.getService
import org.sonarlint.intellij.config.Settings
import org.sonarlint.intellij.util.SonarLintAppUtils.retainOpenFiles
import org.sonarlint.intellij.util.runOnPooledThread

class EventWatcher(
private val project: Project,
watcherName: String,
private val eventMap: ConcurrentHashMap<VirtualFile, Long>,
private val triggerType: TriggerType,
private val timer: Int
) : Thread() {

private var stop: Boolean = false
private var task: Cancelable? = null

init {
isDaemon = true
name = "sonarlint-auto-trigger-$watcherName-${project.name}"
}

fun stopWatcher() {
stop = true
interrupt()
}

override fun run() {
while (!stop) {
checkTimers()
try {
sleep(200)
} catch (e: InterruptedException) {
// continue until stop flag is set
}
}
}

private fun triggerFiles(files: List<VirtualFile>) {
if (Settings.getGlobalSettings().isAutoTrigger) {
val openFilesToAnalyze = retainOpenFiles(project, files)
if (openFilesToAnalyze.isNotEmpty()) {
task?.let {
it.cancel()
task = null
return
}
files.forEach { eventMap.remove(it) }
task = getService(project, AnalysisSubmitter::class.java).autoAnalyzeFiles(openFilesToAnalyze, triggerType)
}
}
}

private fun checkTimers() {
val now = System.currentTimeMillis()

val it = eventMap.entries.iterator()
val filesToTrigger = ArrayList<VirtualFile>()
while (it.hasNext()) {
val event = it.next()
if (!event.key.isValid) {
it.remove()
continue
}
// don't trigger if file currently has errors?
// filter files opened in the editor
// use some heuristics based on analysis time or average pauses? Or make it configurable?
if (event.value + timer < now) {
filesToTrigger.add(event.key)
}
}
runOnPooledThread(project) { triggerFiles(filesToTrigger) }
}

}
1 change: 0 additions & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@

<projectListeners>
<listener class="org.sonarlint.intellij.module.ModuleChangeListener" topic="com.intellij.openapi.project.ModuleListener"/>
<listener class="org.sonarlint.intellij.trigger.EditorOpenTrigger" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" activeInTestMode="false"/>
</projectListeners>

<extensions defaultExtensionNs="com.intellij">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.vfs.VirtualFile;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.sonarlint.intellij.AbstractSonarLintLightTests;
Expand All @@ -36,29 +38,35 @@
import static org.mockito.Mockito.when;

class EditorOpenTriggerTests extends AbstractSonarLintLightTests {
private AnalysisSubmitter analysisSubmitter = mock(AnalysisSubmitter.class);

private final AnalysisSubmitter analysisSubmitter = mock(AnalysisSubmitter.class);
private EditorOpenTrigger editorTrigger;
private VirtualFile file;
private FileEditorManager editorManager;

@BeforeEach
void start() {
editorTrigger = new EditorOpenTrigger();
editorTrigger = new EditorOpenTrigger(getProject());
getGlobalSettings().setAutoTrigger(true);
replaceProjectService(AnalysisSubmitter.class, analysisSubmitter);

file = createAndOpenTestVirtualFile("MyClass.java", Language.findLanguageByID("JAVA"), "class MyClass{}");
editorManager = mock(FileEditorManager.class);
when(editorManager.getProject()).thenReturn(getProject());
reset(analysisSubmitter);
editorTrigger.onProjectOpened();
}

@AfterEach
void cleanup() {
editorTrigger.dispose();
}

@Test
void should_trigger() {
editorTrigger.fileOpened(editorManager, file);

verify(analysisSubmitter, timeout(1000)).autoAnalyzeFile(file, TriggerType.EDITOR_OPEN);
verify(analysisSubmitter, timeout(3000)).autoAnalyzeFiles(List.of(file), TriggerType.EDITOR_OPEN);
}

@Test
Expand Down

0 comments on commit a5fbea7

Please sign in to comment.