From 3e152d485a9fafbca97cc58f38e988c8d4bb43ba Mon Sep 17 00:00:00 2001 From: Sophio Japharidze Date: Fri, 24 Oct 2025 10:43:43 +0200 Subject: [PATCH] SLLS-345 fall back to window/showMessage for unsupported clients --- ...NotificationDisplayOptionDeserializer.java | 52 +++++++++++++++ .../sonarlint/ls/SkippedPluginsNotifier.java | 8 ++- .../ls/SonarLintExtendedLanguageClient.java | 8 ++- .../sonarlint/ls/SonarLintLanguageServer.java | 2 + ...ficationDisplayOptionDeserializerTest.java | 65 +++++++++++++++++++ .../ls/SkippedPluginsNotifierTests.java | 26 +++++++- .../AbstractLanguageServerMediumTests.java | 4 +- 7 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializer.java create mode 100644 src/test/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializerTest.java diff --git a/src/main/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializer.java b/src/main/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializer.java new file mode 100644 index 000000000..6a718c32f --- /dev/null +++ b/src/main/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializer.java @@ -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 { + @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."); + }; + } +} diff --git a/src/main/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifier.java b/src/main/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifier.java index 57bf67c11..a581edd32 100644 --- a/src/main/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifier.java +++ b/src/main/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifier.java @@ -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; @@ -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); diff --git a/src/main/java/org/sonarsource/sonarlint/ls/SonarLintExtendedLanguageClient.java b/src/main/java/org/sonarsource/sonarlint/ls/SonarLintExtendedLanguageClient.java index ccb430de7..30f07380f 100644 --- a/src/main/java/org/sonarsource/sonarlint/ls/SonarLintExtendedLanguageClient.java +++ b/src/main/java/org/sonarsource/sonarlint/ls/SonarLintExtendedLanguageClient.java @@ -73,8 +73,14 @@ public interface SonarLintExtendedLanguageClient extends LanguageClient { @JsonNotification("doNotShowMissingRequirementsMessageAgain") void doNotShowMissingRequirementsMessageAgain(); + enum MissingRequirementsNotificationDisplayOption { + FULL, + ERROR_ONLY, + DO_NOT_SHOW_AGAIN + } + @JsonRequest("canShowMissingRequirementsNotification") - CompletableFuture canShowMissingRequirementsNotification(); + CompletableFuture canShowMissingRequirementsNotification(); @JsonNotification("openConnectionSettings") void openConnectionSettings(boolean isSonarCloud); diff --git a/src/main/java/org/sonarsource/sonarlint/ls/SonarLintLanguageServer.java b/src/main/java/org/sonarsource/sonarlint/ls/SonarLintLanguageServer.java index 7a1bc5e94..37b2362b7 100644 --- a/src/main/java/org/sonarsource/sonarlint/ls/SonarLintLanguageServer.java +++ b/src/main/java/org/sonarsource/sonarlint/ls/SonarLintLanguageServer.java @@ -205,6 +205,8 @@ public class SonarLintLanguageServer implements SonarLintExtendedLanguageServer, var input = new ExitingInputStream(inputStream, this); var launcher = new Launcher.Builder() + .configureGson(configureGson -> configureGson.registerTypeAdapter(SonarLintExtendedLanguageClient.MissingRequirementsNotificationDisplayOption.class, + new MissingRequirementsNotificationDisplayOptionDeserializer())) .setLocalService(this) .setRemoteInterface(SonarLintExtendedLanguageClient.class) .setInput(input) diff --git a/src/test/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializerTest.java b/src/test/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializerTest.java new file mode 100644 index 000000000..73cab946c --- /dev/null +++ b/src/test/java/org/sonarsource/sonarlint/ls/MissingRequirementsNotificationDisplayOptionDeserializerTest.java @@ -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)); + } +} diff --git a/src/test/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifierTests.java b/src/test/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifierTests.java index 3551a4650..f7c93a9f2 100644 --- a/src/test/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifierTests.java +++ b/src/test/java/org/sonarsource/sonarlint/ls/SkippedPluginsNotifierTests.java @@ -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; @@ -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) { @@ -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"); @@ -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); + } } diff --git a/src/test/java/org/sonarsource/sonarlint/ls/mediumtests/AbstractLanguageServerMediumTests.java b/src/test/java/org/sonarsource/sonarlint/ls/mediumtests/AbstractLanguageServerMediumTests.java index a7a30e363..63d5c03cc 100644 --- a/src/test/java/org/sonarsource/sonarlint/ls/mediumtests/AbstractLanguageServerMediumTests.java +++ b/src/test/java/org/sonarsource/sonarlint/ls/mediumtests/AbstractLanguageServerMediumTests.java @@ -605,8 +605,8 @@ public void endProgressNotification(EndProgressNotificationParams params) { } @Override - public CompletableFuture canShowMissingRequirementsNotification() { - return CompletableFuture.completedFuture(false); + public CompletableFuture canShowMissingRequirementsNotification() { + return CompletableFuture.completedFuture(MissingRequirementsNotificationDisplayOption.DO_NOT_SHOW_AGAIN); } @Override