diff --git a/its/plugin/plugins/consumer-plugin/pom.xml b/its/plugin/plugins/consumer-plugin/pom.xml new file mode 100644 index 00000000000..a1decf43f3c --- /dev/null +++ b/its/plugin/plugins/consumer-plugin/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + org.sonarsource.javascript + javascript-it-plugin-plugins + 10.15.0-SNAPSHOT + + + consumer-plugin + sonar-plugin + + JavaScript :: IT :: Plugin :: Plugins :: Consumer Plugin + Consumer Plugin + + + + org.sonarsource.api.plugin + sonar-plugin-api + + + org.sonarsource.javascript + api + + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.sonarsource.sonar-packaging-maven-plugin + sonar-packaging-maven-plugin + true + + ${pluginApiMinVersion} + org.sonar.samples.javascript.consumer.ConsumerPlugin + js + + + + + + diff --git a/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/Consumer.java b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/Consumer.java new file mode 100644 index 00000000000..e1e75c6d2b5 --- /dev/null +++ b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/Consumer.java @@ -0,0 +1,55 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2012-2024 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.sonar.samples.javascript.consumer; + +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.plugins.javascript.api.JsAnalysisConsumer; +import org.sonar.plugins.javascript.api.JsFile; + +public class Consumer implements JsAnalysisConsumer { + + private static final Logger LOG = LoggerFactory.getLogger(Consumer.class); + + private final List jsFiles = new ArrayList<>(); + private boolean done; + + @Override + public void accept(JsFile jsFile) { + LOG.info("Accepted file: {}", jsFile.inputFile()); + jsFiles.add(jsFile); + } + + @Override + public void doneAnalysis() { + LOG.info("Done analysis"); + done = true; + } + + public List getJsFiles() { + return jsFiles; + } + + public boolean isDone() { + return done; + } +} diff --git a/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/ConsumerPlugin.java b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/ConsumerPlugin.java new file mode 100644 index 00000000000..02068fdbeb7 --- /dev/null +++ b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/ConsumerPlugin.java @@ -0,0 +1,33 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2012-2024 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.sonar.samples.javascript.consumer; + +import org.sonar.api.Plugin; + +public class ConsumerPlugin implements Plugin { + + @Override + public void define(Context context) { + context.addExtensions( + Consumer.class, + ConsumerSensor.class + ); + } +} diff --git a/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/ConsumerSensor.java b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/ConsumerSensor.java new file mode 100644 index 00000000000..0b95706821e --- /dev/null +++ b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/ConsumerSensor.java @@ -0,0 +1,63 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2012-2024 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.sonar.samples.javascript.consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.DependsUpon; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.scanner.sensor.ProjectSensor; +import org.sonar.plugins.javascript.api.JsFile; + +@ScannerSide +// We depend on the "js-analysis" extension to make sure that the analysis is done before we consume it +@DependsUpon("js-analysis") +public class ConsumerSensor implements ProjectSensor { + + private static final Logger LOG = LoggerFactory.getLogger(ConsumerSensor.class); + + private final Consumer consumer; + + /** + * We use Dependency Injection to get Consumer instance + * + * @param consumer Consumer instance + */ + public ConsumerSensor(Consumer consumer) { + this.consumer = consumer; + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Consumer Sensor"); + } + + @Override + public void execute(SensorContext context) { + if (!consumer.isDone()) { + throw new IllegalStateException("Consumer is not done"); + } + for (JsFile jsFile : consumer.getJsFiles()) { + LOG.info("Processing file {}", jsFile.inputFile()); + } + } +} diff --git a/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/package-info.java b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/package-info.java new file mode 100644 index 00000000000..e36dc61df1d --- /dev/null +++ b/its/plugin/plugins/consumer-plugin/src/main/java/org/sonar/samples/javascript/consumer/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.samples.javascript.consumer; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/its/plugin/plugins/consumer-plugin/src/test/java/org/sonar/samples/javascript/consumer/ConsumerTest.java b/its/plugin/plugins/consumer-plugin/src/test/java/org/sonar/samples/javascript/consumer/ConsumerTest.java new file mode 100644 index 00000000000..185ab56fff0 --- /dev/null +++ b/its/plugin/plugins/consumer-plugin/src/test/java/org/sonar/samples/javascript/consumer/ConsumerTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2012-2024 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.sonar.samples.javascript.consumer; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ConsumerTest { + + @Test + void test() { + var consumer = new Consumer(); + consumer.doneAnalysis(); + assertTrue(consumer.isDone()); + } + +} diff --git a/its/plugin/plugins/eslint-custom-rules-plugin/src/main/java/org/sonar/samples/javascript/package-info.java b/its/plugin/plugins/eslint-custom-rules-plugin/src/main/java/org/sonar/samples/javascript/package-info.java new file mode 100644 index 00000000000..0522c738ac2 --- /dev/null +++ b/its/plugin/plugins/eslint-custom-rules-plugin/src/main/java/org/sonar/samples/javascript/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.samples.javascript; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/its/plugin/plugins/pom.xml b/its/plugin/plugins/pom.xml index 8591fc8715a..4731cff3f29 100644 --- a/its/plugin/plugins/pom.xml +++ b/its/plugin/plugins/pom.xml @@ -14,6 +14,7 @@ eslint-custom-rules-plugin + consumer-plugin diff --git a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/ConsumerPluginTest.java b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/ConsumerPluginTest.java new file mode 100644 index 00000000000..8a967fdf1a1 --- /dev/null +++ b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/ConsumerPluginTest.java @@ -0,0 +1,102 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2012-2024 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 com.sonar.javascript.it.plugin; + +import static com.sonar.javascript.it.plugin.OrchestratorStarter.JAVASCRIPT_PLUGIN_LOCATION; +import static org.assertj.core.api.Assertions.assertThat; + +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.BuildResult; +import com.sonar.orchestrator.build.SonarScanner; +import com.sonar.orchestrator.junit5.OrchestratorExtension; +import com.sonar.orchestrator.locator.FileLocation; +import java.io.File; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class ConsumerPluginTest { + + private static final String PLUGIN_ARTIFACT_ID = "consumer-plugin"; + + private static OrchestratorExtension orchestrator; + + @BeforeAll + public static void before() { + orchestrator = initOrchestrator(PLUGIN_ARTIFACT_ID); + } + + @AfterAll + public static void after() { + orchestrator.stop(); + } + + static OrchestratorExtension initOrchestrator(String customRulesArtifactId) { + var orchestrator = OrchestratorExtension + .builderEnv() + .useDefaultAdminCredentialsForBuilds(true) + .setSonarVersion(System.getProperty("sonar.runtimeVersion", "LATEST_RELEASE")) + .addPlugin(JAVASCRIPT_PLUGIN_LOCATION) + .restoreProfileAtStartup(FileLocation.ofClasspath("/empty-js-profile.xml")) + .addPlugin( + FileLocation.byWildcardMavenFilename( + new File("../plugins/" + customRulesArtifactId + "/target"), + customRulesArtifactId + "-*.jar" + ) + ) + .restoreProfileAtStartup(FileLocation.ofClasspath("/profile-javascript-custom-rules.xml")) + .restoreProfileAtStartup(FileLocation.ofClasspath("/profile-typescript-custom-rules.xml")) + .restoreProfileAtStartup(FileLocation.ofClasspath("/nosonar.xml")) + .build(); + // Installation of SQ server in orchestrator is not thread-safe, so we need to synchronize + synchronized (OrchestratorStarter.class) { + orchestrator.start(); + } + return orchestrator; + } + + static BuildResult runBuild(Orchestrator orchestrator) { + SonarScanner build = OrchestratorStarter + .createScanner() + .setProjectDir(TestUtils.projectDirNoCopy("custom_rules")) + .setProjectKey("custom-rules") + .setProjectName("Custom Rules") + .setProjectVersion("1.0") + .setDebugLogs(true) + .setSourceDirs("src"); + orchestrator.getServer().provisionProject("custom-rules", "Custom Rules"); + orchestrator + .getServer() + .associateProjectToQualityProfile("custom-rules", "js", "javascript-custom-rules-profile"); + orchestrator + .getServer() + .associateProjectToQualityProfile("custom-rules", "ts", "ts-custom-rules-profile"); + return orchestrator.executeBuild(build); + } + + @Test + void test() { + var buildResult = runBuild(orchestrator); + var logMatch = ".*DEBUG: Registered JsAnalysisConsumers \\[org.sonar.samples.javascript.consumer.Consumer.*]"; + assertThat(buildResult.getLogsLines(l -> l.matches(logMatch))).hasSize(1); + + assertThat(buildResult.getLogsLines(l -> l.matches(".*Processing file src/dir.*"))).hasSize(2); + } +} diff --git a/sonar-plugin/api/src/main/java/org/sonar/plugins/javascript/api/JsAnalysisConsumer.java b/sonar-plugin/api/src/main/java/org/sonar/plugins/javascript/api/JsAnalysisConsumer.java new file mode 100644 index 00000000000..e811533a29b --- /dev/null +++ b/sonar-plugin/api/src/main/java/org/sonar/plugins/javascript/api/JsAnalysisConsumer.java @@ -0,0 +1,44 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 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.sonar.plugins.javascript.api; + +import org.sonar.api.scanner.ScannerSide; +import org.sonarsource.api.sonarlint.SonarLintSide; + +/** + * Implementations of this interface will be invoked during the analysis of JavaScript/TypeScript files. + * + */ +@ScannerSide +@SonarLintSide +public interface JsAnalysisConsumer { + + /** + * Called for each file during the analysis. + * @param jsFile the file which was analyzed + */ + void accept(JsFile jsFile); + + /** + * + * Called at the end of the analysis. + */ + void doneAnalysis(); +} diff --git a/sonar-plugin/api/src/main/java/org/sonar/plugins/javascript/api/JsFile.java b/sonar-plugin/api/src/main/java/org/sonar/plugins/javascript/api/JsFile.java new file mode 100644 index 00000000000..2e753468de6 --- /dev/null +++ b/sonar-plugin/api/src/main/java/org/sonar/plugins/javascript/api/JsFile.java @@ -0,0 +1,25 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 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.sonar.plugins.javascript.api; + +import org.sonar.api.batch.fs.InputFile; + +public record JsFile(InputFile inputFile /*, ESTreeNode node */) { +} diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java index bf0b02a68e8..16964e0e8f3 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java @@ -31,6 +31,7 @@ import org.sonar.css.CssRulesDefinition; import org.sonar.css.StylelintReportSensor; import org.sonar.css.metrics.CssMetricSensor; +import org.sonar.plugins.javascript.analysis.AnalysisConsumers; import org.sonar.plugins.javascript.analysis.AnalysisProcessor; import org.sonar.plugins.javascript.analysis.AnalysisWithProgram; import org.sonar.plugins.javascript.analysis.AnalysisWithWatchProgram; @@ -138,6 +139,7 @@ public class JavaScriptPlugin implements Plugin { @Override public void define(Context context) { context.addExtensions( + AnalysisConsumers.class, JavaScriptLanguage.class, JavaScriptExclusionsFileFilter.class, JavaScriptRulesDefinition.class, diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AbstractAnalysis.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AbstractAnalysis.java index a05ced58af5..2b763e27f61 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AbstractAnalysis.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AbstractAnalysis.java @@ -22,16 +22,22 @@ import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.plugins.javascript.CancellationException; import org.sonar.plugins.javascript.JavaScriptLanguage; import org.sonar.plugins.javascript.TypeScriptLanguage; +import org.sonar.plugins.javascript.analysis.cache.CacheAnalysis; +import org.sonar.plugins.javascript.analysis.cache.CacheStrategies; +import org.sonar.plugins.javascript.api.JsFile; import org.sonar.plugins.javascript.bridge.AnalysisMode; import org.sonar.plugins.javascript.bridge.AnalysisWarningsWrapper; import org.sonar.plugins.javascript.bridge.BridgeServer; import org.sonar.plugins.javascript.JavaScriptFilePredicate; +import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgram; import org.sonar.plugins.javascript.utils.ProgressReport; abstract class AbstractAnalysis { @@ -48,6 +54,7 @@ abstract class AbstractAnalysis { ProgressReport progressReport; AnalysisMode analysisMode; protected final AnalysisWarningsWrapper analysisWarnings; + private AnalysisConsumers consumers; AbstractAnalysis( BridgeServer bridgeServer, @@ -65,12 +72,13 @@ protected static String inputFileLanguage(InputFile file) { : JavaScriptLanguage.KEY; } - void initialize(SensorContext context, JsTsChecks checks, AnalysisMode analysisMode) { + void initialize(SensorContext context, JsTsChecks checks, AnalysisMode analysisMode, AnalysisConsumers consumers) { LOG.debug("Initializing {}", getClass().getName()); this.context = context; contextUtils = new ContextUtils(context); this.checks = checks; this.analysisMode = analysisMode; + this.consumers = consumers; } protected boolean isJavaScript(InputFile file) { @@ -78,4 +86,57 @@ protected boolean isJavaScript(InputFile file) { } abstract void analyzeFiles(List inputFiles, List tsConfigs) throws IOException; + + protected void analyzeFile(InputFile file, @Nullable List tsConfigs, @Nullable TsProgram tsProgram) throws IOException { + if (context.isCancelled()) { + throw new CancellationException( + "Analysis interrupted because the SensorContext is in cancelled state" + ); + } + var cacheStrategy = CacheStrategies.getStrategyFor(context, file); + if (cacheStrategy.isAnalysisRequired()) { + try { + LOG.debug("Analyzing file: {}", file.uri()); + progressReport.nextFile(file.toString()); + var fileContent = contextUtils.shouldSendFileContent(file) ? file.contents() : null; + var request = getJsAnalysisRequest(file, fileContent, tsProgram, tsConfigs); + + var response = isJavaScript(file) + ? bridgeServer.analyzeJavaScript(request) + : bridgeServer.analyzeTypeScript(request); + + analysisProcessor.processResponse(context, checks, file, response); + cacheStrategy.writeAnalysisToCache( + CacheAnalysis.fromResponse(response.ucfgPaths(), response.cpdTokens()), + file + ); + consumers.accept(new JsFile(file)); + } catch (IOException e) { + LOG.error("Failed to get response while analyzing " + file.uri(), e); + throw e; + } + } else { + LOG.debug("Processing cache analysis of file: {}", file.uri()); + var cacheAnalysis = cacheStrategy.readAnalysisFromCache(); + analysisProcessor.processCacheAnalysis(context, file, cacheAnalysis); + } + } + + private BridgeServer.JsAnalysisRequest getJsAnalysisRequest( + InputFile file, + @Nullable String fileContent, + @Nullable TsProgram tsProgram, + @Nullable List tsConfigs + ) { + return new BridgeServer.JsAnalysisRequest( + file.absolutePath(), + file.type().toString(), + inputFileLanguage(file), + fileContent, + contextUtils.ignoreHeaderComments(), + tsConfigs, + tsProgram != null ? tsProgram.programId() : null, + analysisMode.getLinterIdFor(file) + ); + } } diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisConsumers.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisConsumers.java new file mode 100644 index 00000000000..12213eaeacb --- /dev/null +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisConsumers.java @@ -0,0 +1,57 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 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.sonar.plugins.javascript.analysis; + +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.plugins.javascript.api.JsAnalysisConsumer; +import org.sonar.plugins.javascript.api.JsFile; +import org.sonarsource.api.sonarlint.SonarLintSide; + +@ScannerSide +@SonarLintSide +public class AnalysisConsumers implements JsAnalysisConsumer { + + private static final Logger LOG = LoggerFactory.getLogger(AnalysisConsumers.class); + + private final List consumers; + + public AnalysisConsumers() { + consumers = List.of(); + LOG.debug("No registered JsAnalysisConsumer."); + } + + public AnalysisConsumers(List consumers) { + this.consumers = List.copyOf(consumers); + LOG.debug("Registered JsAnalysisConsumers {}", this.consumers); + } + + @Override + public void accept(JsFile jsFile) { + consumers.forEach(c -> c.accept(jsFile)); + } + + @Override + public void doneAnalysis() { + consumers.forEach(JsAnalysisConsumer::doneAnalysis); + } +} diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java index d7d7fa2d8e6..fd2bc86cd79 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java @@ -26,18 +26,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.scanner.ScannerSide; -import org.sonar.plugins.javascript.CancellationException; import org.sonar.plugins.javascript.JavaScriptPlugin; -import org.sonar.plugins.javascript.analysis.cache.CacheAnalysis; -import org.sonar.plugins.javascript.analysis.cache.CacheStrategies; import org.sonar.plugins.javascript.bridge.AnalysisWarningsWrapper; import org.sonar.plugins.javascript.bridge.BridgeServer; -import org.sonar.plugins.javascript.bridge.BridgeServer.JsAnalysisRequest; import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgram; import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgramRequest; import org.sonar.plugins.javascript.utils.ProgressReport; @@ -107,7 +102,7 @@ void analyzeFiles(List inputFiles, List tsConfigs) throws IOE ); for (var f : skippedFiles) { LOG.debug("File not part of any tsconfig.json: {}", f); - analyze(f, null); + analyzeFile(f, null, null); } } success = true; @@ -139,7 +134,7 @@ private void analyzeProgram(TsProgram program, Set analyzedFiles) thr continue; } if (analyzedFiles.add(inputFile)) { - analyze(inputFile, program); + analyzeFile(inputFile, null, program); counter++; } else { LOG.debug( @@ -152,54 +147,4 @@ private void analyzeProgram(TsProgram program, Set analyzedFiles) thr LOG.info("Analyzed {} file(s) with current program", counter); } - private void analyze(InputFile file, @Nullable TsProgram tsProgram) throws IOException { - if (context.isCancelled()) { - throw new CancellationException( - "Analysis interrupted because the SensorContext is in cancelled state" - ); - } - var cacheStrategy = CacheStrategies.getStrategyFor(context, file); - if (cacheStrategy.isAnalysisRequired()) { - try { - LOG.debug("Analyzing file: {}", file.uri()); - progressReport.nextFile(file.toString()); - var fileContent = contextUtils.shouldSendFileContent(file) ? file.contents() : null; - var request = getJsAnalysisRequest(file, tsProgram, fileContent); - - var response = isJavaScript(file) - ? bridgeServer.analyzeJavaScript(request) - : bridgeServer.analyzeTypeScript(request); - - analysisProcessor.processResponse(context, checks, file, response); - cacheStrategy.writeAnalysisToCache( - CacheAnalysis.fromResponse(response.ucfgPaths(), response.cpdTokens()), - file - ); - } catch (IOException e) { - LOG.error("Failed to get response while analyzing " + file.uri(), e); - throw e; - } - } else { - LOG.debug("Processing cache analysis of file: {}", file.uri()); - var cacheAnalysis = cacheStrategy.readAnalysisFromCache(); - analysisProcessor.processCacheAnalysis(context, file, cacheAnalysis); - } - } - - private JsAnalysisRequest getJsAnalysisRequest( - InputFile file, - @Nullable TsProgram tsProgram, - @Nullable String fileContent - ) { - return new JsAnalysisRequest( - file.absolutePath(), - file.type().toString(), - inputFileLanguage(file), - fileContent, - contextUtils.ignoreHeaderComments(), - null, - tsProgram != null ? tsProgram.programId() : null, - analysisMode.getLinterIdFor(file) - ); - } } diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithWatchProgram.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithWatchProgram.java index 8d2eee508d8..1168d97f886 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithWatchProgram.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithWatchProgram.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.List; @@ -33,12 +32,8 @@ import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.scanner.ScannerSide; -import org.sonar.plugins.javascript.CancellationException; -import org.sonar.plugins.javascript.analysis.cache.CacheAnalysis; -import org.sonar.plugins.javascript.analysis.cache.CacheStrategies; import org.sonar.plugins.javascript.bridge.AnalysisWarningsWrapper; import org.sonar.plugins.javascript.bridge.BridgeServer; -import org.sonar.plugins.javascript.bridge.BridgeServer.JsAnalysisRequest; import org.sonar.plugins.javascript.bridge.TsConfigFile; import org.sonar.plugins.javascript.utils.ProgressReport; import org.sonarsource.api.sonarlint.SonarLintSide; @@ -121,52 +116,9 @@ private List loadTsConfigs(List tsConfigPaths) { private void analyzeTsConfig(@Nullable TsConfigFile tsConfigFile, List files) throws IOException { + List tsConfigs = tsConfigFile == null ? List.of() : List.of(tsConfigFile.getFilename()); for (InputFile inputFile : files) { - if (context.isCancelled()) { - throw new CancellationException( - "Analysis interrupted because the SensorContext is in cancelled state" - ); - } - analyze(inputFile, tsConfigFile); - progressReport.nextFile(inputFile.toString()); - } - } - - private void analyze(InputFile file, @Nullable TsConfigFile tsConfigFile) throws IOException { - var cacheStrategy = CacheStrategies.getStrategyFor(context, file); - if (cacheStrategy.isAnalysisRequired()) { - try { - LOG.debug("Analyzing file: {}", file); - var fileContent = contextUtils.shouldSendFileContent(file) ? file.contents() : null; - var tsConfigs = tsConfigFile == null - ? Collections.emptyList() - : List.of(tsConfigFile.getFilename()); - var request = new JsAnalysisRequest( - file.absolutePath(), - file.type().toString(), - inputFileLanguage(file), - fileContent, - contextUtils.ignoreHeaderComments(), - tsConfigs, - null, - analysisMode.getLinterIdFor(file) - ); - var response = isJavaScript(file) - ? bridgeServer.analyzeJavaScript(request) - : bridgeServer.analyzeTypeScript(request); - analysisProcessor.processResponse(context, checks, file, response); - cacheStrategy.writeAnalysisToCache( - CacheAnalysis.fromResponse(response.ucfgPaths(), response.cpdTokens()), - file - ); - } catch (IOException e) { - LOG.error("Failed to get response while analyzing " + file.uri(), e); - throw e; - } - } else { - LOG.debug("Processing cache analysis of file: {}", file.uri()); - var cacheAnalysis = cacheStrategy.readAnalysisFromCache(); - analysisProcessor.processCacheAnalysis(context, file, cacheAnalysis); + analyzeFile(inputFile, tsConfigs, null); } } } diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/JsTsSensor.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/JsTsSensor.java index 1568738e73b..506724d74f1 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/JsTsSensor.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/JsTsSensor.java @@ -25,6 +25,7 @@ import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.api.batch.DependedUpon; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; @@ -36,6 +37,7 @@ import org.sonar.plugins.javascript.JavaScriptFilePredicate; import org.sonar.plugins.javascript.sonarlint.SonarLintTypeCheckingChecker; +@DependedUpon("js-analysis") public class JsTsSensor extends AbstractBridgeSensor { private static final Logger LOG = LoggerFactory.getLogger(JsTsSensor.class); @@ -43,20 +45,23 @@ public class JsTsSensor extends AbstractBridgeSensor { private final AnalysisWithWatchProgram analysisWithWatchProgram; private final JsTsChecks checks; private final SonarLintTypeCheckingChecker javaScriptProjectChecker; + private final AnalysisConsumers consumers; // Constructor for SonarCloud without the optional dependency (Pico doesn't support optional dependencies) public JsTsSensor( JsTsChecks checks, BridgeServer bridgeServer, AnalysisWithProgram analysisWithProgram, - AnalysisWithWatchProgram analysisWithWatchProgram + AnalysisWithWatchProgram analysisWithWatchProgram, + AnalysisConsumers consumers ) { this( checks, bridgeServer, null, analysisWithProgram, - analysisWithWatchProgram + analysisWithWatchProgram, + consumers ); } @@ -65,13 +70,15 @@ public JsTsSensor( BridgeServer bridgeServer, @Nullable SonarLintTypeCheckingChecker javaScriptProjectChecker, AnalysisWithProgram analysisWithProgram, - AnalysisWithWatchProgram analysisWithWatchProgram + AnalysisWithWatchProgram analysisWithWatchProgram, + AnalysisConsumers consumers ) { super(bridgeServer, "JS/TS"); this.analysisWithProgram = analysisWithProgram; this.analysisWithWatchProgram = analysisWithWatchProgram; this.checks = checks; this.javaScriptProjectChecker = javaScriptProjectChecker; + this.consumers = consumers; } @Override @@ -117,8 +124,9 @@ protected void analyzeFiles(List inputFiles) throws IOException { if (tsConfigs.isEmpty()) { LOG.info("No tsconfig.json file found"); } - analysis.initialize(context, checks, analysisMode); + analysis.initialize(context, checks, analysisMode, consumers); analysis.analyzeFiles(inputFiles, tsConfigs); + consumers.doneAnalysis(); } private String createTsConfigFile(String content) throws IOException { diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/package-info.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/package-info.java new file mode 100644 index 00000000000..d3c7bdd729e --- /dev/null +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/package-info.java @@ -0,0 +1,21 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 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. + */ +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.plugins.javascript.analysis; diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java index 5346dcc24b2..09dc5b6de43 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java @@ -39,7 +39,7 @@ class JavaScriptPluginTest { - private static final int BASE_EXTENSIONS = 35; + private static final int BASE_EXTENSIONS = 36; private static final int JS_ADDITIONAL_EXTENSIONS = 4; private static final int TS_ADDITIONAL_EXTENSIONS = 3; private static final int CSS_ADDITIONAL_EXTENSIONS = 3; diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java index 2393fb7739d..46042e11c61 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java @@ -534,7 +534,8 @@ void should_skip_analysis_when_no_files() { checks(ESLINT_BASED_RULE), bridgeServerMock, analysisWithProgram, - analysisWithWatchProgram + analysisWithWatchProgram, + new AnalysisConsumers() ); javaScriptEslintBasedSensor.execute(context); assertThat(logTester.logs(Level.INFO)).contains("No input files found for analysis"); @@ -550,7 +551,8 @@ void handle_missing_node() throws Exception { checks(ESLINT_BASED_RULE), bridgeServerMock, analysisWithProgram, - analysisWithWatchProgram + analysisWithWatchProgram, + new AnalysisConsumers() ); createInputFile(context); @@ -612,7 +614,8 @@ void should_not_create_parsing_issue_when_no_rule() throws IOException { checks(ESLINT_BASED_RULE), bridgeServerMock, analysisWithProgram, - analysisWithWatchProgram + analysisWithWatchProgram, + new AnalysisConsumers() ) .execute(context); Collection issues = context.allIssues(); @@ -793,7 +796,8 @@ private JsTsSensor createSensor( bridgeServerMock, sonarlintTypeCheckingChecker, analysisWithProgram, - analysisWithWatchProgram + analysisWithWatchProgram, + new AnalysisConsumers() ); } diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java index 978855d7437..fbea47081a3 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java @@ -39,6 +39,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -87,6 +88,8 @@ import org.sonar.plugins.javascript.JavaScriptPlugin; import org.sonar.plugins.javascript.TestUtils; import org.sonar.plugins.javascript.analysis.cache.CacheTestUtils; +import org.sonar.plugins.javascript.api.JsAnalysisConsumer; +import org.sonar.plugins.javascript.api.JsFile; import org.sonar.plugins.javascript.bridge.BridgeServer.AnalysisResponse; import org.sonar.plugins.javascript.bridge.BridgeServer.JsAnalysisRequest; import org.sonar.plugins.javascript.bridge.BridgeServer.ParsingError; @@ -776,12 +779,48 @@ void should_save_cached_cpd_with_program() throws IOException { .contains("Processing cache analysis of file: " + file.uri()); } + @Test + void should_invoke_analysis_consumers() throws Exception { + var consumer = new JsAnalysisConsumer() { + final List files = new ArrayList<>(); + boolean done; + + @Override + public void accept(JsFile jsFile) { + files.add(jsFile); + } + + @Override + public void doneAnalysis() { + done = true; + } + }; + + var sensor = new JsTsSensor( + checks(ESLINT_BASED_RULE, "S2260"), + bridgeServerMock, + analysisWithProgram(), + analysisWithWatchProgram(), + new AnalysisConsumers(List.of(consumer)) + ); + + var inputFile = createInputFile(context); + var tsProgram = new TsProgram("1", List.of(inputFile.absolutePath()), List.of(), false, null); + when(bridgeServerMock.createProgram(any())).thenReturn(tsProgram); + + sensor.execute(context); + assertThat(consumer.files).hasSize(1); + assertThat(consumer.files.get(0).inputFile()).isEqualTo(inputFile); + assertThat(consumer.done).isTrue(); + } + private JsTsSensor createSensor() { return new JsTsSensor( checks(ESLINT_BASED_RULE, "S2260"), bridgeServerMock, analysisWithProgram(), - analysisWithWatchProgram() + analysisWithWatchProgram(), + new AnalysisConsumers() ); }