diff options
Diffstat (limited to 'sonar-css-plugin/src')
14 files changed, 1439 insertions, 34 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 b4f78f7..c35836f 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 @@ -22,6 +22,7 @@ package org.sonar.css.plugin; import org.sonar.api.Plugin; 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; public class CssPlugin implements Plugin { @@ -29,19 +30,26 @@ public class CssPlugin implements Plugin { static final String FILE_SUFFIXES_KEY = "sonar.css.file.suffixes"; public static final String FILE_SUFFIXES_DEFVALUE = ".css,.less,.scss"; + public static final String STYLELINT_REPORT_PATHS = "sonar.css.stylelint.reportPaths"; + public static final String STYLELINT_REPORT_PATHS_DEFAULT_VALUE = ""; + private static final String CSS_CATEGORY = "CSS"; + private static final String LINTER_SUBCATEGORY = "Popular Rule Engines"; private static final String GENERAL_SUBCATEGORY = "General"; @Override public void define(Context context) { + boolean externalIssuesSupported = context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 2)); + context.addExtensions( MetricSensor.class, CssLanguage.class, SonarWayProfile.class, - CssRulesDefinition.class, + new CssRulesDefinition(externalIssuesSupported), CssBundleHandler.class, CssRuleSensor.class, StylelintCommandProvider.class, + StylelintReportSensor.class, PropertyDefinition.builder(FILE_SUFFIXES_KEY) .defaultValue(FILE_SUFFIXES_DEFVALUE) @@ -53,5 +61,19 @@ public class CssPlugin implements Plugin { .multiValues(true) .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.MODULE, Qualifiers.PROJECT) + .subCategory(LINTER_SUBCATEGORY) + .category(CSS_CATEGORY) + .multiValues(true) + .build()); + } } } 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 0601839..4664921 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 @@ -36,7 +36,10 @@ 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.rule.RuleKey; 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; public class CssRuleSensor implements Sensor { @@ -55,8 +58,7 @@ public class CssRuleSensor implements Sensor { public void describe(SensorDescriptor descriptor) { descriptor .onlyOnLanguage(CssLanguage.KEY) - .name("SonarCSS Rules") - .onlyOnFileType(InputFile.Type.MAIN); + .name("SonarCSS Rules"); } @Override @@ -107,24 +109,17 @@ public class CssRuleSensor implements Sensor { .at(inputFile.selectLine(issue.line)) .message(issue.text); + RuleKey ruleKey = cssRules.getActiveSonarKey(issue.rule); + if (ruleKey == null) { + throw new IllegalStateException("Unknown stylelint rule or rule not enabled " + issue.rule); + } sonarIssue .at(location) - .forRule(cssRules.getSonarKey(issue.rule)) + .forRule(ruleKey) .save(); } } } } - static class IssuesPerFile { - String source; - Issue[] warnings; - } - - static class Issue { - int line; - String rule; - String text; - } - } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRules.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRules.java index 92d59aa..0db8f90 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRules.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRules.java @@ -19,25 +19,25 @@ */ package org.sonar.css.plugin; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.rule.Checks; import org.sonar.api.rule.RuleKey; +import org.sonar.css.plugin.rules.ColorNoInvalidHex; import org.sonar.css.plugin.rules.CommentNoEmpty; import org.sonar.css.plugin.rules.CssRule; import org.sonar.css.plugin.rules.DeclarationBlockNoDuplicateProperties; import org.sonar.css.plugin.rules.KeyframeDeclarationNoImportant; import org.sonar.css.plugin.rules.NoEmptySource; -import org.sonar.css.plugin.rules.ColorNoInvalidHex; import org.sonar.css.plugin.rules.StringNoNewline; import org.sonar.css.plugin.rules.UnitNoUnknown; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - public class CssRules { private final Map<String, RuleKey> stylelintKeyToRuleKey; @@ -66,12 +66,9 @@ public class CssRules { )); } - public RuleKey getSonarKey(String stylelintKey) { - RuleKey ruleKey = stylelintKeyToRuleKey.get(stylelintKey); - if (ruleKey == null) { - throw new IllegalStateException("Unknown stylelint rule or rule not enabled " + stylelintKey); - } - return ruleKey; + @Nullable + public RuleKey getActiveSonarKey(String stylelintKey) { + return stylelintKeyToRuleKey.get(stylelintKey); } public StylelintConfig getConfig() { 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 a338d7f..203dcb8 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,6 +31,12 @@ public class CssRulesDefinition implements RulesDefinition { public static final String RESOURCE_FOLDER = "org/sonar/l10n/css/rules/css"; + private boolean externalIssuesSupported; + + public CssRulesDefinition(boolean externalIssuesSupported) { + this.externalIssuesSupported = externalIssuesSupported; + } + @Override public void define(Context context) { NewRepository repository = context @@ -40,5 +46,9 @@ public class CssRulesDefinition implements RulesDefinition { RuleMetadataLoader ruleMetadataLoader = new RuleMetadataLoader(RESOURCE_FOLDER, PROFILE_PATH); ruleMetadataLoader.addRulesByAnnotatedClass(repository, CssRules.getRuleClasses()); repository.done(); + + if (externalIssuesSupported) { + StylelintReportSensor.createExternalRuleRepository(context); + } } } diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintReport.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintReport.java new file mode 100644 index 0000000..723b698 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintReport.java @@ -0,0 +1,38 @@ +/* + * SonarCSS + * Copyright (C) 2018-2018 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; + +public class StylelintReport { + + private StylelintReport(){ + } + + static class IssuesPerFile { + String source; + Issue[] warnings; + } + + static class Issue { + int line; + String rule; + String text; + } + +} diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintReportSensor.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintReportSensor.java new file mode 100644 index 0000000..a2225b3 --- /dev/null +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintReportSensor.java @@ -0,0 +1,219 @@ +/* + * SonarCSS + * Copyright (C) 2018-2018 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 com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.CheckFactory; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +import org.sonar.api.server.rule.RulesDefinition.Context; +import org.sonar.api.server.rule.RulesDefinition.NewRepository; +import org.sonar.api.server.rule.RulesDefinition.NewRule; +import org.sonar.api.utils.Version; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.css.plugin.StylelintReport.Issue; +import org.sonar.css.plugin.StylelintReport.IssuesPerFile; + +public class StylelintReportSensor implements Sensor { + + private static final Logger LOG = Loggers.get(StylelintReportSensor.class); + + private static final String REPOSITORY = "stylelint"; + + private static final long DEFAULT_REMEDIATION_COST = 5L; + private static final Severity DEFAULT_SEVERITY = Severity.MAJOR; + private static final String FILE_EXCEPTION_MESSAGE = "No issues information will be saved as the report file can't be read."; + + private static final Set<String> BUG_RULES = new HashSet<>(Arrays.asList( + "selector-type-no-unknown", + "no-invalid-double-slash-comments", + "no-descending-specificity", + "at-rule-no-unknown", + "selector-type-no-unknown", + "selector-pseudo-element-no-unknown", + "selector-pseudo-class-no-unknown", + "declaration-block-no-shorthand-property-overrides", + "declaration-block-no-duplicate-properties", + "keyframe-declaration-no-important", + "property-no-unknown", + "unit-no-unknown", + "function-linear-gradient-no-nonstandard-direction", + "function-calc-no-unspaced-operator", + "font-family-no-missing-generic-family-keyword", + "color-no-invalid-hex" + )); + + private final CssRules cssRules; + + public StylelintReportSensor(CheckFactory checkFactory) { + cssRules = new CssRules(checkFactory); + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .onlyOnLanguage(CssLanguage.KEY) + .name("Import of stylelint issues"); + } + + @Override + public void execute(SensorContext context) { + boolean externalIssuesSupported = context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 2)); + String[] reportPaths = context.config().getStringArray(CssPlugin.STYLELINT_REPORT_PATHS); + + if (reportPaths.length == 0) { + return; + } + + if (!externalIssuesSupported) { + LOG.error("Import of external issues requires SonarQube 7.2 or greater."); + return; + } + + for (String reportPath : reportPaths) { + File report = getIOFile(context.fileSystem().baseDir(), reportPath); + importReport(report, context); + } + } + + private void importReport(File report, SensorContext context) { + LOG.info("Importing {}", report.getAbsoluteFile()); + + try (InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(report), StandardCharsets.UTF_8)) { + IssuesPerFile[] issues = new Gson().fromJson(inputStreamReader, IssuesPerFile[].class); + for (IssuesPerFile issuesPerFile : issues) { + InputFile inputFile = getInputFile(context, issuesPerFile.source); + if (inputFile != null) { + for (Issue issue : issuesPerFile.warnings) { + saveStylelintIssue(context, issue, inputFile); + } + } + } + } catch (IOException e) { + LOG.error(FILE_EXCEPTION_MESSAGE, e); + } catch (JsonSyntaxException e) { + LOG.error("Failed to parse json stylelint report", e); + } + } + + @Nullable + private static InputFile getInputFile(SensorContext context, String fileName) { + FilePredicates predicates = context.fileSystem().predicates(); + InputFile inputFile = context.fileSystem().inputFile(predicates.or(predicates.hasRelativePath(fileName), predicates.hasAbsolutePath(fileName))); + if (inputFile == null) { + LOG.warn("No input file found for {}. No stylelint issues will be imported on this file.", fileName); + return null; + } + return inputFile; + } + + private void saveStylelintIssue(SensorContext context, Issue issue, InputFile inputFile) { + String stylelintKey = issue.rule; + + RuleKey sonarKey = cssRules.getActiveSonarKey(stylelintKey); + if (sonarKey != null) { + String message = "Stylelint issue for rule '{}' is skipped because this rule is activated in your SonarQube profile for CSS (rule key in SQ {})"; + LOG.debug(message, stylelintKey, sonarKey.toString()); + return; + } + + NewExternalIssue newExternalIssue = context.newExternalIssue(); + + NewIssueLocation primaryLocation = newExternalIssue.newLocation() + .message(issue.text) + .on(inputFile) + .at(inputFile.selectLine(issue.line)); + + newExternalIssue + .at(primaryLocation) + .forRule(RuleKey.of(REPOSITORY, stylelintKey)) + .type(ruleType(stylelintKey)) + .severity(DEFAULT_SEVERITY) + .remediationEffortMinutes(DEFAULT_REMEDIATION_COST) + .save(); + } + + private static RuleType ruleType(String stylelintKey) { + return BUG_RULES.contains(stylelintKey) + ? RuleType.BUG + : RuleType.CODE_SMELL; + } + + /** + * Returns a java.io.File for the given path. + * If path is not absolute, returns a File with module base directory as parent path. + */ + private static File getIOFile(File baseDir, String path) { + File file = new File(path); + if (!file.isAbsolute()) { + file = new File(baseDir, path); + } + + return file; + } + + static void createExternalRuleRepository(Context context) { + NewRepository externalRepo = context.createExternalRepository(REPOSITORY, CssLanguage.KEY).setName(REPOSITORY); + String pathToRulesMeta = "org/sonar/l10n/css/rules/" + REPOSITORY + "/rules.json"; + String description = "See the description of %s rule <code>%s</code> at <a href=\"%s\">%s website</a>."; + + try (InputStreamReader inputStreamReader = new InputStreamReader(StylelintReportSensor.class.getClassLoader().getResourceAsStream(pathToRulesMeta), StandardCharsets.UTF_8)) { + ExternalRule[] rules = new Gson().fromJson(inputStreamReader, ExternalRule[].class); + for (ExternalRule rule : rules) { + NewRule newRule = externalRepo.createRule(rule.key).setName(rule.name); + newRule.setHtmlDescription(String.format(description, REPOSITORY, rule.key, rule.url, REPOSITORY)); + newRule.setDebtRemediationFunction(newRule.debtRemediationFunctions().constantPerIssue(DEFAULT_REMEDIATION_COST + "min")); + if (BUG_RULES.contains(rule.key)) { + newRule.setType(RuleType.BUG); + } + } + + } catch (IOException e) { + throw new IllegalStateException("Can't read resource: " + pathToRulesMeta, e); + } + + externalRepo.done(); + } + + private static class ExternalRule { + String url; + String key; + String name; + } +} diff --git a/sonar-css-plugin/src/main/resources/org/sonar/l10n/css/rules/stylelint/rules.json b/sonar-css-plugin/src/main/resources/org/sonar/l10n/css/rules/stylelint/rules.json new file mode 100644 index 0000000..4cd1ee0 --- /dev/null +++ b/sonar-css-plugin/src/main/resources/org/sonar/l10n/css/rules/stylelint/rules.json @@ -0,0 +1,852 @@ +[ + { + "key": "color-no-invalid-hex", + "url": "https://stylelint.io/user-guide/rules/color-no-invalid-hex/", + "name": "Disallow invalid hex colors" + }, + { + "key": "font-family-no-duplicate-names", + "url": "https://stylelint.io/user-guide/rules/font-family-no-duplicate-names/", + "name": "Disallow duplicate font family names" + }, + { + "key": "font-family-no-missing-generic-family-keyword", + "url": "https://stylelint.io/user-guide/rules/font-family-no-missing-generic-family-keyword/", + "name": "Disallow missing generic families in lists of font family names" + }, + { + "key": "function-calc-no-unspaced-operator", + "url": "https://stylelint.io/user-guide/rules/function-calc-no-unspaced-operator/", + "name": "Disallow an unspaced operator within `calc` functions" + }, + { + "key": "function-linear-gradient-no-nonstandard-direction", + "url": "https://stylelint.io/user-guide/rules/function-linear-gradient-no-nonstandard-direction/", + "name": "Disallow direction values in `linear-gradient()` calls that are not valid according to the [standard syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient#Syntax)" + }, + { + "key": "string-no-newline", + "url": "https://stylelint.io/user-guide/rules/string-no-newline/", + "name": "Disallow (unescaped) newlines in strings" + }, + { + "key": "unit-no-unknown", + "url": "https://stylelint.io/user-guide/rules/unit-no-unknown/", + "name": "Disallow unknown units" + }, + { + "key": "property-no-unknown", + "url": "https://stylelint.io/user-guide/rules/property-no-unknown/", + "name": "Disallow unknown properties" + }, + { + "key": "keyframe-declaration-no-important", + "url": "https://stylelint.io/user-guide/rules/keyframe-declaration-no-important/", + "name": "Disallow `!important` within keyframe declarations" + }, + { + "key": "declaration-block-no-duplicate-properties", + "url": "https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties/", + "name": "Disallow duplicate properties within declaration blocks" + }, + { + "key": "declaration-block-no-shorthand-property-overrides", + "url": "https://stylelint.io/user-guide/rules/declaration-block-no-shorthand-property-overrides/", + "name": "Disallow shorthand properties that override related longhand properties within declaration blocks" + }, + { + "key": "block-no-empty", + "url": "https://stylelint.io/user-guide/rules/block-no-empty/", + "name": "Disallow empty blocks" + }, + { + "key": "selector-pseudo-class-no-unknown", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-class-no-unknown/", + "name": "Disallow unknown pseudo-class selectors" + }, + { + "key": "selector-pseudo-element-no-unknown", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-element-no-unknown/", + "name": "Disallow unknown pseudo-element selectors" + }, + { + "key": "selector-type-no-unknown", + "url": "https://stylelint.io/user-guide/rules/selector-type-no-unknown/", + "name": "Disallow unknown type selectors" + }, + { + "key": "media-feature-name-no-unknown", + "url": "https://stylelint.io/user-guide/rules/media-feature-name-no-unknown/", + "name": "Disallow unknown media feature names" + }, + { + "key": "at-rule-no-unknown", + "url": "https://stylelint.io/user-guide/rules/at-rule-no-unknown/", + "name": "Disallow unknown at-rules" + }, + { + "key": "comment-no-empty", + "url": "https://stylelint.io/user-guide/rules/comment-no-empty/", + "name": " Disallow empty comments" + }, + { + "key": "no-descending-specificity", + "url": "https://stylelint.io/user-guide/rules/no-descending-specificity/", + "name": "Disallow selectors of lower specificity from coming after overriding selectors of higher specificity" + }, + { + "key": "no-duplicate-at-import-rules", + "url": "https://stylelint.io/user-guide/rules/no-duplicate-at-import-rules/", + "name": "Disallow duplicate `@import` rules within a stylesheet" + }, + { + "key": "no-duplicate-selectors", + "url": "https://stylelint.io/user-guide/rules/no-duplicate-selectors/", + "name": "Disallow duplicate selectors" + }, + { + "key": "no-empty-source", + "url": "https://stylelint.io/user-guide/rules/no-empty-source/", + "name": "Disallow empty sources" + }, + { + "key": "no-extra-semicolons", + "url": "https://stylelint.io/user-guide/rules/no-extra-semicolons/", + "name": "Disallow extra semicolons" + }, + { + "key": "no-invalid-double-slash-comments", + "url": "https://stylelint.io/user-guide/rules/no-invalid-double-slash-comments/", + "name": "Disallow double-slash comments (`//...`) which are not supported by CSS" + }, + { + "key": "color-named", + "url": "https://stylelint.io/user-guide/rules/color-named/", + "name": "Require (where possible) or disallow named colors" + }, + { + "key": "color-no-hex", + "url": "https://stylelint.io/user-guide/rules/color-no-hex/", + "name": "Disallow hex colors" + }, + { + "key": "function-blacklist", + "url": "https://stylelint.io/user-guide/rules/function-blacklist/", + "name": "Specify a blacklist of disallowed functions" + }, + { + "key": "function-url-no-scheme-relative", + "url": "https://stylelint.io/user-guide/rules/function-url-no-scheme-relative/", + "name": "Disallow scheme-relative urls" + }, + { + "key": "function-url-scheme-blacklist", + "url": "https://stylelint.io/user-guide/rules/function-url-scheme-blacklist/", + "name": "Specify a blacklist of disallowed url schemes" + }, + { + "key": "function-url-scheme-whitelist", + "url": "https://stylelint.io/user-guide/rules/function-url-scheme-whitelist/", + "name": "Specify a whitelist of allowed url schemes" + }, + { + "key": "function-whitelist", + "url": "https://stylelint.io/user-guide/rules/function-whitelist/", + "name": "Specify a whitelist of allowed functions" + }, + { + "key": "keyframes-name-pattern", + "url": "https://stylelint.io/user-guide/rules/keyframes-name-pattern/", + "name": "Specify a pattern for keyframe names" + }, + { + "key": "number-max-precision", + "url": "https://stylelint.io/user-guide/rules/number-max-precision/", + "name": "Limit the number of decimal places allowed in numbers" + }, + { + "key": "time-min-milliseconds", + "url": "https://stylelint.io/user-guide/rules/time-min-milliseconds/", + "name": "Specify the minimum number of milliseconds for time values" + }, + { + "key": "unit-blacklist", + "url": "https://stylelint.io/user-guide/rules/unit-blacklist/", + "name": "Specify a blacklist of disallowed units" + }, + { + "key": "unit-whitelist", + "url": "https://stylelint.io/user-guide/rules/unit-whitelist/", + "name": "Specify a whitelist of allowed units" + }, + { + "key": "shorthand-property-no-redundant-values", + "url": "https://stylelint.io/user-guide/rules/shorthand-property-no-redundant-values/", + "name": "Disallow redundant values in shorthand properties" + }, + { + "key": "value-no-vendor-prefix", + "url": "https://stylelint.io/user-guide/rules/value-no-vendor-prefix/", + "name": "Disallow vendor prefixes for values" + }, + { + "key": "custom-property-pattern", + "url": "https://stylelint.io/user-guide/rules/custom-property-pattern/", + "name": "Specify a pattern for custom properties" + }, + { + "key": "property-blacklist", + "url": "https://stylelint.io/user-guide/rules/property-blacklist/", + "name": "Specify a blacklist of disallowed properties" + }, + { + "key": "property-no-vendor-prefix", + "url": "https://stylelint.io/user-guide/rules/property-no-vendor-prefix/", + "name": "Disallow vendor prefixes for properties" + }, + { + "key": "property-whitelist", + "url": "https://stylelint.io/user-guide/rules/property-whitelist/", + "name": "Specify a whitelist of allowed properties" + }, + { + "key": "declaration-block-no-redundant-longhand-properties", + "url": "https://stylelint.io/user-guide/rules/declaration-block-no-redundant-longhand-properties/", + "name": "Disallow longhand properties that can be combined into one shorthand property" + }, + { + "key": "declaration-no-important", + "url": "https://stylelint.io/user-guide/rules/declaration-no-important/", + "name": "Disallow `!important` within declarations" + }, + { + "key": "declaration-property-unit-blacklist", + "url": "https://stylelint.io/user-guide/rules/declaration-property-unit-blacklist/", + "name": "Specify a blacklist of disallowed property and unit pairs within declarations" + }, + { + "key": "declaration-property-unit-whitelist", + "url": "https://stylelint.io/user-guide/rules/declaration-property-unit-whitelist/", + "name": "Specify a whitelist of allowed property and unit pairs within declarations" + }, + { + "key": "declaration-property-value-blacklist", + "url": "https://stylelint.io/user-guide/rules/declaration-property-value-blacklist/", + "name": "Specify a blacklist of disallowed property and value pairs within declarations" + }, + { + "key": "declaration-property-value-whitelist", + "url": "https://stylelint.io/user-guide/rules/declaration-property-value-whitelist/", + "name": "Specify a whitelist of allowed property and value pairs within declarations" + }, + { + "key": "declaration-block-single-line-max-declarations", + "url": "https://stylelint.io/user-guide/rules/declaration-block-single-line-max-declarations/", + "name": "Limit the number of declaration within single line declaration blocks" + }, + { + "key": "selector-attribute-operator-blacklist", + "url": "https://stylelint.io/user-guide/rules/selector-attribute-operator-blacklist/", + "name": "Specify a blacklist of disallowed attribute operators" + }, + { + "key": "selector-attribute-operator-whitelist", + "url": "https://stylelint.io/user-guide/rules/selector-attribute-operator-whitelist/", + "name": "Specify a whitelist of allowed attribute operators" + }, + { + "key": "selector-class-pattern", + "url": "https://stylelint.io/user-guide/rules/selector-class-pattern/", + "name": "Specify a pattern for class selectors" + }, + { + "key": "selector-combinator-blacklist", + "url": "https://stylelint.io/user-guide/rules/selector-combinator-blacklist/", + "name": "Specify a blacklist of disallowed combinators" + }, + { + "key": "selector-combinator-whitelist", + "url": "https://stylelint.io/user-guide/rules/selector-combinator-whitelist/", + "name": "Specify a whitelist of allowed combinators" + }, + { + "key": "selector-id-pattern", + "url": "https://stylelint.io/user-guide/rules/selector-id-pattern/", + "name": "Specify a pattern for id selectors" + }, + { + "key": "selector-max-attribute", + "url": "https://stylelint.io/user-guide/rules/selector-max-attribute/", + "name": "Limit the number of attribute selectors in a selector" + }, + { + "key": "selector-max-class", + "url": "https://stylelint.io/user-guide/rules/selector-max-class/", + "name": "Limit the number of classes in a selector" + }, + { + "key": "selector-max-combinators", + "url": "https://stylelint.io/user-guide/rules/selector-max-combinators/", + "name": "Limit the number of combinators in a selector" + }, + { + "key": "selector-max-compound-selectors", + "url": "https://stylelint.io/user-guide/rules/selector-max-compound-selectors/", + "name": "Limit the number of compound selectors in a selector" + }, + { + "key": "selector-max-empty-lines", + "url": "https://stylelint.io/user-guide/rules/selector-max-empty-lines/", + "name": "Limit the number of adjacent empty lines within selectors" + }, + { + "key": "selector-max-id", + "url": "https://stylelint.io/user-guide/rules/selector-max-id/", + "name": "Limit the number of id selectors in a selector" + }, + { + "key": "selector-max-pseudo-class", + "url": "https://stylelint.io/user-guide/rules/selector-max-pseudo-class/", + "name": "Limit the number of pseudo-classes in a selector" + }, + { + "key": "selector-max-specificity", + "url": "https://stylelint.io/user-guide/rules/selector-max-specificity/", + "name": "Limit the specificity of selectors" + }, + { + "key": "selector-max-type", + "url": "https://stylelint.io/user-guide/rules/selector-max-type/", + "name": "Limit the number of type in a selector" + }, + { + "key": "selector-max-universal", + "url": "https://stylelint.io/user-guide/rules/selector-max-universal/", + "name": "Limit the number of universal selectors in a selector" + }, + { + "key": "selector-nested-pattern", + "url": "https://stylelint.io/user-guide/rules/selector-nested-pattern/", + "name": "Specify a pattern for the selectors of rules nested within rules" + }, + { + "key": "selector-no-qualifying-type", + "url": "https://stylelint.io/user-guide/rules/selector-no-qualifying-type/", + "name": "Disallow qualifying a selector by type" + }, + { + "key": "selector-no-vendor-prefix", + "url": "https://stylelint.io/user-guide/rules/selector-no-vendor-prefix/", + "name": "Disallow vendor prefixes for selectors" + }, + { + "key": "selector-pseudo-class-blacklist", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-class-blacklist/", + "name": "Specify a blacklist of disallowed pseudo-class selectors" + }, + { + "key": "selector-pseudo-class-whitelist", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-class-whitelist/", + "name": "Specify a whitelist of allowed pseudo-class selectors" + }, + { + "key": "selector-pseudo-element-blacklist", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-element-blacklist/", + "name": "Specify a blacklist of disallowed pseudo-element selectors" + }, + { + "key": "selector-pseudo-element-whitelist", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-element-whitelist/", + "name": "Specify a whitelist of allowed pseudo-element selectors" + }, + { + "key": "media-feature-name-blacklist", + "url": "https://stylelint.io/user-guide/rules/media-feature-name-blacklist/", + "name": "Specify a blacklist of disallowed media feature names" + }, + { + "key": "media-feature-name-no-vendor-prefix", + "url": "https://stylelint.io/user-guide/rules/media-feature-name-no-vendor-prefix/", + "name": "Disallow vendor prefixes for media feature names" + }, + { + "key": "media-feature-name-value-whitelist", + "url": "https://stylelint.io/user-guide/rules/media-feature-name-value-whitelist/", + "name": "Specify a whitelist of allowed media feature name and value pairs" + }, + { + "key": "media-feature-name-whitelist", + "url": "https://stylelint.io/user-guide/rules/media-feature-name-whitelist/", + "name": "Specify a whitelist of allowed media feature names" + }, + { + "key": "custom-media-pattern", + "url": "https://stylelint.io/user-guide/rules/custom-media-pattern/", + "name": "Specify a pattern for custom media query names" + }, + { + "key": "at-rule-blacklist", + "url": "https://stylelint.io/user-guide/rules/at-rule-blacklist/", + "name": "Specify a blacklist of disallowed at-rules" + }, + { + "key": "at-rule-no-vendor-prefix", + "url": "https://stylelint.io/user-guide/rules/at-rule-no-vendor-prefix/", + "name": "Disallow vendor prefixes for at-rules" + }, + { + "key": "at-rule-whitelist", + "url": "https://stylelint.io/user-guide/rules/at-rule-whitelist/", + "name": "Specify a whitelist of allowed at-rules" + }, + { + "key": "comment-word-blacklist", + "url": "https://stylelint.io/user-guide/rules/comment-word-blacklist/", + "name": "Specify a blacklist of disallowed words within comments" + }, + { + "key": "max-nesting-depth", + "url": "https://stylelint.io/user-guide/rules/max-nesting-depth/", + "name": "Limit the depth of nesting" + }, + { + "key": "no-unknown-animations", + "url": "https://stylelint.io/user-guide/rules/no-unknown-animations/", + "name": "Disallow unknown animations" + }, + { + "key": "color-hex-case", + "url": "https://stylelint.io/user-guide/rules/color-hex-case/", + "name": "Specify lowercase or uppercase for hex colors" + }, + { + "key": "color-hex-length", + "url": "https://stylelint.io/user-guide/rules/color-hex-length/", + "name": "Specify short or long notation for hex colors" + }, + { + "key": "font-family-name-quotes", + "url": "https://stylelint.io/user-guide/rules/font-family-name-quotes/", + "name": "Specify whether or not quotation marks should be used around font family names" + }, + { + "key": "font-weight-notation", + "url": "https://stylelint.io/user-guide/rules/font-weight-notation/", + "name": "Require numeric or named (where possible) `font-weight` values" + }, + { + "key": "function-comma-newline-after", + "url": "https://stylelint.io/user-guide/rules/function-comma-newline-after/", + "name": "Require a newline or disallow whitespace after the commas of functions" + }, + { + "key": "function-comma-newline-before", + "url": "https://stylelint.io/user-guide/rules/function-comma-newline-before/", + "name": "Require a newline or disallow whitespace before the commas of functions" + }, + { + "key": "function-comma-space-after", + "url": "https://stylelint.io/user-guide/rules/function-comma-space-after/", + "name": "Require a single space or disallow whitespace after the commas of functions" + }, + { + "key": "function-comma-space-before", + "url": "https://stylelint.io/user-guide/rules/function-comma-space-before/", + "name": "Require a single space or disallow whitespace before the commas of functions" + }, + { + "key": "function-max-empty-lines", + "url": "https://stylelint.io/user-guide/rules/function-max-empty-lines/", + "name": "Limit the number of adjacent empty lines within functions" + }, + { + "key": "function-name-case", + "url": "https://stylelint.io/user-guide/rules/function-name-case/", + "name": "Specify lowercase or uppercase for function names" + }, + { + "key": "function-parentheses-newline-inside", + "url": "https://stylelint.io/user-guide/rules/function-parentheses-newline-inside/", + "name": "Require a newline or disallow whitespace on the inside of the parentheses of functions" + }, + { + "key": "function-parentheses-space-inside", + "url": "https://stylelint.io/user-guide/rules/function-parentheses-space-inside/", + "name": "Require a single space or disallow whitespace on the inside of the parentheses of functions" + }, + { + "key": "function-url-quotes", + "url": "https://stylelint.io/user-guide/rules/function-url-quotes/", + "name": "Require or disallow quotes for urls" + }, + { + "key": "function-whitespace-after", + "url": "https://stylelint.io/user-guide/rules/function-whitespace-after/", + "name": "Require or disallow whitespace after functions" + }, + { + "key": "number-leading-zero", + "url": "https://stylelint.io/user-guide/rules/number-leading-zero/", + "name": "Require or disallow a leading zero for fractional numbers less than 1" + }, + { + "key": "number-no-trailing-zeros", + "url": "https://stylelint.io/user-guide/rules/number-no-trailing-zeros/", + "name": "Disallow trailing zeros in numbers" + }, + { + "key": "string-quotes", + "url": "https://stylelint.io/user-guide/rules/string-quotes/", + "name": "Specify single or double quotes around strings" + }, + { + "key": "length-zero-no-unit", + "url": "https://stylelint.io/user-guide/rules/length-zero-no-unit/", + "name": "Disallow units for zero lengths" + }, + { + "key": "unit-case", + "url": "https://stylelint.io/user-guide/rules/unit-case/", + "name": "Specify lowercase or uppercase for units" + }, + { + "key": "value-keyword-case", + "url": "https://stylelint.io/user-guide/rules/value-keyword-case/", + "name": "Specify lowercase or uppercase for keywords values" + }, + { + "key": "value-list-comma-newline-after", + "url": "https://stylelint.io/user-guide/rules/value-list-comma-newline-after/", + "name": "Require a newline or disallow whitespace after the commas of value lists" + }, + { + "key": "value-list-comma-newline-before", + "url": "https://stylelint.io/user-guide/rules/value-list-comma-newline-before/", + "name": "Require a newline or disallow whitespace before the commas of value lists" + }, + { + "key": "value-list-comma-space-after", + "url": "https://stylelint.io/user-guide/rules/value-list-comma-space-after/", + "name": "Require a single space or disallow whitespace after the commas of value lists" + }, + { + "key": "value-list-comma-space-before", + "url": "https://stylelint.io/user-guide/rules/value-list-comma-space-before/", + "name": "Require a single space or disallow whitespace before the commas of value lists" + }, + { + "key": "value-list-max-empty-lines", + "url": "https://stylelint.io/user-guide/rules/value-list-max-empty-lines/", + "name": "Limit the number of adjacent empty lines within value lists" + }, + { + "key": "custom-property-empty-line-before", + "url": "https://stylelint.io/user-guide/rules/custom-property-empty-line-before/", + "name": "Require or disallow an empty line before custom properties" + }, + { + "key": "property-case", + "url": "https://stylelint.io/user-guide/rules/property-case/", + "name": "Specify lowercase or uppercase for properties" + }, + { + "key": "declaration-bang-space-after", + "url": "https://stylelint.io/user-guide/rules/declaration-bang-space-after/", + "name": "Require a single space or disallow whitespace after the bang of declarations" + }, + { + "key": "declaration-bang-space-before", + "url": "https://stylelint.io/user-guide/rules/declaration-bang-space-before/", + "name": "Require a single space or disallow whitespace before the bang of declarations" + }, + { + "key": "declaration-colon-newline-after", + "url": "https://stylelint.io/user-guide/rules/declaration-colon-newline-after/", + "name": "Require a newline or disallow whitespace after the colon of declarations" + }, + { + "key": "declaration-colon-space-after", + "url": "https://stylelint.io/user-guide/rules/declaration-colon-space-after/", + "name": "Require a single space or disallow whitespace after the colon of declarations" + }, + { + "key": "declaration-colon-space-before", + "url": "https://stylelint.io/user-guide/rules/declaration-colon-space-before/", + "name": "Require a single space or disallow whitespace before the colon of declarations" + }, + { + "key": "declaration-empty-line-before", + "url": "https://stylelint.io/user-guide/rules/declaration-empty-line-before/", + "name": "Require or disallow an empty line before declarations" + }, + { + "key": "declaration-block-semicolon-newline-after", + "url": "https://stylelint.io/user-guide/rules/declaration-block-semicolon-newline-after/", + "name": "Require a newline or disallow whitespace after the semicolons of declaration blocks" + }, + { + "key": "declaration-block-semicolon-newline-before", + "url": "https://stylelint.io/user-guide/rules/declaration-block-semicolon-newline-before/", + "name": "Require a newline or disallow whitespace before the semicolons of declaration blocks" + }, + { + "key": "declaration-block-semicolon-space-after", + "url": "https://stylelint.io/user-guide/rules/declaration-block-semicolon-space-after/", + "name": "Require a single space or disallow whitespace after the semicolons of declaration blocks" + }, + { + "key": "declaration-block-semicolon-space-before", + "url": "https://stylelint.io/user-guide/rules/declaration-block-semicolon-space-before/", + "name": "Require a single space or disallow whitespace before the semicolons of declaration blocks" + }, + { + "key": "declaration-block-trailing-semicolon", + "url": "https://stylelint.io/user-guide/rules/declaration-block-trailing-semicolon/", + "name": "Require or disallow a trailing semicolon within declaration blocks" + }, + { + "key": "block-closing-brace-empty-line-before", + "url": "https://stylelint.io/user-guide/rules/block-closing-brace-empty-line-before/", + "name": "Require or disallow an empty line before the closing brace of blocks" + }, + { + "key": "block-closing-brace-newline-after", + "url": "https://stylelint.io/user-guide/rules/block-closing-brace-newline-after/", + "name": "Require a newline or disallow whitespace after the closing brace of blocks" + }, + { + "key": "block-closing-brace-newline-before", + "url": "https://stylelint.io/user-guide/rules/block-closing-brace-newline-before/", + "name": "Require a newline or disallow whitespace before the closing brace of blocks" + }, + { + "key": "block-closing-brace-space-after", + "url": "https://stylelint.io/user-guide/rules/block-closing-brace-space-after/", + "name": "Require a single space or disallow whitespace after the closing brace of blocks" + }, + { + "key": "block-closing-brace-space-before", + "url": "https://stylelint.io/user-guide/rules/block-closing-brace-space-before/", + "name": "Require a single space or disallow whitespace before the closing brace of blocks" + }, + { + "key": "block-opening-brace-newline-after", + "url": "https://stylelint.io/user-guide/rules/block-opening-brace-newline-after/", + "name": "Require a newline after the opening brace of blocks" + }, + { + "key": "block-opening-brace-newline-before", + "url": "https://stylelint.io/user-guide/rules/block-opening-brace-newline-before/", + "name": "Require a newline or disallow whitespace before the opening brace of blocks" + }, + { + "key": "block-opening-brace-space-after", + "url": "https://stylelint.io/user-guide/rules/block-opening-brace-space-after/", + "name": "Require a single space or disallow whitespace after the opening brace of blocks" + }, + { + "key": "block-opening-brace-space-before", + "url": "https://stylelint.io/user-guide/rules/block-opening-brace-space-before/", + "name": "Require a single space or disallow whitespace before the opening brace of blocks" + }, + { + "key": "selector-attribute-brackets-space-inside", + "url": "https://stylelint.io/user-guide/rules/selector-attribute-brackets-space-inside/", + "name": "Require a single space or disallow whitespace on the inside of the brackets within attribute selectors" + }, + { + "key": "selector-attribute-operator-space-after", + "url": "https://stylelint.io/user-guide/rules/selector-attribute-operator-space-after/", + "name": "Require a single space or disallow whitespace after operators within attribute selectors" + }, + { + "key": "selector-attribute-operator-space-before", + "url": "https://stylelint.io/user-guide/rules/selector-attribute-operator-space-before/", + "name": "Require a single space or disallow whitespace before operators within attribute selectors" + }, + { + "key": "selector-attribute-quotes", + "url": "https://stylelint.io/user-guide/rules/selector-attribute-quotes/", + "name": "Require or disallow quotes for attribute values" + }, + { + "key": "selector-combinator-space-after", + "url": "https://stylelint.io/user-guide/rules/selector-combinator-space-after/", + "name": "Require a single space or disallow whitespace after the combinators of selectors" + }, + { + "key": "selector-combinator-space-before", + "url": "https://stylelint.io/user-guide/rules/selector-combinator-space-before/", + "name": "Require a single space or disallow whitespace before the combinators of selectors" + }, + { + "key": "selector-descendant-combinator-no-non-space", + "url": "https://stylelint.io/user-guide/rules/selector-descendant-combinator-no-non-space/", + "name": "Disallow non-space characters for descendant combinators of selectors" + }, + { + "key": "selector-pseudo-class-case", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-class-case/", + "name": "Specify lowercase or uppercase for pseudo-class selectors" + }, + { + "key": "selector-pseudo-class-parentheses-space-inside", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-class-parentheses-space-inside/", + "name": "Require a single space or disallow whitespace on the inside of the parentheses within pseudo-class selectors" + }, + { + "key": "selector-pseudo-element-case", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-element-case/", + "name": "Specify lowercase or uppercase for pseudo-element selectors" + }, + { + "key": "selector-pseudo-element-colon-notation", + "url": "https://stylelint.io/user-guide/rules/selector-pseudo-element-colon-notation/", + "name": "Specify single or double colon notation for applicable pseudo-elements" + }, + { + "key": "selector-type-case", + "url": "https://stylelint.io/user-guide/rules/selector-type-case/", + "name": "Specify lowercase or uppercase for type selector" + }, + { + "key": "selector-list-comma-newline-after", + "url": "https://stylelint.io/user-guide/rules/selector-list-comma-newline-after/", + "name": "Require a newline or disallow whitespace after the commas of selector lists" + }, + { + "key": "selector-list-comma-newline-before", + "url": "https://stylelint.io/user-guide/rules/selector-list-comma-newline-before/", + "name": "Require a newline or disallow whitespace before the commas of selector lists" + }, + { + "key": "selector-list-comma-space-after", + "url": "https://stylelint.io/user-guide/rules/selector-list-comma-space-after/", + "name": "Require a single space or disallow whitespace after the commas of selector lists" + }, + { + "key": "selector-list-comma-space-before", + "url": "https://stylelint.io/user-guide/rules/selector-list-comma-space-before/", + "name": "Require a single space or disallow whitespace before the commas of selector lists" + }, + { + "key": "rule-empty-line-before", + "url": "https://stylelint.io/user-guide/rules/rule-empty-line-before/", + "name": "Require or disallow an empty line before rules" + }, + { + "key": "media-feature-colon-space-after", + "url": "https://stylelint.io/user-guide/rules/media-feature-colon-space-after/", + "name": "Require a single space or disallow whitespace after the colon in media features" + }, + { + "key": "media-feature-colon-space-before", + "url": "https://stylelint.io/user-guide/rules/media-feature-colon-space-before/", + "name": "Require a single space or disallow whitespace before the colon in media features" + }, + { + "key": "media-feature-name-case", + "url": "https://stylelint.io/user-guide/rules/media-feature-name-case/", + "name": "Specify lowercase or uppercase for media feature names" + }, + { + "key": "media-feature-parentheses-space-inside", + "url": "https://stylelint.io/user-guide/rules/media-feature-parentheses-space-inside/", + "name": "Require a single space or disallow whitespace on the inside of the parentheses within media features" + }, + { + "key": "media-feature-range-operator-space-after", + "url": "https://stylelint.io/user-guide/rules/media-feature-range-operator-space-after/", + "name": "Require a single space or disallow whitespace after the range operator in media features" + }, + { + "key": "media-feature-range-operator-space-before", + "url": "https://stylelint.io/user-guide/rules/media-feature-range-operator-space-before/", + "name": "Require a single space or disallow whitespace before the range operator in media features" + }, + { + "key": "media-query-list-comma-newline-after", + "url": "https://stylelint.io/user-guide/rules/media-query-list-comma-newline-after/", + "name": "Require a newline or disallow whitespace after the commas of media query lists" + }, + { + "key": "media-query-list-comma-newline-before", + "url": "https://stylelint.io/user-guide/rules/media-query-list-comma-newline-before/", + "name": "Require a newline or disallow whitespace before the commas of media query lists" + }, + { + "key": "media-query-list-comma-space-after", + "url": "https://stylelint.io/user-guide/rules/media-query-list-comma-space-after/", + "name": "Require a single space or disallow whitespace after the commas of media query lists" + }, + { + "key": "media-query-list-comma-space-before", + "url": "https://stylelint.io/user-guide/rules/media-query-list-comma-space-before/", + "name": "Require a single space or disallow whitespace before the commas of media query lists" + }, + { + "key": "at-rule-empty-line-before", + "url": "https://stylelint.io/user-guide/rules/at-rule-empty-line-before/", + "name": "Require or disallow an empty line before at-rules" + }, + { + "key": "at-rule-name-case", + "url": "https://stylelint.io/user-guide/rules/at-rule-name-case/", + "name": "Specify lowercase or uppercase for at-rules names" + }, + { + "key": "at-rule-name-newline-after", + "url": "https://stylelint.io/user-guide/rules/at-rule-name-newline-after/", + "name": "Require a newline after at-rule names" + }, + { + "key": "at-rule-name-space-after", + "url": "https://stylelint.io/user-guide/rules/at-rule-name-space-after/", + "name": "Require a single space after at-rule names" + }, + { + "key": "at-rule-semicolon-newline-after", + "url": "https://stylelint.io/user-guide/rules/at-rule-semicolon-newline-after/", + "name": "Require a newline after the semicolon of at-rules" + }, + { + "key": "at-rule-semicolon-space-before", + "url": "https://stylelint.io/user-guide/rules/at-rule-semicolon-space-before/", + "name": "Require a single space or disallow whitespace before the semicolons of at rules" + }, + { + "key": "comment-empty-line-before", + "url": "https://stylelint.io/user-guide/rules/comment-empty-line-before/", + "name": "Require or disallow an empty line before comments" + }, + { + "key": "comment-whitespace-inside", + "url": "https://stylelint.io/user-guide/rules/comment-whitespace-inside/", + "name": "Require or disallow whitespace on the inside of comment markers" + }, + { + "key": "indentation", + "url": "https://stylelint.io/user-guide/rules/indentation/", + "name": "Specify indentation" + }, + { + "key": "max-empty-lines", + "url": "https://stylelint.io/user-guide/rules/max-empty-lines/", + "name": "Limit the number of adjacent empty lines" + }, + { + "key": "max-line-length", + "url": "https://stylelint.io/user-guide/rules/max-line-length/", + "name": "Limit the length of a line" + }, + { + "key": "no-eol-whitespace", + "url": "https://stylelint.io/user-guide/rules/no-eol-whitespace/", + "name": "Disallow end-of-line whitespace" + }, + { + "key": "no-missing-end-of-source-newline", + "url": "https://stylelint.io/user-guide/rules/no-missing-end-of-source-newline/", + "name": "Disallow missing end-of-source newlines" + } +] diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java index 1cbade9..c5d9bbd 100644 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java @@ -36,6 +36,6 @@ public class CssPluginTest { Plugin.Context context = new Plugin.Context(runtime); Plugin underTest = new CssPlugin(); underTest.define(context); - assertThat(context.getExtensions()).hasSize(8); + assertThat(context.getExtensions()).hasSize(9); } } diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java index f86e24f..471c4ef 100644 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java @@ -60,7 +60,6 @@ public class CssRuleSensorTest { sensor.describe(sensorDescriptor); assertThat(sensorDescriptor.name()).isEqualTo("SonarCSS Rules"); assertThat(sensorDescriptor.languages()).containsOnly("css"); - assertThat(sensorDescriptor.type()).isEqualTo(Type.MAIN); } @Test diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java index 7dffb35..167aeef 100644 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java @@ -27,14 +27,35 @@ import static org.assertj.core.api.Assertions.assertThat; public class CssRulesDefinitionTest { @Test - public void test() { - CssRulesDefinition rulesDefinition = new CssRulesDefinition(); + public void test_with_external_rules() { + CssRulesDefinition rulesDefinition = new CssRulesDefinition(true); + RulesDefinition.Context context = new RulesDefinition.Context(); + rulesDefinition.define(context); + + assertThat(context.repositories()).hasSize(2); + RulesDefinition.Repository mainRepository = context.repository("css"); + RulesDefinition.Repository externalRepository = context.repository("external_stylelint"); + + assertThat(externalRepository.name()).isEqualTo("stylelint"); + assertThat(externalRepository.language()).isEqualTo("css"); + assertThat(externalRepository.isExternal()).isEqualTo(true); + assertThat(externalRepository.rules()).hasSize(170); + + assertThat(mainRepository.name()).isEqualTo("SonarAnalyzer"); + assertThat(mainRepository.language()).isEqualTo("css"); + assertThat(mainRepository.isExternal()).isEqualTo(false); + assertThat(mainRepository.rules()).hasSize(CssRules.getRuleClasses().size()); + } + + @Test + public void test_no_external_rules() throws Exception { + CssRulesDefinition rulesDefinition = new CssRulesDefinition(false); RulesDefinition.Context context = new RulesDefinition.Context(); rulesDefinition.define(context); - RulesDefinition.Repository repository = context.repository("css"); assertThat(context.repositories()).hasSize(1); + RulesDefinition.Repository repository = context.repository("css"); assertThat(repository.name()).isEqualTo("SonarAnalyzer"); assertThat(repository.language()).isEqualTo("css"); assertThat(repository.isExternal()).isEqualTo(false); diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintReportSensorTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintReportSensorTest.java new file mode 100644 index 0000000..bbbc8d8 --- /dev/null +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintReportSensorTest.java @@ -0,0 +1,212 @@ +/* + * SonarCSS + * Copyright (C) 2018-2018 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.io.FileWriter; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.fs.InputFile.Type; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; +import org.sonar.api.batch.rule.CheckFactory; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.batch.sensor.issue.ExternalIssue; +import org.sonar.api.internal.SonarRuntimeImpl; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.Version; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StylelintReportSensorTest { + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + + @Rule + public final LogTester logTester = new LogTester(); + + private static final File BASE_DIR = new File("src/test/resources/stylelint-report/").getAbsoluteFile(); + private static final String CONTENT = ".foo {\n}"; + + private SensorContextTester context = SensorContextTester.create(BASE_DIR); + private static final CheckFactory EMPTY_CHECK_FACTORY = new CheckFactory(new TestActiveRules()); + private static final CheckFactory CHECK_FACTORY_WITH_RULE = new CheckFactory(new TestActiveRules("S4647")); + + private StylelintReportSensor stylelintReportSensor = new StylelintReportSensor(EMPTY_CHECK_FACTORY); + private DefaultInputFile inputFile = createInputFile(context, CONTENT, "file.css"); + + @Before + public void setUp() throws Exception { + context.setRuntime(getRuntime(7, 2)); + context.fileSystem().add(inputFile); + } + + @Test + public void should_add_issues_from_report() throws Exception { + setReport("report.json"); + stylelintReportSensor.execute(context); + + Collection<ExternalIssue> externalIssues = context.allExternalIssues(); + assertThat(externalIssues).hasSize(2); + Iterator<ExternalIssue> iterator = externalIssues.iterator(); + ExternalIssue first = iterator.next(); + ExternalIssue second = iterator.next(); + + assertThat(first.type()).isEqualTo(RuleType.BUG); + assertThat(second.type()).isEqualTo(RuleType.CODE_SMELL); + + assertThat(first.remediationEffort()).isEqualTo(5); + assertThat(first.severity()).isEqualTo(Severity.MAJOR); + assertThat(first.primaryLocation().message()).isEqualTo("external issue message (color-no-invalid-hex)"); + assertThat(first.primaryLocation().textRange().start().line()).isEqualTo(1); + } + + @Test + public void should_support_absolute_file_paths_in_report() throws Exception { + String report = "[\n" + + " {\n" + + " \"source\": \"%s\",\n" + + " \"warnings\": [\n" + + " {\n" + + " \"line\": 1,\n" + + " \"rule\": \"color\\-no\\-invalid-hex\",\n" + + " \"text\": \"external issue message\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n"; + + File reportFile = tmpDir.newFile(); + FileWriter writer = new FileWriter(reportFile); + writer.write(String.format(report, inputFile.absolutePath())); + writer.close(); + + setReport(reportFile.getAbsolutePath()); + stylelintReportSensor.execute(context); + + assertThat(context.allExternalIssues()).hasSize(1); + } + + @Test + public void should_skip_duplicates() throws Exception { + // when in SQ CSS profile there is an activated rule matching to an external issue, + // that external issue is ignored + setReport("report.json"); + new StylelintReportSensor(CHECK_FACTORY_WITH_RULE).execute(context); + + Collection<ExternalIssue> externalIssues = context.allExternalIssues(); + assertThat(externalIssues).hasSize(1); + assertThat(externalIssues.iterator().next().primaryLocation().message()).isEqualTo("external issue message (comment-no-empty)"); + + assertThat(logTester.logs(LoggerLevel.DEBUG)) + .contains("Stylelint issue for rule 'color-no-invalid-hex' is skipped because this rule is activated in your SonarQube profile for CSS (rule key in SQ css:S4647)"); + } + + @Test + public void should_ignore_report_on_older_sonarqube() throws Exception { + context.setRuntime(getRuntime(7, 1)); + setReport("report.json"); + stylelintReportSensor.execute(context); + + assertThat(context.allExternalIssues()).isEmpty(); + assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Import of external issues requires SonarQube 7.2 or greater."); + } + + @Test + public void should_do_nothing_when_no_report() throws Exception { + setReport(""); + stylelintReportSensor.execute(context); + + assertThat(context.allExternalIssues()).isEmpty(); + } + + @Test + public void should_log_when_not_existing_report_file() throws Exception { + setReport("not-exist.json"); + stylelintReportSensor.execute(context); + + assertThat(context.allExternalIssues()).isEmpty(); + assertThat(logTester.logs(LoggerLevel.ERROR)).contains("No issues information will be saved as the report file can't be read."); + } + + @Test + public void should_log_when_not_found_input_file() throws Exception { + setReport("invalid-file.json"); + stylelintReportSensor.execute(context); + + assertThat(context.allExternalIssues()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("No input file found for not-exist.css. No stylelint issues will be imported on this file."); + } + + @Test + public void should_accept_absolute_path_to_report() throws Exception { + setReport(new File(BASE_DIR, "report.json").getAbsolutePath()); + stylelintReportSensor.execute(context); + assertThat(context.allExternalIssues()).hasSize(2); + } + + @Test + public void should_accept_several_reports() throws Exception { + setReport("report.json, invalid-file.json"); + stylelintReportSensor.execute(context); + assertThat(context.allExternalIssues()).hasSize(3); + } + + @Test + public void test_descriptor() throws Exception { + DefaultSensorDescriptor sensorDescriptor = new DefaultSensorDescriptor(); + stylelintReportSensor.describe(sensorDescriptor); + assertThat(sensorDescriptor.name()).isEqualTo("Import of stylelint issues"); + assertThat(sensorDescriptor.languages()).containsOnly(CssLanguage.KEY); + } + + private void setReport(String reportFileName) { + context.settings().setProperty(CssPlugin.STYLELINT_REPORT_PATHS, reportFileName); + } + + private SonarRuntime getRuntime(int major, int minor) { + return SonarRuntimeImpl.forSonarQube(Version.create(major, minor), SonarQubeSide.SERVER); + } + + private static DefaultInputFile createInputFile(SensorContextTester sensorContext, String content, String relativePath) { + DefaultInputFile testInputFile = new TestInputFileBuilder("moduleKey", relativePath) + .setModuleBaseDir(sensorContext.fileSystem().baseDirPath()) + .setType(Type.MAIN) + .setLanguage(CssLanguage.KEY) + .setCharset(StandardCharsets.UTF_8) + .setContents(content) + .build(); + + sensorContext.fileSystem().add(testInputFile); + return testInputFile; + } +} diff --git a/sonar-css-plugin/src/test/resources/stylelint-report/file.css b/sonar-css-plugin/src/test/resources/stylelint-report/file.css new file mode 100644 index 0000000..59aa5dd --- /dev/null +++ b/sonar-css-plugin/src/test/resources/stylelint-report/file.css @@ -0,0 +1 @@ +File content is set in test file diff --git a/sonar-css-plugin/src/test/resources/stylelint-report/invalid-file.json b/sonar-css-plugin/src/test/resources/stylelint-report/invalid-file.json new file mode 100644 index 0000000..0dc724c --- /dev/null +++ b/sonar-css-plugin/src/test/resources/stylelint-report/invalid-file.json @@ -0,0 +1,22 @@ +[ + { + "source": "not-exist.css", + "warnings": [ + { + "line": 1, + "rule": "color-no-invalid-hex", + "text": "external issue message" + } + ] + }, + { + "source": "file.css", + "warnings": [ + { + "line": 1, + "rule": "comment-no-empty", + "text": "external issue message (comment-no-empty)" + } + ] + } +] diff --git a/sonar-css-plugin/src/test/resources/stylelint-report/report.json b/sonar-css-plugin/src/test/resources/stylelint-report/report.json new file mode 100644 index 0000000..6e758b3 --- /dev/null +++ b/sonar-css-plugin/src/test/resources/stylelint-report/report.json @@ -0,0 +1,17 @@ +[ + { + "source": "file.css", + "warnings": [ + { + "line": 1, + "rule": "color-no-invalid-hex", + "text": "external issue message (color-no-invalid-hex)" + }, + { + "line": 1, + "rule": "comment-no-empty", + "text": "external issue message (comment-no-empty)" + } + ] + } +] |
