diff options
Diffstat (limited to 'sonar-css-plugin/src/main/java/org/sonar')
13 files changed, 629 insertions, 307 deletions
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java index d627527..a5df2fc 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java @@ -20,17 +20,13 @@  package org.sonar.css.plugin;  import org.sonar.api.Plugin; -import org.sonar.api.SonarProduct; -import org.sonar.api.SonarRuntime;  import org.sonar.api.config.PropertyDefinition;  import org.sonar.api.resources.Qualifiers; -import org.sonar.api.utils.Version; -import org.sonar.css.plugin.bundle.CssBundleHandler; +import org.sonar.css.plugin.bundle.CssAnalyzerBundle; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer;  public class CssPlugin implements Plugin { -  private static final Version ANALYSIS_WARNINGS_MIN_SUPPORTED_SQ_VERSION = Version.create(7, 4); -    static final String FILE_SUFFIXES_KEY = "sonar.css.file.suffixes";    public static final String FILE_SUFFIXES_DEFVALUE = ".css,.less,.scss"; @@ -45,16 +41,14 @@ public class CssPlugin implements Plugin {    @Override    public void define(Context context) { -    boolean externalIssuesSupported = context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 2)); -      context.addExtensions(        MetricSensor.class,        CssLanguage.class,        SonarWayProfile.class, -      new CssRulesDefinition(externalIssuesSupported), -      CssBundleHandler.class, +      CssRulesDefinition.class, +      CssAnalyzerBundle.class, +      CssAnalyzerBridgeServer.class,        CssRuleSensor.class, -      StylelintCommandProvider.class,        StylelintReportSensor.class,        MinifiedFilesFilter.class, @@ -69,30 +63,16 @@ public class CssPlugin implements Plugin {          .build()      ); - -    if (externalIssuesSupported) { -      context.addExtension( -        PropertyDefinition.builder(STYLELINT_REPORT_PATHS) -          .defaultValue(STYLELINT_REPORT_PATHS_DEFAULT_VALUE) -          .name("Stylelint Report Files") -          .description("Paths (absolute or relative) to the JSON files with stylelint issues.") -          .onQualifiers(Qualifiers.PROJECT) -          .subCategory(LINTER_SUBCATEGORY) -          .category(CSS_CATEGORY) -          .multiValues(true) -          .build()); -    } - -    if (isAnalysisWarningsSupported(context.getRuntime())) { -      context.addExtension(AnalysisWarningsWrapper.class); -    } +    context.addExtension( +      PropertyDefinition.builder(STYLELINT_REPORT_PATHS) +        .defaultValue(STYLELINT_REPORT_PATHS_DEFAULT_VALUE) +        .name("Stylelint Report Files") +        .description("Paths (absolute or relative) to the JSON files with stylelint issues.") +        .onQualifiers(Qualifiers.PROJECT) +        .subCategory(LINTER_SUBCATEGORY) +        .category(CSS_CATEGORY) +        .multiValues(true) +        .build());    } -  /** -   * Drop this and related when the minimum supported version of SonarQube API reaches 7.4. -   */ -  private static boolean isAnalysisWarningsSupported(SonarRuntime runtime) { -    return runtime.getApiVersion().isGreaterThanOrEqual(ANALYSIS_WARNINGS_MIN_SUPPORTED_SQ_VERSION) -      && runtime.getProduct() != SonarProduct.SONARLINT; -  }  } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java index 87328d7..644ece8 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java @@ -21,16 +21,22 @@ package org.sonar.css.plugin;  import com.google.gson.Gson;  import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException;  import java.io.File;  import java.io.IOException; +import java.net.URI;  import java.nio.charset.StandardCharsets;  import java.nio.file.Files; -import java.nio.file.Paths;  import java.util.Collections; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit;  import java.util.regex.Matcher;  import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport;  import javax.annotation.Nullable; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FilePredicates;  import org.sonar.api.batch.fs.FileSystem;  import org.sonar.api.batch.fs.InputFile;  import org.sonar.api.batch.rule.CheckFactory; @@ -39,109 +45,180 @@ import org.sonar.api.batch.sensor.SensorContext;  import org.sonar.api.batch.sensor.SensorDescriptor;  import org.sonar.api.batch.sensor.issue.NewIssue;  import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.notifications.AnalysisWarnings;  import org.sonar.api.rule.RuleKey;  import org.sonar.api.utils.log.Logger;  import org.sonar.api.utils.log.Loggers;  import org.sonar.css.plugin.CssRules.StylelintConfig; -import org.sonar.css.plugin.StylelintReport.Issue; -import org.sonar.css.plugin.StylelintReport.IssuesPerFile; -import org.sonar.css.plugin.bundle.BundleHandler; -import org.sonarsource.nodejs.NodeCommand; +import org.sonar.css.plugin.server.AnalyzerBridgeServer.Issue; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer; +import org.sonar.css.plugin.server.AnalyzerBridgeServer.Request; +import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException; +import org.sonarsource.analyzer.commons.ProgressReport;  import org.sonarsource.nodejs.NodeCommandException;  public class CssRuleSensor implements Sensor {    private static final Logger LOG = Loggers.get(CssRuleSensor.class); +  private static final String CONFIG_PATH = "css-bundle/stylelintconfig.json"; -  private final BundleHandler bundleHandler;    private final CssRules cssRules; -  private final LinterCommandProvider linterCommandProvider; -  @Nullable -  private final AnalysisWarningsWrapper analysisWarnings; +  private final CssAnalyzerBridgeServer cssAnalyzerBridgeServer; +  private final AnalysisWarnings analysisWarnings; +    public CssRuleSensor( -    BundleHandler bundleHandler,      CheckFactory checkFactory, -    LinterCommandProvider linterCommandProvider, -    @Nullable AnalysisWarningsWrapper analysisWarnings +    CssAnalyzerBridgeServer cssAnalyzerBridgeServer, +    @Nullable AnalysisWarnings analysisWarnings    ) { -    this.bundleHandler = bundleHandler; -    this.linterCommandProvider = linterCommandProvider;      this.cssRules = new CssRules(checkFactory); +    this.cssAnalyzerBridgeServer = cssAnalyzerBridgeServer;      this.analysisWarnings = analysisWarnings;    } -  public CssRuleSensor( -    BundleHandler bundleHandler, -    CheckFactory checkFactory, -    LinterCommandProvider linterCommandProvider -  ) { -    this(bundleHandler, checkFactory, linterCommandProvider, null); -  } -    @Override    public void describe(SensorDescriptor descriptor) {      descriptor +      .createIssuesForRuleRepository("css")        .name("SonarCSS Rules");    }    @Override    public void execute(SensorContext context) { +    reportOldNodeProperty(context); + +    boolean failFast = context.config().getBoolean("sonar.internal.analysis.failFast").orElse(false); + +    try { +      List<InputFile> inputFiles = getInputFiles(context); +      if (inputFiles.isEmpty()) { +        LOG.info("No CSS, PHP or HTML files are found in the project. CSS analysis is skipped."); +      } else { +        cssAnalyzerBridgeServer.startServerLazily(context); +        File configFile = createLinterConfig(context); +        analyzeFiles(context, inputFiles, configFile); +      } +    } catch (CancellationException e) { +      // do not propagate the exception +      LOG.info(e.toString()); +    } catch (ServerAlreadyFailedException e) { +      LOG.debug("Skipping start of css-bundle server due to the failure during first analysis"); +      LOG.debug("Skipping execution of CSS rules due to the problems with css-bundle server"); +    } catch (NodeCommandException e) { +      LOG.error(e.getMessage(), e); +      reportAnalysisWarning("CSS rules were not executed. " + e.getMessage()); +      if (failFast) { +        throw new IllegalStateException("Analysis failed (\"sonar.internal.analysis.failFast\"=true)", e); +      } +    } catch (Exception e) { +      LOG.error("Failure during analysis, " + cssAnalyzerBridgeServer.getCommandInfo(), e); +      if (failFast) { +        throw new IllegalStateException("Analysis failed (\"sonar.internal.analysis.failFast\"=true)", e); +      } +    } +  } + +  private void reportOldNodeProperty(SensorContext context) {      if (context.config().hasKey(CssPlugin.FORMER_NODE_EXECUTABLE)) {        String msg = "Property '" + CssPlugin.FORMER_NODE_EXECUTABLE + "' is ignored, 'sonar.nodejs.executable' should be used instead";        LOG.warn(msg); -      if (analysisWarnings != null) { -        analysisWarnings.addUnique(msg); +      reportAnalysisWarning(msg); +    } +  } + +  void analyzeFiles(SensorContext context, List<InputFile> inputFiles, File configFile) throws InterruptedException, IOException { +    ProgressReport progressReport = new ProgressReport("Analysis progress", TimeUnit.SECONDS.toMillis(10)); +    boolean success = false; +    try { +      progressReport.start(inputFiles.stream().map(InputFile::toString).collect(Collectors.toList())); +      for (InputFile inputFile : inputFiles) { +        if (context.isCancelled()) { +          throw new CancellationException("Analysis interrupted because the SensorContext is in cancelled state"); +        } +        if (cssAnalyzerBridgeServer.isAlive()) { +          try { +            analyzeFile(context, inputFile, configFile); +          } catch (IOException | RuntimeException e) { +            throw new IOException("Failure during analysis of " + inputFile.uri() + ": " + e.getMessage()); +          } +          progressReport.nextFile(); +        } else { +          throw new IllegalStateException("css-bundle server is not answering"); +        }        } +      success = true; +    } finally { +      if (success) { +        progressReport.stop(); +      } else { +        progressReport.cancel(); +      } +      progressReport.join();      } +  } -    if (cssRules.isEmpty()) { -      LOG.warn("No rules are activated in CSS Quality Profile"); +  void analyzeFile(SensorContext context, InputFile inputFile, File configFile) throws IOException { +    URI uri = inputFile.uri(); +    if (!"file".equalsIgnoreCase(uri.getScheme())) { +      LOG.debug("Skipping {} as it has not 'file' scheme", uri);        return;      } +    Request request = new Request(new File(uri).getAbsolutePath(), configFile.toString()); +    LOG.debug("Analyzing " + request.filePath); +    Issue[] issues = cssAnalyzerBridgeServer.analyze(request); +    LOG.debug("Found {} issue(s)", issues.length); +    saveIssues(context, inputFile, issues); +  } -    File deployDestination = context.fileSystem().workDir(); +  private void saveIssues(SensorContext context, InputFile inputFile, Issue[] issues) { +    for (Issue issue : issues) { +      NewIssue sonarIssue = context.newIssue(); -    try { -      bundleHandler.deployBundle(deployDestination); -      createLinterConfig(deployDestination); -      StringBuilder output = new StringBuilder(); +      RuleKey ruleKey = cssRules.getActiveSonarKey(issue.rule); -      NodeCommand nodeCommand = linterCommandProvider.nodeCommand(deployDestination, context, output::append, LOG::error); -      LOG.debug("Starting process: " + nodeCommand.toString()); -      nodeCommand.start(); +      if (ruleKey == null) { +        if ("CssSyntaxError".equals(issue.rule)) { +          String errorMessage = issue.text.replace("(CssSyntaxError)", "").trim(); +          LOG.error("Failed to parse {}, line {}, {}", inputFile.uri(), issue.line, errorMessage); +        } else { +          LOG.error("Unknown stylelint rule or rule not enabled: '" + issue.rule + "'"); +        } -      if (isSuccessful(nodeCommand.waitFor())) { -        saveIssues(context, output.toString()); -      } -    } catch (NodeCommandException e) { -      LOG.error(e.getMessage() + " No CSS files will be analyzed.", e); -      if (analysisWarnings != null) { -        analysisWarnings.addUnique("CSS files were not analyzed. " + e.getMessage()); +      } else { +        NewIssueLocation location = sonarIssue.newLocation() +          .on(inputFile) +          .at(inputFile.selectLine(issue.line)) +          .message(normalizeMessage(issue.text)); + +        sonarIssue +          .at(location) +          .forRule(ruleKey) +          .save();        } -    } catch (Exception e) { -      LOG.error("Failed to run external linting process", e);      }    } -  private boolean isSuccessful(int exitValue) { -    // exit codes 0 and 2 are expected. 0 - means no issues were found, 2 - means that at least one "error-level" rule found issue -    // see https://github.com/stylelint/stylelint/blob/master/docs/user-guide/cli.md#exit-codes -    boolean isSuccessful = exitValue == 0 || exitValue == 2; -    if (!isSuccessful) { -      LOG.error("Analysis didn't terminate normally, please verify ERROR and WARN logs above. Exit code {}", exitValue); -    } -    return isSuccessful; +  private static List<InputFile> getInputFiles(SensorContext context) { +    FileSystem fileSystem = context.fileSystem(); +    FilePredicates predicates = context.fileSystem().predicates(); +    FilePredicate mainFilePredicate = predicates.and( +      fileSystem.predicates().hasType(InputFile.Type.MAIN), +      fileSystem.predicates().hasLanguages(CssLanguage.KEY, "php", "web")); +    return StreamSupport.stream(fileSystem.inputFiles(mainFilePredicate).spliterator(), false) +      .collect(Collectors.toList());    } -  private void createLinterConfig(File deployDestination) throws IOException { -    String configPath = linterCommandProvider.configPath(deployDestination); +  private File createLinterConfig(SensorContext context) throws IOException {      StylelintConfig config = cssRules.getConfig();      final GsonBuilder gsonBuilder = new GsonBuilder();      gsonBuilder.registerTypeAdapter(StylelintConfig.class, config);      final Gson gson = gsonBuilder.create();      String configAsJson = gson.toJson(config); -    Files.write(Paths.get(configPath), Collections.singletonList(configAsJson), StandardCharsets.UTF_8); +    File configFile = new File(context.fileSystem().workDir(), CONFIG_PATH).getAbsoluteFile(); +    Files.createDirectories(configFile.toPath().getParent()); +    Files.write(configFile.toPath(), Collections.singletonList(configAsJson), StandardCharsets.UTF_8); +    return configFile;    }    private static String normalizeMessage(String message) { @@ -155,51 +232,9 @@ public class CssRuleSensor implements Sensor {      }    } -  private void saveIssues(SensorContext context, String issuesAsJson) { -    IssuesPerFile[] issues; -    try { -      issues = new Gson().fromJson(issuesAsJson, IssuesPerFile[].class); -    } catch (JsonSyntaxException e) { -      throw new IllegalStateException("Failed to parse JSON result of external linting process execution: \n-------\n" + issuesAsJson + "\n-------", e); -    } - -    FileSystem fileSystem = context.fileSystem(); - -    for (IssuesPerFile issuesPerFile : issues) { -      InputFile inputFile = fileSystem.inputFile(fileSystem.predicates().hasAbsolutePath(issuesPerFile.source)); - -      if (inputFile != null) { -        for (Issue issue : issuesPerFile.warnings) { -          saveIssue(context, inputFile, issue); -        } -      } +  private void reportAnalysisWarning(String message) { +    if (analysisWarnings != null) { +      analysisWarnings.addUnique(message);      }    } - -  private void saveIssue(SensorContext context, InputFile inputFile, Issue issue) { -    NewIssue sonarIssue = context.newIssue(); - -    RuleKey ruleKey = cssRules.getActiveSonarKey(issue.rule); - -    if (ruleKey == null) { -      if ("CssSyntaxError".equals(issue.rule)) { -        String errorMessage = issue.text.replace("(CssSyntaxError)", "").trim(); -        LOG.error("Failed to parse {}, line {}, {}", inputFile.uri(), issue.line, errorMessage); -      } else { -        LOG.error("Unknown stylelint rule or rule not enabled: '" + issue.rule + "'"); -      } - -    } else { -      NewIssueLocation location = sonarIssue.newLocation() -        .on(inputFile) -        .at(inputFile.selectLine(issue.line)) -        .message(normalizeMessage(issue.text)); - -      sonarIssue -        .at(location) -        .forRule(ruleKey) -        .save(); -    } -  } -  } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java index cc88ad3..4d0d959 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java @@ -31,12 +31,6 @@ public class CssRulesDefinition implements RulesDefinition {    public static final String RESOURCE_FOLDER = "org/sonar/l10n/css/rules/"; -  private final boolean externalIssuesSupported; - -  public CssRulesDefinition(boolean externalIssuesSupported) { -    this.externalIssuesSupported = externalIssuesSupported; -  } -    @Override    public void define(Context context) {      NewRepository repository = context @@ -47,8 +41,6 @@ public class CssRulesDefinition implements RulesDefinition {      ruleMetadataLoader.addRulesByAnnotatedClass(repository, CssRules.getRuleClasses());      repository.done(); -    if (externalIssuesSupported) { -      StylelintReportSensor.getStylelintRuleLoader().createExternalRuleRepository(context); -    } +    StylelintReportSensor.getStylelintRuleLoader().createExternalRuleRepository(context);    }  } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java deleted file mode 100644 index c43af5c..0000000 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarCSS - * Copyright (C) 2018-2019 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.css.plugin; - -import java.io.File; -import java.nio.file.Paths;import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.sonar.api.batch.ScannerSide; -import org.sonar.api.batch.sensor.SensorContext; -import org.sonarsource.nodejs.NodeCommand; - -@ScannerSide -public class StylelintCommandProvider implements LinterCommandProvider { - -  private static final String CONFIG_PATH = "css-bundle/stylelintconfig.json"; -  private static final List<String> LANGUAGES_TO_ANALYZE = Arrays.asList("css", "html", "php"); - -  @Override -  public NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error) { -    String projectBaseDir = context.fileSystem().baseDir().getAbsolutePath(); - -    List<String> suffixes = LANGUAGES_TO_ANALYZE.stream() -      .map(language -> context.config().getStringArray("sonar." + language + ".file.suffixes")) -      .flatMap(Stream::of) -      .collect(Collectors.toList()); - -    String filesGlob = "**" + File.separator + "*{" + String.join(",", suffixes) + "}"; -    String filesToAnalyze = Paths.get(projectBaseDir, "TOREPLACE").toString(); -    filesToAnalyze = filesToAnalyze.replace("TOREPLACE", filesGlob); - -    String[] args = { -      new File(deployDestination, "css-bundle/node_modules/stylelint/bin/stylelint").getAbsolutePath(), -      filesToAnalyze, -      "--config", new File(deployDestination, CONFIG_PATH).getAbsolutePath(), -      "-f", "json" -    }; - -    return NodeCommand.builder() -      .outputConsumer(output) -      .errorConsumer(error) -      .minNodeVersion(6) -      .configuration(context.config()) -      .nodeJsArgs(args) -      .build(); -  } - -  @Override -  public String configPath(File deployDestination) { -    return new File(deployDestination, CONFIG_PATH).getAbsolutePath(); -  } - -} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/LinterCommandProvider.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/Bundle.java index 74a343c..8e9ce9b 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/LinterCommandProvider.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/Bundle.java @@ -17,17 +17,18 @@   * 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.css.plugin; +package org.sonar.css.plugin.bundle; -import java.io.File; -import java.util.function.Consumer; -import org.sonar.api.batch.sensor.SensorContext; -import org.sonarsource.nodejs.NodeCommand; +import java.io.IOException; +import java.nio.file.Path; -public interface LinterCommandProvider { +public interface Bundle { -  NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error); +  void deploy(Path deployLocation) throws IOException; -  String configPath(File deployDestination); +  /** +   * should be called after deploy(Path deployLocation) +   */ +  String startServerScript();  } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java new file mode 100644 index 0000000..ba5d714 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java @@ -0,0 +1,82 @@ +/* + * SonarCSS + * Copyright (C) 2018-2019 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.css.plugin.bundle; + +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.sonar.api.internal.google.common.annotations.VisibleForTesting; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.css.plugin.Zip; +import org.sonarsource.api.sonarlint.SonarLintSide; + +import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES; + +@ScannerSide +@SonarLintSide(lifespan = MULTIPLE_ANALYSES) +public class CssAnalyzerBundle implements Bundle { + +  private static final Logger LOG = Loggers.get(CssAnalyzerBundle.class); +  private static final Profiler PROFILER = Profiler.createIfDebug(LOG); + +  // this archive is created in css-bundle module +  private static final String DEFAULT_BUNDLE_LOCATION = "/css-bundle.zip"; +  private static final Path DEFAULT_STARTUP_SCRIPT = Paths.get("css-bundle", "bin", "server"); + +  final String bundleLocation; + +  private String startServerScript = DEFAULT_STARTUP_SCRIPT.toString(); + +  public CssAnalyzerBundle() { +    this(DEFAULT_BUNDLE_LOCATION); +  } + +  @VisibleForTesting +  CssAnalyzerBundle(String bundleLocation) { +    this.bundleLocation = bundleLocation; +  } + +  @Override +  public void deploy(Path deployLocation) { +    PROFILER.startDebug("Deploying bundle"); +    LOG.debug("Deploying css-bundle into {}", deployLocation); +    InputStream bundle = getClass().getResourceAsStream(bundleLocation); +    if (bundle == null) { +      throw new IllegalStateException("css-bundle not found in " + bundleLocation); +    } +    try { +      LOG.debug("Deploying css-bundle to {}", deployLocation.toAbsolutePath()); +      Zip.extract(bundle, deployLocation.toFile()); +      startServerScript = deployLocation.resolve(DEFAULT_STARTUP_SCRIPT).toAbsolutePath().toString(); +    } catch (Exception e) { +      throw new IllegalStateException("Failed to deploy css-bundle (with classpath '" + bundleLocation + "')", e); +    } +    PROFILER.stopDebug(); +  } + +  @Override +  public String startServerScript() { +    return startServerScript; +  } + +} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java deleted file mode 100644 index 41019b5..0000000 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarCSS - * Copyright (C) 2018-2019 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.css.plugin.bundle; - -import java.io.File; -import java.io.InputStream; -import org.sonar.api.batch.ScannerSide; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.css.plugin.Zip; - -@ScannerSide -public class CssBundleHandler implements BundleHandler { - -  private static final String BUNDLE_LOCATION = "/css-bundle.zip"; -  private static final Logger LOG = Loggers.get(CssBundleHandler.class); -  String bundleLocation = BUNDLE_LOCATION; - -  /** -   * Extracting "css-bundle.zip" (containing stylelint) -   * to deployDestination (".sonar" directory of the analyzed project). -   */ -  @Override -  public void deployBundle(File deployDestination) { -    InputStream bundle = getClass().getResourceAsStream(bundleLocation); -    if (bundle == null) { -      throw new IllegalStateException("CSS bundle not found at " + bundleLocation); -    } -    try { -      LOG.debug("Deploying bundle to {}", deployDestination.getAbsolutePath()); -      Zip.extract(bundle, deployDestination); -    } catch (Exception e) { -      throw new IllegalStateException("Failed to deploy CSS bundle (with classpath '" + bundleLocation + "')", e); -    } -  } - -} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java new file mode 100644 index 0000000..0656b06 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java @@ -0,0 +1,64 @@ +/* + * SonarCSS + * Copyright (C) 2018-2019 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.css.plugin.server; + +import java.io.IOException; +import org.sonar.api.Startable; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.scanner.ScannerSide; +import org.sonarsource.api.sonarlint.SonarLintSide; + +import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES; + +@ScannerSide +@SonarLintSide(lifespan = MULTIPLE_ANALYSES) +public interface AnalyzerBridgeServer extends Startable { + +  void startServerLazily(SensorContext context) throws IOException; + +  Issue[] analyze(Request request) throws IOException; + +  String getCommandInfo(); + +  boolean isAlive(); + +  class Request { +    public final String filePath; +    public final String configFile; + +    public Request(String filePath, String configFile) { +      this.filePath = filePath; +      this.configFile = configFile; +    } +  } + +  class Issue { +    public final Integer line; +    public final String rule; +    public final String text; + +    public Issue(Integer line, String rule, String text) { +      this.line = line; +      this.rule = rule; +      this.text = text; +    } +  } + +} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java new file mode 100644 index 0000000..fb8ee45 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java @@ -0,0 +1,232 @@ +/* + * SonarCSS + * Copyright (C) 2018-2019 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.css.plugin.server; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.css.plugin.bundle.Bundle; +import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException; +import org.sonarsource.nodejs.NodeCommand; +import org.sonarsource.nodejs.NodeCommandBuilder; +import org.sonarsource.nodejs.NodeCommandException; + +public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer { + +  private static final Logger LOG = Loggers.get(CssAnalyzerBridgeServer.class); +  private static final Profiler PROFILER = Profiler.createIfDebug(LOG); + +  private static final int DEFAULT_TIMEOUT_SECONDS = 60; +  // internal property to set "--max-old-space-size" for Node process running this server +  private static final String MAX_OLD_SPACE_SIZE_PROPERTY = "sonar.css.node.maxspace"; +  private static final Gson GSON = new Gson(); + +  private final OkHttpClient client; +  private final NodeCommandBuilder nodeCommandBuilder; +  final int timeoutSeconds; +  private final Bundle bundle; +  private int port; +  private NodeCommand nodeCommand; +  private boolean failedToStart; + +  // Used by pico container for dependency injection +  @SuppressWarnings("unused") +  public CssAnalyzerBridgeServer(Bundle bundle) { +    this(NodeCommand.builder(), DEFAULT_TIMEOUT_SECONDS, bundle); +  } + +  protected CssAnalyzerBridgeServer(NodeCommandBuilder nodeCommandBuilder, int timeoutSeconds, Bundle bundle) { +    this.nodeCommandBuilder = nodeCommandBuilder; +    this.timeoutSeconds = timeoutSeconds; +    this.bundle = bundle; +    this.client = new OkHttpClient.Builder() +      .callTimeout(Duration.ofSeconds(timeoutSeconds)) +      .readTimeout(Duration.ofSeconds(timeoutSeconds)) +      .build(); +  } + +  // for testing purposes +  public void deploy(File deployLocation) throws IOException { +    bundle.deploy(deployLocation.toPath()); +  } + +  public void startServer(SensorContext context) throws IOException { +    PROFILER.startDebug("Starting server"); +    port = NetUtils.findOpenPort(); + +    File scriptFile = new File(bundle.startServerScript()); +    if (!scriptFile.exists()) { +      throw new NodeCommandException("Node.js script to start css-bundle server doesn't exist: " + scriptFile.getAbsolutePath()); +    } + +    initNodeCommand(context, scriptFile); + +    LOG.debug("Starting Node.js process to start css-bundle server at port " + port); +    nodeCommand.start(); + +    if (!NetUtils.waitServerToStart("localhost", port, timeoutSeconds * 1000)) { +      throw new NodeCommandException("Failed to start server (" + timeoutSeconds + "s timeout)"); +    } +    PROFILER.stopDebug(); +  } + +  private void initNodeCommand(SensorContext context, File scriptFile) { +    nodeCommandBuilder +      .outputConsumer(message -> { +        if (message.startsWith("DEBUG")) { +          LOG.debug(message.substring(5).trim()); +        } else if (message.startsWith("WARN")) { +          LOG.warn(message.substring(4).trim()); +        } else { +          LOG.info(message); +        } +      }) +      .minNodeVersion(8) +      .configuration(context.config()) +      .script(scriptFile.getAbsolutePath()) +      .scriptArgs(String.valueOf(port)); + +    context.config() +      .getInt(MAX_OLD_SPACE_SIZE_PROPERTY) +      .ifPresent(nodeCommandBuilder::maxOldSpaceSize); + +    nodeCommand = nodeCommandBuilder.build(); +  } + +  @Override +  public void startServerLazily(SensorContext context) throws IOException { +    // required for SonarLint context to avoid restarting already failed server +    if (failedToStart) { +      throw new ServerAlreadyFailedException(); +    } + +    try { +      if (isAlive()) { +        LOG.debug("css-bundle server is up, no need to start."); +        return; +      } +      deploy(context.fileSystem().workDir()); +      startServer(context); +    } catch (NodeCommandException e) { +      failedToStart = true; +      throw e; +    } +  } + +  @Override +  public Issue[] analyze(Request request) throws IOException { +    String json = GSON.toJson(request); +    return parseResponse(request(json)); +  } + +  private String request(String json) throws IOException { +    okhttp3.Request request = new okhttp3.Request.Builder() +      .url(url("analyze")) +      .post(RequestBody.create(MediaType.get("application/json"), json)) +      .build(); + +    try (Response response = client.newCall(request).execute()) { +      // in this case response.body() is never null (according to docs) +      return response.body().string(); +    } +  } + +  private static Issue[] parseResponse(String result) { +    try { +      return GSON.fromJson(result, Issue[].class); +    } catch (JsonSyntaxException e) { +      String msg = "Failed to parse response: \n-----\n" + result + "\n-----\n"; +      LOG.error(msg, e); +      throw new IllegalStateException("Failed to parse response", e); +    } +  } + +  public boolean isAlive() { +    if (nodeCommand == null) { +      return false; +    } +    okhttp3.Request request = new okhttp3.Request.Builder() +      .url(url("status")) +      .get() +      .build(); + +    try (Response response = client.newCall(request).execute()) { +      String body = response.body().string(); +      // in this case response.body() is never null (according to docs) +      return "OK!".equals(body); +    } catch (IOException e) { +      LOG.error("Error requesting server status. Server is probably dead.", e); +      return false; +    } +  } + +  @Override +  public String getCommandInfo() { +    if (nodeCommand == null) { +      return "Node.js command to start css-bundle server was not built yet."; +    } else { +      return "Node.js command to start css-bundle was: " + nodeCommand.toString(); +    } +  } + +  @Override +  public void start() { +    // Server is started lazily by the sensor +  } + +  @Override +  public void stop() { +    clean(); +  } + +  void clean() { +    if (nodeCommand != null) { +      nodeCommand.destroy(); +      nodeCommand = null; +    } +  } + +  private HttpUrl url(String endpoint) { +    HttpUrl.Builder builder = new HttpUrl.Builder(); +    return builder +      .scheme("http") +      .host("localhost") +      .port(port) +      .addPathSegment(endpoint) +      .build(); +  } + +  // for testing purposes +  public void setPort(int port) { +    this.port = port; +  } + +} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java new file mode 100644 index 0000000..fd3ab85 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java @@ -0,0 +1,64 @@ +/* + * SonarCSS + * Copyright (C) 2018-2019 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.css.plugin.server; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; + +public class NetUtils { + +  private NetUtils() { +  } + +  public static int findOpenPort() throws IOException { +    try (ServerSocket socket = new ServerSocket(0)) { +      return socket.getLocalPort(); +    } +  } + +  public static boolean waitServerToStart(String host, int port, int timeoutMs) { +    int sleepStep = 20; +    SocketAddress address = new InetSocketAddress(host, port); +    long start = System.currentTimeMillis(); +    try { +      while (!serverListening(address)) { +        if (System.currentTimeMillis() - start > timeoutMs) { +          return false; +        } +        Thread.sleep(sleepStep); +      } +    } catch (InterruptedException e) { +      Thread.currentThread().interrupt(); +    } +    return true; +  } + +  private static boolean serverListening(SocketAddress address) { +    try (Socket s = new Socket()) { +      s.connect(address, 1); +      return true; +    } catch (Exception e) { +      return false; +    } +  } +} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/AnalysisWarningsWrapper.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java index 84871bf..0865aa4 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/AnalysisWarningsWrapper.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java @@ -17,27 +17,11 @@   * 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.css.plugin; - -import org.sonar.api.batch.InstantiationStrategy; -import org.sonar.api.batch.ScannerSide; -import org.sonar.api.notifications.AnalysisWarnings; +package org.sonar.css.plugin.server.exception;  /** - * Wrap an AnalysisWarnings instance, available since SQ API 7.4. - * Do not load this class on older runtimes. - * Drop this class when the minimum supported version of SonarQube API reaches 7.4. + * This exception is required to inform sensor about analyzer bridge server start up failure in SonarLint + * It is required to not try to start it again   */ -@ScannerSide -@InstantiationStrategy("PER_BATCH") -public class AnalysisWarningsWrapper { -  private final AnalysisWarnings analysisWarnings; - -  public AnalysisWarningsWrapper(AnalysisWarnings analysisWarnings) { -    this.analysisWarnings = analysisWarnings; -  } - -  public void addUnique(String text) { -    this.analysisWarnings.addUnique(text); -  } +public class ServerAlreadyFailedException extends RuntimeException {  } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/BundleHandler.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java index 3000751..4f5b753 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/BundleHandler.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java @@ -17,12 +17,5 @@   * 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.css.plugin.bundle; - -import java.io.File; - -public interface BundleHandler { - -  void deployBundle(File deployDestination); - -} +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.css.plugin.server.exception; diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java new file mode 100644 index 0000000..66fd848 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java @@ -0,0 +1,21 @@ +/* + * SonarCSS + * Copyright (C) 2018-2019 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.css.plugin.server;  | 
