Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* SonarLint Language Server
* Copyright (C) 2009-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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 02110-1301, USA.
*/
package org.sonarsource.sonarlint.ls;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.Locale;

public class MissingRequirementsNotificationDisplayOptionDeserializer implements
JsonDeserializer<SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption> {
@Override
public SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
if (jsonElement.isJsonNull()) {
return SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.ERROR_ONLY;
}

var primitive = jsonElement.getAsJsonPrimitive();

if (primitive.isBoolean()) {
return primitive.getAsBoolean() ? SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.FULL
: SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN;
}

return switch (primitive.getAsString().toLowerCase(Locale.US)) {
case "full" -> SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.FULL;
case "error_only" -> SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.ERROR_ONLY;
case "never_again" -> SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN;
default -> throw new JsonParseException("Cannot deserialize missing requirements notifications.");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Set;
import javax.annotation.Nullable;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason;
Expand Down Expand Up @@ -76,17 +77,18 @@ private void handleMissingNodeRuntimeRequirement(String minVersion, @Nullable St
}
var isNotificationAllowed = client.canShowMissingRequirementsNotification().join();
globalLogOutput.warn(content);
if (Boolean.TRUE.equals(isNotificationAllowed)) {
if (SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.FULL.equals(isNotificationAllowed)) {
showMessageWithOpenSettingsAction(client, formatMessage(title, content), NODEJS,
client::openPathToNodeSettings);
} else if (SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.ERROR_ONLY.equals(isNotificationAllowed)) {
client.showMessage(new MessageParams(MessageType.Error, formatMessage(title, content)));
}
}

private void showMessageWithOpenSettingsAction(SonarLintExtendedLanguageClient client, String message,
SkipReason.UnsatisfiedRuntimeRequirement.RuntimeRequirement requirementType, Runnable callback) {
if (displayedMessages.add(message)) {
var params = requirementType == NODEJS ?
new ShowMessageRequestParams(List.of(ACTION_OPEN_SETTINGS, ACTION_DONT_SHOW_AGAIN))
var params = requirementType == NODEJS ? new ShowMessageRequestParams(List.of(ACTION_OPEN_SETTINGS, ACTION_DONT_SHOW_AGAIN))
: new ShowMessageRequestParams(List.of(ACTION_OPEN_SETTINGS));
params.setType(MessageType.Error);
params.setMessage(message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ public interface SonarLintExtendedLanguageClient extends LanguageClient {
@JsonNotification("doNotShowMissingRequirementsMessageAgain")
void doNotShowMissingRequirementsMessageAgain();

enum MissingRequirementsNotificationDisplayOption {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#548 introduced ShowMissingRequirementsNotificationDeserializer to keep the protocol backward compatible (sonarlint/canShowMissingRequirementsNotification can then return boolean and the enum values).

This could be extremely helpful, because currently, I know that users of sonarlint.nvim are on different versions. For example, people on Mason did not receive a recent update because mason-org/mason-registry#11979 is blocked for some reason.

If you don't want to include the custom deserializer, then I suppose this is another major version bump, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the PR with the custom deserialiser. I've added tests for it, and tested manually that it works. Please also double-check on your side.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking the time. I wanted to test it locally by building the version on this branch, but the compilation failed because some dependencies are not publicly available:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.163 s
[INFO] Finished at: 2025-11-06T08:23:02+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project sonarlint-language-server: Could not resolve dependencies for project org.sonarsource.sonarlint.ls:sonarlint-language-server:jar:4.5-SNAPSHOT
[ERROR] dependency: org.sonarsource.sonarlint.core:sonarlint-java-client-utils:jar:10.36.0.83608 (compile)
[ERROR]         Could not find artifact org.sonarsource.sonarlint.core:sonarlint-java-client-utils:jar:10.36.0.83608 in central (https://repo.maven.apache.org/maven2)
[ERROR] dependency: org.sonarsource.sonarlint.core:sonarlint-rpc-protocol:jar:10.36.0.83608 (compile)
[ERROR]         Could not find artifact org.sonarsource.sonarlint.core:sonarlint-rpc-protocol:jar:10.36.0.83608 in central (https://repo.maven.apache.org/maven2)
[ERROR] dependency: org.sonarsource.sonarlint.core:sonarlint-rpc-impl:jar:10.36.0.83608 (compile)
[ERROR]         Could not find artifact org.sonarsource.sonarlint.core:sonarlint-rpc-impl:jar:10.36.0.83608 in central (https://repo.maven.apache.org/maven2)
[ERROR] dependency: org.sonarsource.sonarlint.core:sonarlint-rpc-java-client:jar:10.36.0.83608 (compile)
[ERROR]         Could not find artifact org.sonarsource.sonarlint.core:sonarlint-rpc-java-client:jar:10.36.0.83608 in central (https://repo.maven.apache.org/maven2)

Is there a public Maven repo that I can add to my Maven settings and that hosts these versions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a workaround: I cherry-picked your commit onto the tagged release and then I could build it locally. Then, I could see the error in Neovim based, using this MR

grafik

FULL,
ERROR_ONLY,
DO_NOT_SHOW_AGAIN
}

@JsonRequest("canShowMissingRequirementsNotification")
CompletableFuture<Boolean> canShowMissingRequirementsNotification();
CompletableFuture<MissingRequirementsNotificationDisplayOption> canShowMissingRequirementsNotification();

@JsonNotification("openConnectionSettings")
void openConnectionSettings(boolean isSonarCloud);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ public class SonarLintLanguageServer implements SonarLintExtendedLanguageServer,

var input = new ExitingInputStream(inputStream, this);
var launcher = new Launcher.Builder<SonarLintExtendedLanguageClient>()
.configureGson(configureGson -> configureGson.registerTypeAdapter(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.class,
new MissingRequirementsNotificationDisplayOptionDeserializer()))
.setLocalService(this)
.setRemoteInterface(SonarLintExtendedLanguageClient.class)
.setInput(input)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* SonarLint Language Server
* Copyright (C) 2009-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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 02110-1301, USA.
*/
package org.sonarsource.sonarlint.ls;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonNull;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;

class MissingRequirementsNotificationDisplayOptionDeserializerTest {
private final JsonDeserializationContext mockContext = mock(JsonDeserializationContext.class);
private final MissingRequirementsNotificationDisplayOptionDeserializer underTest = new MissingRequirementsNotificationDisplayOptionDeserializer();

@Test
void should_default_to_error_only() {
var result = underTest.deserialize(JsonNull.INSTANCE, null, mockContext);

assertEquals(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.ERROR_ONLY, result);
}

@Test
void should_support_old_clients_with_boolean_values() {
var trueResult = underTest.deserialize(new JsonPrimitive(true), null, mockContext);
var falseResult = underTest.deserialize(new JsonPrimitive(false), null, mockContext);

assertEquals(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.FULL, trueResult);
assertEquals(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN, falseResult);
}

@Test
void should_support_new_clients_with_message_display_options() {
var fullResult = underTest.deserialize(new JsonPrimitive("full"), null, mockContext);
var errorOnlyResult = underTest.deserialize(new JsonPrimitive("error_only"), null, mockContext);
var neverAgainResult = underTest.deserialize(new JsonPrimitive("never_again"), null, mockContext);

assertEquals(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.FULL, fullResult);
assertEquals(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.ERROR_ONLY, errorOnlyResult);
assertEquals(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN, neverAgainResult);

var randomPrimitive = new JsonPrimitive("random");
assertThrows(JsonParseException.class, () -> underTest.deserialize(randomPrimitive, null, mockContext));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -46,7 +48,8 @@ class SkippedPluginsNotifierTests {
void initClient() {
languageClient = mock(SonarLintExtendedLanguageClient.class);
underTest = new SkippedPluginsNotifier(languageClient, mock(LanguageClientLogger.class));
when(languageClient.canShowMissingRequirementsNotification()).thenReturn(CompletableFuture.completedFuture(true));
when(languageClient.canShowMissingRequirementsNotification())
.thenReturn(CompletableFuture.completedFuture(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.FULL));
}

private void preparePopupSelection(@Nullable MessageActionItem selectedItem) {
Expand All @@ -55,7 +58,8 @@ private void preparePopupSelection(@Nullable MessageActionItem selectedItem) {

@Test
void no_job_if_notifs_disabled() {
when(languageClient.canShowMissingRequirementsNotification()).thenReturn(CompletableFuture.completedFuture(false));
when(languageClient.canShowMissingRequirementsNotification())
.thenReturn(CompletableFuture.completedFuture(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN));

underTest.notifyOnceForSkippedPlugins(Language.YAML, DidSkipLoadingPluginParams.SkipReason.UNSATISFIED_NODE_JS, "18", "14");

Expand Down Expand Up @@ -142,4 +146,22 @@ void should_turn_off_notifications_when_user_opts_out() {
.contains("Node.js runtime version minNodeJsVersion or later is required. Current version is currentNodeJsVersion.");
assertThat(message.getActions()).containsExactly(SkippedPluginsNotifier.ACTION_OPEN_SETTINGS, SkippedPluginsNotifier.ACTION_DONT_SHOW_AGAIN);
}

@Test
void should_send_only_error_notification_when_not_supported_by_client() {
when(languageClient.canShowMissingRequirementsNotification())
.thenReturn(CompletableFuture.completedFuture(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.ERROR_ONLY));

underTest.notifyOnceForSkippedPlugins(Language.JS, DidSkipLoadingPluginParams.SkipReason.UNSATISFIED_NODE_JS, "minNodeJsVersion", "currentNodeJsVersion");

var messageCaptor = ArgumentCaptor.forClass(MessageParams.class);
verify(languageClient, times(1)).showMessage(messageCaptor.capture());
verify(languageClient, never()).openPathToNodeSettings();
verify(languageClient, never()).doNotShowMissingRequirementsMessageAgain();

var message = messageCaptor.getValue();
assertThat(message.getMessage()).contains("SonarQube for VS Code failed to analyze JavaScript code")
.contains("Node.js runtime version minNodeJsVersion or later is required. Current version is currentNodeJsVersion.");
assertThat(message.getType()).isEqualTo(MessageType.Error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,8 @@ public void endProgressNotification(EndProgressNotificationParams params) {
}

@Override
public CompletableFuture<Boolean> canShowMissingRequirementsNotification() {
return CompletableFuture.completedFuture(false);
public CompletableFuture<MissingRequirementsNotificationDisplayOption> canShowMissingRequirementsNotification() {
return CompletableFuture.completedFuture(MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN);
}

@Override
Expand Down
Loading