diff --git a/build.gradle.kts b/build.gradle.kts index bd650ad56..0f600a07b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -214,6 +214,7 @@ dependencies { compileOnly(libs.findbugs.jsr305) runtimeOnly(project(":clion")) runtimeOnly(project(":clion-resharper")) + runtimeOnly(project(":nodejs")) runtimeOnly(project(":rider")) runtimeOnly(project(":git")) testImplementation(platform(libs.junit.bom)) diff --git a/common/src/main/java/org/sonarlint/intellij/common/util/NodeJsProvider.java b/common/src/main/java/org/sonarlint/intellij/common/util/NodeJsProvider.java new file mode 100644 index 000000000..44dc02866 --- /dev/null +++ b/common/src/main/java/org/sonarlint/intellij/common/util/NodeJsProvider.java @@ -0,0 +1,35 @@ +/* + * SonarLint for IntelliJ IDEA + * Copyright (C) 2015-2025 SonarSource + * sonarlint@sonarsource.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 02 + */ +package org.sonarlint.intellij.common.util; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import java.nio.file.Path; +import javax.annotation.Nullable; + +public interface NodeJsProvider { + + // Name is constructed from plugin-id.extension-point-name + ExtensionPointName EP_NAME = ExtensionPointName.create("org.sonarlint.idea.nodeJsProvider"); + + @Nullable + Path getNodeJsPathFor(Project project); + +} diff --git a/gradle.properties b/gradle.properties index 08f106273..14ea82315 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,3 +13,4 @@ riderBuildVersion=RD-2022.3.1 clionBuildVersion=CL-2022.3.1 clionResharperBuildVersion=RD-2024.1 intellijBuildVersion=IC-2022.3.1 +intellijUltimateBuildVersion=IU-2022.3.1 diff --git a/nodejs/build.gradle.kts b/nodejs/build.gradle.kts new file mode 100644 index 000000000..7234e8cf7 --- /dev/null +++ b/nodejs/build.gradle.kts @@ -0,0 +1,20 @@ +val intellijUltimateBuildVersion: String by project +val ideaHome: String? = System.getenv("IDEA_HOME") + +plugins { + kotlin("jvm") +} + +intellij { + if (!ideaHome.isNullOrBlank()) { + localPath.set(ideaHome) + localSourcesPath.set(ideaHome) + } else { + version.set(intellijUltimateBuildVersion) + } + plugins.set(listOf("JavaScript")) +} + +dependencies { + implementation(project(":common")) +} diff --git a/nodejs/src/main/java/org/sonarlint/intellij/nodejs/JavaScriptNodeJsProvider.java b/nodejs/src/main/java/org/sonarlint/intellij/nodejs/JavaScriptNodeJsProvider.java new file mode 100644 index 000000000..c783de8b9 --- /dev/null +++ b/nodejs/src/main/java/org/sonarlint/intellij/nodejs/JavaScriptNodeJsProvider.java @@ -0,0 +1,38 @@ +/* + * SonarLint for IntelliJ IDEA + * Copyright (C) 2015-2025 SonarSource + * sonarlint@sonarsource.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 02 + */ +package org.sonarlint.intellij.nodejs; + +import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager; +import com.intellij.openapi.project.Project; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.jetbrains.annotations.Nullable; +import org.sonarlint.intellij.common.util.NodeJsProvider; + +public class JavaScriptNodeJsProvider implements NodeJsProvider { + + @Nullable + @Override + public Path getNodeJsPathFor(Project project) { + var interpreter = NodeJsInterpreterManager.getInstance(project).getInterpreter(); + return interpreter != null ? Paths.get(interpreter.getReferenceName()) : null; + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 79d539320..2831950bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ rootProject.name = "sonarlint-intellij" -include("its", "clion", "clion-resharper", "clion-common", "common", "git", "rider") +include("its", "clion", "clion-resharper", "nodejs", "clion-common", "common", "git", "rider") dependencyResolutionManagement { versionCatalogs { diff --git a/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt b/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt index 7df38d9b2..32c66011f 100644 --- a/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt +++ b/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt @@ -57,6 +57,7 @@ import java.nio.file.Paths import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate +import java.time.Duration import java.util.UUID import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture @@ -111,6 +112,7 @@ import org.sonarlint.intellij.sharing.ConfigurationSharing import org.sonarlint.intellij.sharing.SonarLintSharedFolderUtils.Companion.findSharedFolder import org.sonarlint.intellij.trigger.TriggerType import org.sonarlint.intellij.ui.UiUtils.Companion.runOnUiThread +import org.sonarlint.intellij.util.FutureUtils import org.sonarlint.intellij.util.GlobalLogOutput import org.sonarlint.intellij.util.ProjectUtils.tryFindFile import org.sonarlint.intellij.util.SonarLintAppUtils.findModuleForFile @@ -133,7 +135,6 @@ import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSugge import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto -import org.sonarsource.sonarlint.core.rpc.protocol.client.event.DidReceiveServerHotspotEvent import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FixSuggestionDto import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.HotspotDetailsDto import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto @@ -632,7 +633,9 @@ object SonarLintIntelliJClient : SonarLintRpcClientDelegate { resultFuture.complete(result) } }) - return computeOnPooledThread(project, "Waiting for branch matching result") { resultFuture.get() } + return computeOnPooledThread(project, "Waiting for branch matching result") { + FutureUtils.waitForTask(resultFuture, "", Duration.ofSeconds(10)) + } } override fun matchProjectBranch( diff --git a/src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalOptionsPanel.java b/src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalOptionsPanel.java index 064df57c7..efd1819c2 100644 --- a/src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalOptionsPanel.java +++ b/src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalOptionsPanel.java @@ -21,6 +21,7 @@ import com.intellij.ide.BrowserUtil; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.ui.HyperlinkAdapter; import com.intellij.ui.components.JBCheckBox; @@ -34,6 +35,7 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Objects; import javax.swing.BorderFactory; import javax.swing.JComponent; @@ -42,6 +44,7 @@ import javax.swing.JPanel; import javax.swing.event.HyperlinkEvent; import org.sonarlint.intellij.cayc.CleanAsYouCodeService; +import org.sonarlint.intellij.common.util.NodeJsProvider; import org.sonarlint.intellij.config.ConfigurationPanel; import org.sonarlint.intellij.core.BackendService; import org.sonarlint.intellij.util.HelpLabelUtils; @@ -144,15 +147,30 @@ public void load(SonarLintGlobalSettings model) { private void loadNodeJsSettings(SonarLintGlobalSettings model) { if (model.getNodejsPath() == null || model.getNodejsPath().isBlank()) { - getService(BackendService.class).getAutoDetectedNodeJs().thenAccept(settings -> { - if (settings == null) { - this.nodeJsPath.getEmptyText().setText("Node.js not found"); - this.nodeJsVersion.setText("N/A"); - } else { - this.nodeJsPath.getEmptyText().setText(settings.getPath().toString()); - this.nodeJsVersion.setText(settings.getVersion()); - } - }); + var optProject = Arrays.stream(ProjectManager.getInstance().getOpenProjects()).findFirst(); + if (optProject.isPresent()) { + var optNodeJs = NodeJsProvider.EP_NAME.getExtensionList().stream().map(e -> + e.getNodeJsPathFor(optProject.get()) + ).findFirst(); + optNodeJs.ifPresent(path -> getService(BackendService.class).changeClientNodeJsPath(path).thenAccept(settings -> { + if (settings == null) { + this.nodeJsVersion.setText("N/A"); + } else { + this.nodeJsPath.setText(settings.getPath().toString()); + this.nodeJsVersion.setText(settings.getVersion()); + } + })); + } else { + getService(BackendService.class).getAutoDetectedNodeJs().thenAccept(settings -> { + if (settings == null) { + this.nodeJsPath.getEmptyText().setText("Node.js not found"); + this.nodeJsVersion.setText("N/A"); + } else { + this.nodeJsPath.getEmptyText().setText(settings.getPath().toString()); + this.nodeJsVersion.setText(settings.getVersion()); + } + }); + } } else { var forcedNodeJsPath = Paths.get(model.getNodejsPath()); getService(BackendService.class).changeClientNodeJsPath(forcedNodeJsPath).thenAccept(settings -> { diff --git a/src/main/resources/META-INF/plugin-nodejs.xml b/src/main/resources/META-INF/plugin-nodejs.xml new file mode 100644 index 000000000..558b1920c --- /dev/null +++ b/src/main/resources/META-INF/plugin-nodejs.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 457539636..31a5a9c5f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -199,6 +199,7 @@ org.jetbrains.plugins.clion.radler com.intellij.modules.rider intellij.jupyter + JavaScript @@ -287,6 +288,7 @@ +