aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-css-plugin/src/main/java/org/sonar
diff options
context:
space:
mode:
authorElena Vilchik2019-12-18 17:10:10 +0100
committerAlban Auzeill2019-12-18 17:10:10 +0100
commitc8f0071c4f5336dfe0efc5d3c218ab49f2401264 (patch)
tree254cd5ed9531d7c62bab4f8ec082e085795ecb8f /sonar-css-plugin/src/main/java/org/sonar
parent13fe08e87c8a70ffe6e248b774ef826bbe1f779d (diff)
downloadsonar-css-c8f0071c4f5336dfe0efc5d3c218ab49f2401264.tar.bz2
Rely on NodeJS API of Stylelint to execute CSS rules (#221)
Diffstat (limited to 'sonar-css-plugin/src/main/java/org/sonar')
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java50
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java235
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java10
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java72
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/Bundle.java (renamed from sonar-css-plugin/src/main/java/org/sonar/css/plugin/LinterCommandProvider.java)17
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java82
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java54
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java64
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java232
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java64
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java (renamed from sonar-css-plugin/src/main/java/org/sonar/css/plugin/AnalysisWarningsWrapper.java)24
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java (renamed from sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/BundleHandler.java)11
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java21
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;