Repository: stylelint-scss/stylelint-scss
Pull request #846 (2023-08-22 17:20:02)

Add at-root-no-redundant rule
Fix (#601).

pamelalozano16: @kristerkari please let us know your thoughts :)
kristerkari: Thanks! I'll need to go through all the new PRs 👍 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e07e624..c7e2063 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+# 5.2.0
+
+- Added: `property-no-unknown` rule to disallow unknown properties. (#490).
+- Added: `at-use-no-redundant-alias` rule to disallow redundant namespace aliases (#445).
+- Added: `function-calculation-no-interpolation` rule to forbid interpolation in calc functions (#539).
+
+**Full Changelog**: https://github.com/stylelint-scss/stylelint-scss/compare/v5.1.0...v5.2.0
+
+# 5.1.0
+
+- Added: `at-root-no-redundant` rule to ban unnecessary `@at-root` rule (#601).
+
+**Full Changelog**: https://github.com/stylelint-scss/stylelint-scss/compare/v5.1.0...v5.2.0
+
 # 5.1.0
 
 - Added: `function-disallowed-list` rule support to ban specific built-in functions (#422, #844).
diff --git a/README.md b/README.md
index dee7946..7de6906 100644
--- a/README.md
+++ b/README.md
@@ -30,9 +30,7 @@ Create the `.stylelintrc.json` config file (or open the existing one), add `styl
 
 ```jsonc
 {
-  "plugins": [
-    "stylelint-scss"
-  ],
+  "plugins": ["stylelint-scss"],
   "rules": {
     // recommended rules
     "at-rule-no-unknown": null,
@@ -102,6 +100,7 @@ Please also see the [example configs](./docs/examples/) for special cases.
 ### `@`-use
 
 - [`at-use-no-unnamespaced`](./src/rules/at-use-no-unnamespaced/README.md): Disallow `@use` without a namespace (i.e. `@use "..." as *`).
+- [`at-use-no-redundant-alias`](./src/rules/at-use-no-redundant-alias/README.md): Disallow redundant namespace aliases (i.e. `@use "foo" as foo`).
 
 ### `$`-variable
 
@@ -135,6 +134,7 @@ Please also see the [example configs](./docs/examples/) for special cases.
 
 - [`declaration-nested-properties`](./src/rules/declaration-nested-properties/README.md): Require or disallow properties with `-` in their names to be in a form of a nested group.
 - [`declaration-nested-properties-no-divided-groups`](./src/rules/declaration-nested-properties-no-divided-groups/README.md): Disallow nested properties of the same "namespace" to be divided into multiple groups.
+- [`property-no-unknown`](./src/rules/property-no-unknown/README.md): Disallow unknown properties, including [nested properties](https://sass-lang.com/documentation/style-rules/declarations/#nesting).
 
 ### Dimension
 
@@ -145,6 +145,7 @@ Please also see the [example configs](./docs/examples/) for special cases.
 - [`function-color-relative`](./src/rules/function-color-relative/README.md): Encourage the use of the [scale-color](https://sass-lang.com/documentation/modules/color#scale-color) function over regular color functions.
 - [`function-disallowed-list`](./src/rules/function-disallowed-list/README.md): Specify a list of disallowed functions. Should be used **instead of** Stylelint's [function-disallowed-list](https://stylelint.io/user-guide/rules/function-disallowed-list).
 - [`function-no-unknown`](./src/rules/function-no-unknown/README.md): Disallow unknown functions. Should be used **instead of** Stylelint's [function-no-unknown](https://stylelint.io/user-guide/rules/function-no-unknown).
+- [`function-calculation-no-interpolation`](./src/rules/function-calculation-no-interpolation/README.md): Forbids interpolation in `calc()`, `clamp()`, `min()`, and `max()` functions.
 - [`function-quote-no-quoted-strings-inside`](./src/rules/function-quote-no-quoted-strings-inside/README.md): Disallow quoted strings inside the [quote function](https://sass-lang.com/documentation/modules/string#quote) (Autofixable).
 - [`function-unquote-no-unquoted-strings-inside`](./src/rules/function-unquote-no-unquoted-strings-inside/README.md): Disallow unquoted strings inside the [unquote function](https://sass-lang.com/documentation/modules/string#unquote) (Autofixable).
 
diff --git a/src/rules/at-root-no-redundant/README.md b/src/rules/at-root-no-redundant/README.md
new file mode 100644
index 0000000..d4007d1
--- /dev/null
+++ b/src/rules/at-root-no-redundant/README.md
@@ -0,0 +1,75 @@
+# at-root-no-redundant
+
+Disallow redundant `@at-root` rule.
+
+<!-- prettier-ignore -->
+```scss
+@at-root .a { margin: 3px; }
+/** ↑
+ * This rule is unnecessary 
+ */
+```
+
+The `@at-root` rule is redundant in the following cases:
+
+- If `@at-root` is already at the root of the document.
+- If any `@at-root` selector contains the parent selector, [`&`](https://sass-lang.com/documentation/style-rules/parent-selector/), outside interpolation.
+- If `@at-root` is nested within a `@keyframes` block.
+
+## Options
+
+### `true`
+
+The following patterns are considered warnings:
+
+<!-- prettier-ignore -->
+```scss
+@at-root .a { margin: 3px; }
+```
+
+<!-- prettier-ignore -->
+```scss
+.a { @at-root .b & { margin: 3px; } }
+```
+
+<!-- prettier-ignore -->
+```scss
+@keyframes slidein {
+  @at-root from {
+    transform: translateX(0%);
+  }
+
+  to {
+    transform: translateX(100%);
+  }
+}
+```
+
+The following patterns are _not_ considered warnings:
+
+<!-- prettier-ignore -->
+```scss
+.a { @at-root .b { margin: 3px; } }
+```
+
+<!-- prettier-ignore -->
+```scss
+.a { @at-root .b#{&} { margin: 3px; } }
+```
+
+<!-- prettier-ignore -->
+```scss
+.a {
+  @at-root .b {
+    @keyframes slidein {
+      from {
+        transform: translateX(0%);
+      }
+    
+      to {
+        transform: translateX(100%);
+      }
+    }
+  }
+}
+```
diff --git a/src/rules/at-root-no-redundant/index.js b/src/rules/at-root-no-redundant/index.js
new file mode 100644
index 0000000..86a9232
--- /dev/null
+++ b/src/rules/at-root-no-redundant/index.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const { utils } = require("stylelint");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+
+const ruleName = namespace("at-root-no-redundant");
+
+function isWithinKeyframes(node) {
+  let parent = node.parent;
+  while (parent) {
+    if (parent.type === "atrule" && parent.name === "keyframes") {
+      return true;
+    }
+    parent = parent.parent;
+  }
+  return false;
+}
+
+const messages = utils.ruleMessages(ruleName, {
+  rejected: "Unexpected @at-root rule."
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+function rule(actual) {
+  return (root, result) => {
+    const validOptions = utils.validateOptions(result, ruleName, {
+      actual
+    });
+
+    if (!validOptions) {
+      return;
+    }
+
+    root.walkAtRules("at-root", node => {
+      if (
+        node.parent.type === "root" ||
+        node.params.replace(/#{.*}/g, "").includes("&") ||
+        isWithinKeyframes(node)
+      ) {
+        utils.report({
+          message: messages.rejected,
+          node,
+          result,
+          ruleName
+        });
+      }
+    });
+  };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
+module.exports = rule;
diff --git a/src/rules/at-use-no-redundant-alias/README.md b/src/rules/at-use-no-redundant-alias/README.md
new file mode 100644
index 0000000..c5ddc45
--- /dev/null
+++ b/src/rules/at-use-no-redundant-alias/README.md
@@ -0,0 +1,44 @@
+# at-use-no-redundant-alias
+
+Disallow redundant namespace aliases.
+
+```scss
+@use "foo" as foo;
+/**           ↑
+ * Disallow this */
+```
+
+By default, the [module's namespace](https://sass-lang.com/documentation/at-rules/use/#loading-members)
+is the last component of the module’s URL.
+
+## Options
+
+### `true`
+
+The following patterns are considered warnings:
+
+```scss
+@use "foo" as foo;
+```
+
+```scss
+@use "sass:math" as math;
+```
+
+```scss
+@use "src/corners" as corners;
+```
+
+The following patterns are _not_ considered warnings:
+
+```scss
+@use "foo" as bar;
+```
+
+```scss
+@use "sass:math";
+```
+
+```scss
+@use "src/corners" as c;
+```
diff --git a/src/rules/at-use-no-redundant-alias/index.js b/src/rules/at-use-no-redundant-alias/index.js
new file mode 100644
index 0000000..fd3f14a
--- /dev/null
+++ b/src/rules/at-use-no-redundant-alias/index.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const { utils } = require("stylelint");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+
+const ruleName = namespace("at-use-no-redundant-alias");
+
+const messages = utils.ruleMessages(ruleName, {
+  rejected: "Unexpected redundant namespace."
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+function getDefaultNamespace(module) {
+  return module.match(/([^/:]+)$/)[1].replace(/\.[^.]|["]+$/, "");
+}
+
+function separateEachParams(paramString) {
+  const parts = paramString.replace(/"/g, "").split(/\s+as\s+|\s+with\s+/);
+  if (parts.length < 2) return;
+  return parts;
+}
+
+function rule(actual) {
+  return (root, result) => {
+    const validOptions = utils.validateOptions(result, ruleName, { actual });
+
+    if (!validOptions) {
+      return;
+    }
+
+    root.walkAtRules("use", decl => {
+      const parts = separateEachParams(decl.params);
+      if (parts && getDefaultNamespace(parts[0]) === parts[1]) {
+        utils.report({
+          message: messages.rejected,
+          node: decl,
+          result,
+          ruleName
+        });
+      }
+    });
+  };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
+module.exports = rule;
diff --git a/src/rules/function-calculation-no-interpolation/README.md b/src/rules/function-calculation-no-interpolation/README.md
new file mode 100644
index 0000000..729bd55
--- /dev/null
+++ b/src/rules/function-calculation-no-interpolation/README.md
@@ -0,0 +1,64 @@
+# function-calculation-no-interpolation
+
+Disallow interpolation in `calc()`, `clamp()`, `min()`, and `max()`.
+
+<!-- prettier-ignore -->
+```scss
+.a { .b: calc(#{$c} + 1); }
+/**            ↑
+      * This argument */
+```
+
+Since the release of [first-class `calc()`](https://sass-lang.com/documentation/values/calculations/),
+calculation functions `calc()`, `clamp()`, `min()`, and `max()` accept variables
+and function calls as arguments. This rule disallows interpolation to avoid
+extra verbose or even invalid CSS.
+
+## Options
+
+### `true`
+
+The following patterns are considered warnings:
+
+<!-- prettier-ignore -->
+```scss
+$c: 1;
+.a { .b: calc(#{$c + 1}); }
+```
+
+<!-- prettier-ignore -->
+```scss
+$c: 1;
+.a { .b: calc(max(#{$c})); }
+```
+
+<!-- prettier-ignore -->
+```scss
+$c: 1;
+.a { .b: min(#{$c}); }
+```
+
+<!-- prettier-ignore -->
+```scss
+$c: 1;
+.a { .b: clamp(#{$c} + 2px); }
+```
+
+The following patterns are _not_ considered warnings:
+
+<!-- prettier-ignore -->
+```scss
+.a { .b: calc(1 + 1); }
+```
+
+<!-- prettier-ignore -->
+```scss
+$c: 1;
+.a { .b: abc(#{$c} + 1px); }
+```
+
+<!-- prettier-ignore -->
+```scss
+$c: 1;
+.a { .b: calc(abc(#{$c})); }
+```
diff --git a/src/rules/function-calculation-no-interpolation/index.js b/src/rules/function-calculation-no-interpolation/index.js
new file mode 100644
index 0000000..0d3902e
--- /dev/null
+++ b/src/rules/function-calculation-no-interpolation/index.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const { utils } = require("stylelint");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+const valueParser = require("postcss-value-parser");
+
+const ruleName = namespace("function-calculation-no-interpolation");
+
+const messages = utils.ruleMessages(ruleName, {
+  rejected: func => `Unexpected interpolation in "${func}".`
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+function rule(actual) {
+  return (root, result) => {
+    const validOptions = utils.validateOptions(result, ruleName, { actual });
+
+    if (!validOptions) {
+      return;
+    }
+
+    const calculationFunctions = ["calc", "max", "min", "clamp"];
+
+    root.walkDecls(decl => {
+      valueParser(decl.value).walk(node => {
+        if (node.type !== "function" || node.value === "") {
+          return;
+        }
+        if (
+          calculationFunctions.includes(node.value) &&
+          node.nodes.some(
+            args => args.type === "word" && /^#{.*|\s*}/.test(args.value)
+          )
+        ) {
+          utils.report({
+            message: messages.rejected(node.value),
+            node: decl,
+            word: decl.name,
+            result,
+            ruleName
+          });
+        }
+      });
+    });
+  };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
+module.exports = rule;
diff --git a/src/rules/index.js b/src/rules/index.js
index fcdbe99..8ade850 100644
--- a/src/rules/index.js
+++ b/src/rules/index.js
@@ -22,7 +22,9 @@ const rules = {
   "at-mixin-parentheses-space-before": require("./at-mixin-parentheses-space-before"),
   "at-mixin-pattern": require("./at-mixin-pattern"),
   "at-rule-conditional-no-parentheses": require("./at-rule-conditional-no-parentheses"),
+  "at-root-no-redundant": require("./at-root-no-redundant"),
   "at-rule-no-unknown": require("./at-rule-no-unknown"),
+  "at-use-no-redundant-alias": require("./at-use-no-redundant-alias"),
   "at-use-no-unnamespaced": require("./at-use-no-unnamespaced"),
   "comment-no-empty": require("./comment-no-empty"),
   "comment-no-loud": require("./comment-no-loud"),
@@ -43,6 +45,7 @@ const rules = {
   "double-slash-comment-inline": require("./double-slash-comment-inline"),
   "double-slash-comment-whitespace-inside": require("./double-slash-comment-whitespace-inside"),
   "function-disallowed-list": require("./function-disallowed-list"),
+  "function-calculation-no-interpolation": require("./function-calculation-no-interpolation"),
   "function-color-relative": require("./function-color-relative"),
   "function-no-unknown": require("./function-no-unknown"),
   "function-quote-no-quoted-strings-inside": require("./function-quote-no-quoted-strings-inside"),
@@ -58,6 +61,7 @@ const rules = {
   "operator-no-unspaced": require("./operator-no-unspaced"),
   "partial-no-import": require("./partial-no-import"),
   "percent-placeholder-pattern": require("./percent-placeholder-pattern"),
+  "property-no-unknown": require("./property-no-unknown"),
   "selector-nest-combinators": require("./selector-nest-combinators"),
   "selector-no-redundant-nesting-selector": require("./selector-no-redundant-nesting-selector"),
   "selector-no-union-class-name": require("./selector-no-union-class-name")
diff --git a/src/rules/property-no-unknown/README.md b/src/rules/property-no-unknown/README.md
new file mode 100644
index 0000000..90fbdb3
--- /dev/null
+++ b/src/rules/property-no-unknown/README.md
@@ -0,0 +1,207 @@
+# property-no-unknown
+
+Disallow unknown properties. Should be used instead of Stylelint's [property-no-unknown](https://stylelint.io/user-guide/rules/property-no-unknown).
+
+<!-- prettier-ignore -->
+```scss
+a { height: 100%; }
+/** ↑
+ * This property */
+```
+
+This rule considers properties defined in the [CSS Specifications and browser specific properties](https://github.com/betit/known-css-properties#source) to be known.
+
+This rule ignores:
+
+- variables (`$sass`, `@less`, `--custom-property`)
+- vendor-prefixed properties (e.g., `-moz-align-self`, `-webkit-align-self`)
+
+Use option `checkPrefixed` described below to turn on checking of vendor-prefixed properties.
+
+The [`message` secondary option](../../../docs/user-guide/configure.md#message) can accept the arguments of this rule.
+
+## Options
+
+### `true`
+
+The following patterns are considered problems:
+
+<!-- prettier-ignore -->
+```scss
+a {
+  colr: blue;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  my-property: 1;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  font: {
+    stuff: bold;
+  }
+}
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```scss
+a {
+  color: green;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  fill: black;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  -moz-align-self: center;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  -webkit-align-self: center;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  align-self: center;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  font: {
+    weight: bold;
+  }
+}
+```
+
+## Optional secondary options
+
+### `ignoreProperties: ["/regex/", /regex/, "string"]`
+
+Given:
+
+```json
+["/^my-/", "custom"]
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```scss
+a {
+  my-property: 10px;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  my-other-property: 10px;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  custom: 10px;
+}
+```
+
+### `ignoreSelectors: ["/regex/", /regex/, "string"]`
+
+Skips checking properties of the given selectors against this rule.
+
+Given:
+
+```json
+[":root"]
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```scss
+:root {
+  my-property: blue;
+}
+```
+
+### `ignoreAtRules: ["/regex/", /regex/, "string"]`
+
+Ignores properties nested within specified at-rules.
+
+Given:
+
+```json
+["supports"]
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```scss
+@supports (display: grid) {
+  a {
+    my-property: 1;
+  }
+}
+```
+
+### `checkPrefixed: true | false` (default: `false`)
+
+If `true`, this rule will check vendor-prefixed properties.
+
+For example with `true`:
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```scss
+a {
+  -webkit-overflow-scrolling: auto;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  -moz-box-flex: 0;
+}
+```
+
+The following patterns are considered problems:
+
+<!-- prettier-ignore -->
+```scss
+a {
+  -moz-align-self: center;
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+a {
+  -moz-overflow-scrolling: center;
+}
+```
diff --git a/src/rules/property-no-unknown/index.js b/src/rules/property-no-unknown/index.js
new file mode 100644
index 0000000..1230c90
--- /dev/null
+++ b/src/rules/property-no-unknown/index.js
@@ -0,0 +1,128 @@
+"use strict";
+
+const isCustomPropertySet = require("../../utils/isCustomPropertySet");
+const isStandardSyntaxProperty = require("../../utils/isStandardSyntaxProperty");
+const isStandardSyntaxDeclaration = require("../../utils/isStandardSyntaxDeclaration");
+const isType = require("../../utils/isType");
+const optionsMatches = require("../../utils/optionsMatches");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+const properties = require("known-css-properties").all;
+
+const { utils } = require("stylelint");
+const { isBoolean, isRegExp, isString } = require("../../utils/validateTypes");
+
+const ruleName = namespace("property-no-unknown");
+
+function vendorPrefix(node) {
+  const match = node.match(/^(-\w+-)/);
+  if (match) {
+    return match[0] || "";
+  }
+  return "";
+}
+
+const messages = utils.ruleMessages(ruleName, {
+  rejected: property => `Unexpected unknown property "${property}"`
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+function rule(primary, secondaryOptions) {
+  const allValidProperties = new Set(properties);
+
+  return (root, result) => {
+    const validOptions = utils.validateOptions(
+      result,
+      ruleName,
+      { actual: primary },
+      {
+        actual: secondaryOptions,
+        possible: {
+          ignoreProperties: [isString, isRegExp],
+          checkPrefixed: [isBoolean],
+          ignoreSelectors: [isString, isRegExp],
+          ignoreAtRules: [isString, isRegExp]
+        },
+        optional: true
+      }
+    );
+
+    if (!validOptions) {
+      return;
+    }
+
+    const shouldCheckPrefixed =
+      secondaryOptions && secondaryOptions.checkPrefixed;
+
+    root.walkDecls(decl => {
+      const hasNamespace = decl.prop.indexOf(".");
+      let prop =
+        hasNamespace > -1 ? decl.prop.slice(hasNamespace + 1) : decl.prop;
+
+      if (
+        !isStandardSyntaxProperty(prop) ||
+        !isStandardSyntaxDeclaration(decl) ||
+        isCustomPropertySet(prop) ||
+        prop.startsWith("--") ||
+        (!shouldCheckPrefixed && vendorPrefix(prop)) ||
+        optionsMatches(secondaryOptions, "ignoreProperties", prop)
+      ) {
+        return;
+      }
+
+      const parent = decl.parent;
+
+      if (
+        parent &&
+        isType(parent, "rule") &&
+        optionsMatches(secondaryOptions, "ignoreSelectors", parent.selector)
+      ) {
+        return;
+      }
+
+      let node = parent;
+
+      while (node && node.type !== "root") {
+        if (
+          isType(node, "atrule") &&
+          optionsMatches(secondaryOptions, "ignoreAtRules", node.name)
+        ) {
+          return;
+        }
+
+        node = node.parent;
+      }
+
+      // Nested properties
+      if (
+        parent &&
+        isType(parent, "rule") &&
+        parent.selector &&
+        parent.selector[parent.selector.length - 1] === ":" &&
+        parent.selector.substring(0, 2) !== "--"
+      ) {
+        prop = parent.selector.replace(":", "") + "-" + prop;
+      }
+
+      if (allValidProperties.has(prop.toLowerCase())) {
+        return;
+      }
+
+      utils.report({
+        message: messages.rejected(prop),
+        node: decl,
+        word: prop,
+        result,
+        ruleName
+      });
+    });
+  };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+module.exports = rule;
diff --git a/src/utils/isStandardSyntaxDeclaration.js b/src/utils/isStandardSyntaxDeclaration.js
new file mode 100644
index 0000000..f46151b
--- /dev/null
+++ b/src/utils/isStandardSyntaxDeclaration.js
@@ -0,0 +1,50 @@
+"use strict";
+
+/**
+ * Check whether a declaration is standard
+ *
+ * @param {Object} decl
+ * @returns {boolean}
+ */
+module.exports = function (decl) {
+  const prop = decl.prop;
+  const parent = decl.parent;
+
+  // SCSS var (e.g. $var: x), list (e.g. $list: (x)) or map (e.g. $map: (key:value))
+  if (prop.startsWith("$")) {
+    return true;
+  }
+
+  // SCSS var within a namespace (e.g. namespace.$var: x)
+  if (prop.includes(".$")) {
+    return true;
+  }
+
+  // Less var (e.g. @var: x), but exclude variable interpolation (e.g. @{var})
+  if (prop[0] === "@" && prop[1] !== "{") {
+    return false;
+  }
+
+  // Less map declaration
+  if (parent && parent.type === "atrule" && parent.raws.afterName === ":") {
+    return false;
+  }
+
+  // Less map (e.g. #my-map() { myprop: red; })
+  if (
+    parent &&
+    parent.type === "rule" &&
+    parent.selector &&
+    parent.selector.startsWith("#") &&
+    parent.selector.endsWith("()")
+  ) {
+    return false;
+  }
+
+  // Less &:extend
+  if ("extend" in decl && decl.extend) {
+    return false;
+  }
+
+  return true;
+};


Test patch:
diff --git a/src/rules/at-root-no-redundant/__tests__/index.js b/src/rules/at-root-no-redundant/__tests__/index.js
new file mode 100644
index 0000000..0985db3
--- /dev/null
+++ b/src/rules/at-root-no-redundant/__tests__/index.js
@@ -0,0 +1,136 @@
+"use strict";
+
+const { messages, ruleName } = require("..");
+
+testRule({
+  ruleName,
+  config: [true],
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: `
+      .a { @at-root .b { c: d } }
+      `,
+      description: "@at-root rule is nested."
+    },
+    {
+      code: `
+      .a { @at-root .b#{&} { c: d; } }
+      `,
+      description:
+        "@at-root is followed by a selector with an interpolated `&`."
+    },
+    {
+      code: `
+      .a { @at-root .b { .c & { d: e; } } }
+      `,
+      line: 2,
+      column: 12,
+      message: messages.rejected,
+      description: "A parent selector (&) is used under the @at-root rule."
+    },
+    {
+      code: `
+      .a {
+        @at-root .b {
+          @keyframes slidein {
+            from {
+              transform: translateX(0%);
+            }
+          
+            to {
+              transform: translateX(100%);
+            }
+          }
+        }
+      }`,
+      description: "@at-root outside a keyframes block, nested."
+    },
+    {
+      code: `
+      @mixin foo {}
+      
+      @keyframes slidein {
+        from {
+          @include foo;
+          transform: translateX(0%);
+        }
+      
+        to {
+          transform: translateX(100%);
+        }
+      }`,
+      description: "Other at-rules nested inside @keyframes block."
+    }
+  ],
+  reject: [
+    {
+      code: `
+      @at-root .a { b: c; }
+      `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "@at-root rule is already in the root."
+    },
+    {
+      code: `
+      .a { @at-root .b & { c: d; } }
+      `,
+      line: 2,
+      column: 12,
+      message: messages.rejected,
+      description:
+        "@at-root is followed by a selector with a `&` outside an interpolation."
+    },
+    {
+      code: `
+      .a { @at-root .b & #{.c} { d: e; } }
+      `,
+      line: 2,
+      column: 12,
+      message: messages.rejected,
+      description:
+        "@at-root is followed by a selector with a `&` outside an interpolation."
+    },
+    {
+      code: `
+      @keyframes slidein {
+        @at-root from {
+          transform: translateX(0%);
+        }
+      
+        to {
+          transform: translateX(100%);
+        }
+      }`,
+      line: 3,
+      column: 9,
+      message: messages.rejected,
+      description: "@at-root inside a keyframes block, top level."
+    },
+    {
+      code: `
+      @keyframes slidein {
+        from {
+          transform: translateX(0%);
+             @at-root from {
+              transform: translateX(100%);
+             }
+             to {
+                transform: translateX(0%);
+              }
+        }
+      
+        to {
+          transform: translateX(100%);
+        }
+      }`,
+      line: 5,
+      column: 14,
+      message: messages.rejected,
+      description: "@at-root inside a @keyframes block, nested."
+    }
+  ]
+});
diff --git a/src/rules/at-use-no-redundant-alias/__tests__/index.js b/src/rules/at-use-no-redundant-alias/__tests__/index.js
new file mode 100644
index 0000000..601670a
--- /dev/null
+++ b/src/rules/at-use-no-redundant-alias/__tests__/index.js
@@ -0,0 +1,95 @@
+"use strict";
+
+const { messages, ruleName } = require("..");
+
+testRule({
+  ruleName,
+  config: [true],
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: `
+      @use "foo";
+    `,
+      description: "Default namespace"
+    },
+    {
+      code: `
+      @use "foo/bar" as foo;
+    `,
+      description: "Different namespace"
+    },
+    {
+      code: `
+      @use "sass:math" as *;
+    `,
+      description: "Without namespace"
+    },
+    {
+      code: `
+      @use "sass:math";
+    `,
+      description: "No explicit namespace"
+    }
+  ],
+
+  reject: [
+    {
+      code: `
+      @use "foo" as foo;
+    `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "Without url"
+    },
+    {
+      code: `
+      @use "src/corners" as corners;
+    `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "With url"
+    },
+    {
+      code: `
+      @use "sass:math" as math;
+    `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "Built-in module"
+    },
+    {
+      code: `
+      @use "foo"   as   foo;
+    `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "Without space"
+    },
+    {
+      code: `
+      @use "foo" as foo with ($baz: 1px);
+    `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "Configured"
+    },
+    {
+      code: `
+      @use "sass:color" as color with (
+        $baz: 1px
+      );
+    `,
+      line: 2,
+      column: 7,
+      message: messages.rejected,
+      description: "Configured multiline"
+    }
+  ]
+});
diff --git a/src/rules/function-calculation-no-interpolation/__tests__/index.js b/src/rules/function-calculation-no-interpolation/__tests__/index.js
new file mode 100644
index 0000000..fa91900
--- /dev/null
+++ b/src/rules/function-calculation-no-interpolation/__tests__/index.js
@@ -0,0 +1,82 @@
+"use strict";
+
+const { messages, ruleName } = require("..");
+
+testRule({
+  ruleName,
+  config: [true],
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: `.a { .b: calc(1 + 1); }`,
+      description: "`calc` function, no interpolation"
+    },
+    {
+      code: `
+      $c: 1;
+      .a { .b: abc(#{$c} + 1px); }
+      `,
+      description: "Allowed function with interpolation"
+    },
+    {
+      code: `
+      $c: 1;
+      .a { .b: calc(abc(#{$c})); }
+      `,
+      description: "Allowed function with interpolation nested in `calc`"
+    }
+  ],
+  reject: [
+    {
+      code: `
+      $c: 1;
+      .a { .b: calc(#{$c} + 1); }
+      `,
+      line: 3,
+      column: 12,
+      message: messages.rejected("calc"),
+      description: "`calc` function one argument interpolated"
+    },
+    {
+      code: `
+      $c: 1;
+      .a { .b: calc(#{$c + 1}); }
+      `,
+      line: 3,
+      column: 12,
+      message: messages.rejected("calc"),
+      description: "`calc` function all arguments interpolated"
+    },
+    {
+      code: `
+      $c: 1;
+      .a { .b: calc(max(#{$c})); }
+      `,
+      line: 3,
+      column: 12,
+      message: messages.rejected("max"),
+      description: "`max` function with interpolation"
+    },
+    {
+      code: `
+      $c: 1;
+      .a { .b: min(#{$c} + 1px); }
+      `,
+      line: 3,
+      column: 12,
+      message: messages.rejected("min"),
+      description: "`min` function with interpolation"
+    },
+    {
+      code: `
+      $c: 1;
+      .a { .b: clamp(#{$c} + #{$d}); }
+      `,
+      line: 3,
+      column: 12,
+      message: messages.rejected("clamp"),
+      description: "`clamp` function with interpolation"
+    }
+  ]
+});
diff --git a/src/rules/property-no-unknown/__tests__/index.js b/src/rules/property-no-unknown/__tests__/index.js
new file mode 100644
index 0000000..e99c1fb
--- /dev/null
+++ b/src/rules/property-no-unknown/__tests__/index.js
@@ -0,0 +1,335 @@
+"use strict";
+
+const { messages, ruleName } = require("..");
+
+testRule({
+  ruleName,
+  config: [true],
+
+  accept: [
+    {
+      code: ".foo { color: green; }"
+    },
+    {
+      code: ".foo { COLoR: green; }"
+    },
+    {
+      code: ".foo { fill: black; }"
+    },
+    {
+      code: ".foo { -webkit-align-self: center; }"
+    },
+    {
+      code: ".foo { align-self: center; }"
+    },
+    {
+      code: ".foo { --bg-color: white; }",
+      description: "ignore standard CSS variables"
+    },
+    {
+      code: ".foo { -moz-align-self: center; }",
+      description: "ignore vendor prefixes"
+    },
+    {
+      code: ".foo { *width: 100px; }",
+      description: "ignore CSS hacks"
+    },
+    {
+      code: ".foo { --custom-property-set: { colr: blue; } }",
+      description: "ignore custom property sets"
+    }
+  ],
+
+  reject: [
+    {
+      code: ".foo { colr: blue; }",
+      message: messages.rejected("colr"),
+      line: 1,
+      column: 8,
+      endLine: 1,
+      endColumn: 12
+    },
+    {
+      code: ".foo { COLR: blue; }",
+      message: messages.rejected("COLR"),
+      line: 1,
+      column: 8,
+      endLine: 1,
+      endColumn: 12
+    },
+    {
+      code: ".foo {\n  colr: blue;\n}",
+      message: messages.rejected("colr"),
+      line: 2,
+      column: 3,
+      endLine: 2,
+      endColumn: 7
+    },
+    {
+      code: ".foo { *wdth: 100px; }",
+      message: messages.rejected("wdth"),
+      line: 1,
+      column: 8,
+      endLine: 1,
+      endColumn: 12
+    },
+    {
+      code: ":export { my-property: red; }",
+      message: messages.rejected("my-property"),
+      line: 1,
+      column: 11,
+      endLine: 1,
+      endColumn: 22
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  customSyntax: "postcss-scss",
+  config: [true],
+
+  accept: [
+    {
+      code: ".foo { $bgColor: white; }",
+      description: "ignore SCSS variables"
+    },
+    {
+      code: ".foo { namespace.$bgColor: white; }",
+      description: "ignore SCSS variables within namespace"
+    },
+    {
+      code: ".foo { #{$prop}: black; }",
+      description: "ignore property interpolation"
+    },
+    {
+      code: ".foo { border: { style: solid; } }",
+      description: "ignore nested properties"
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  customSyntax: "postcss-less",
+  config: [true],
+
+  accept: [
+    {
+      code: ".foo { @bgColor: white; }",
+      description: "ignore LESS variables"
+    },
+    {
+      code: ".foo { @{prop}: black; }",
+      description: "ignore property interpolation"
+    },
+    {
+      code: ".foo { transform+: rotate(15deg); }",
+      description: "Append property value with space using +"
+    },
+    {
+      code: ".foo { transform+_: rotate(15deg); }",
+      description: "Append property value with space using +_"
+    },
+    {
+      code: "@foo: { prop: red; }",
+      description: "ignore LESS map props"
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [
+    true,
+    {
+      ignoreProperties: ["-moz-overflow-scrolling", "/^my-/"],
+      checkPrefixed: true
+    }
+  ],
+
+  accept: [
+    {
+      code: ".foo { -webkit-overflow-scrolling: auto; }"
+    },
+    {
+      code: ".foo { -moz-overflow-scrolling: auto; }"
+    },
+    {
+      code: ".foo { my-property: 1; }"
+    },
+    {
+      code: ".foo { my-other-property: 1; }"
+    }
+  ],
+
+  reject: [
+    {
+      code: ".foo { overflow-scrolling: auto; }",
+      message: messages.rejected("overflow-scrolling"),
+      line: 1,
+      column: 8
+    },
+    {
+      code: ".foo { not-my-property: 1; }",
+      message: messages.rejected("not-my-property"),
+      line: 1,
+      column: 8
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [
+    true,
+    {
+      ignoreProperties: [/^my-/],
+      checkPrefixed: true
+    }
+  ],
+
+  accept: [
+    {
+      code: ".foo { my-property: 1; }"
+    }
+  ],
+
+  reject: [
+    {
+      code: ".foo { not-my-property: 1; }",
+      message: messages.rejected("not-my-property"),
+      line: 1,
+      column: 8
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [true, { checkPrefixed: true }],
+
+  accept: [
+    {
+      code: ".foo { -webkit-overflow-scrolling: auto; }"
+    },
+    {
+      code: ".foo { -moz-box-flex: 0; }"
+    }
+  ],
+
+  reject: [
+    {
+      code: ".foo { -moz-overflow-scrolling: auto; }",
+      message: messages.rejected("-moz-overflow-scrolling"),
+      line: 1,
+      column: 8
+    },
+    {
+      code: ".foo { -moz-align-self: center; }",
+      message: messages.rejected("-moz-align-self"),
+      line: 1,
+      column: 8
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [true, { ignoreSelectors: [":export", ":import"] }],
+
+  accept: [
+    {
+      code: ":export { my-property: 1; }"
+    }
+  ],
+
+  reject: [
+    {
+      code: ":not-export { my-property: 1; }",
+      message: messages.rejected("my-property"),
+      line: 1,
+      column: 15
+    },
+    {
+      // Non-regex strings must exactly match the parent selector.
+      code: ':import("path/to/file.css") { my-property: 1; }',
+      message: messages.rejected("my-property"),
+      line: 1,
+      column: 31
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [true, { ignoreSelectors: ["/:export/", /^:import/] }],
+
+  accept: [
+    {
+      code: ":export { my-property: 1; }"
+    },
+    {
+      code: ':import("path/to/file.css") { my-property: 1; }'
+    }
+  ],
+
+  reject: [
+    {
+      code: ":exprat { my-property: 1; }",
+      message: messages.rejected("my-property"),
+      line: 1,
+      column: 11
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [true, { ignoreAtRules: ["supports", /^my-/] }],
+
+  accept: [
+    {
+      code: "@supports (display: grid) { my-property: 1; }"
+    },
+    {
+      code: "@my-at-rule { foo: 1; }"
+    },
+    {
+      code: "@supports (display:grid) { @media (min-width: 10px) { foo: 1; } }"
+    },
+    {
+      code: "@supports (display:grid) { @media (min-width: 10px) { a { foo: 1; } } }"
+    },
+    {
+      code: "@my-other-at-rule { a { foo: 1; } }"
+    }
+  ],
+
+  reject: [
+    {
+      code: "@media screen { a { foo: 1; } }",
+      message: messages.rejected("foo"),
+      line: 1,
+      column: 21
+    },
+    {
+      code: "@not-my-at-rule { foo: 1; }",
+      message: messages.rejected("foo"),
+      line: 1,
+      column: 19
+    },
+    {
+      code: "a { foo: 1; }",
+      message: messages.rejected("foo"),
+      line: 1,
+      column: 5
+    },
+    {
+      code: "@not-my-at-rule foobar { foo: 1; }",
+      message: messages.rejected("foo"),
+      line: 1,
+      column: 26
+    }
+  ]
+});