Repository: stylelint-scss/stylelint-scss (js)

Issue:
Add `declaration-property-value-no-unknown` support for SCSS.
Add [`declaration-property-value-no-unknown`](https://github.com/stylelint/stylelint/tree/main/lib/rules/declaration-property-value-no-unknown) support for SCSS features and ignore SassScript.
E.g.
```scss
// Functions
@function get-color() {
   @return #336699;
}
.element {
  color: get-color(); // Unexpected unknown value "get-color" (declaration-property-value-no-unknown)
}

// Variables shorthand values
$var1: 1px;
a {
  margin: 2px $var1; // Cannot parse property value "2px $var1" for property "margin" (declaration-property-value-no-unknown)
}

// Hidden declarations
$rounded-corners: false;
.button {
  border: 1px solid black;
  border-radius: if($rounded-corners, 5px, null); // Cannot parse property value "if($rounded-corners, 5px, null)" for property "border-radius" (declaration-property-value-no-unknown)
}

// Nested properties
.b {
  font: {
    size: red; // No errors
  }
  font-size: red; // Unexpected unknown value "red" for property "font-size" (declaration-property-value-no-unknown)
}
```

[Demo](https://stylelint.io/demo/#N4Igxg9gJgpiBcID0SAEA1AhgJwJaYCMAbGAZ1VIAsJsAXSzAOylQDdMiBXMgHUYBJ22AIzxUwgA4APANx8+mVMHm0AtjgDmuRmIBM01IJzC5jAL7zGKVADFOjMLVwRGpPgAEAZvcfPGqDRhaAFpIIhoACgBKJT5UePdsIM5sfwBiAGYMgDZsgE480wtGPgA6GBJVGEZaWP9UMJoxQJDG7Giiy2sAOTJaGCgy7U8IYIlMQLr49WwtHVRMTloIKfjUAghaZdUxAAZTNdRliT1pA9RinloVEZqxZRLr2lJcAC8YMSSoUyvLx9uQi93p8Bp0SlY0AAJXBQWD+WBgIg4TBOFxuATYCD2WBQUI0RgwbCkMSeDikGA-RilAhLZb+B7xDbYWDYMSSKQUCBEGHrJFgADW5yZLOC2EwUFwnGJqFwngi-Ex2IGeNShNIABpUABWaSaxicIhEKJgvjWACSNUJEi5KL8fH4EiSnlwUjEPBAxAgAvdlPKulWEtIEiRAE8xNpuQTgmlgA6nS6zCaIagALKYCTo-htaUROJrR24GZh1BpXa7ADsBE8nnVefi5MgzBwxbS2TA5a15agtfq9c4YDAZGlaV0AA5MOWACxanuHKBMQKsktQMAZLXT2drADuOEY2g0YjS1bAwgrfGNlmptHpdfWmAFGkVzDx4SX6gkpRa8uzmoLRYv5iWEBjAgOqIDOiQ3SYFUCAgDAUjQcGMClKQYCkKQoHgC4zoaLBDKoO68H9MwbgIKgADat7uqQtAhiQkatNhuAaMENFMPOzKsWhpF5gAupu7rYAavBkfh8TugiSJiqijBjJiEiErRwTsFwMDBIwoz2PyGlbiUZG0EJMB5sUZiYY2OE2DQ6i0LBABWpAuJhsAZnhebUbR9HaIC3Hum6IBIv0NHugJIA0XRFReXijA4axtDsTguKoehvkEf5KJ9O6fCmWYQA)

Pull request description:
Add `declaration-property-value-no-unknown` support for SCSS.
Fix #1017.

Added support for:
- Functions
- Variables shorthand values
- Hidden declarations
- Nested properties


Patch:
diff --git a/package-lock.json b/package-lock.json
index 8437a09..9b2845a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,8 @@
       "version": "6.4.1",
       "license": "MIT",
       "dependencies": {
+        "css-tree": "2.3.1",
+        "is-plain-object": "5.0.0",
         "known-css-properties": "^0.34.0",
         "postcss-media-query-parser": "^0.2.3",
         "postcss-resolve-nested-selector": "^0.1.4",
@@ -2674,7 +2676,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
       "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
-      "dev": true,
       "dependencies": {
         "mdn-data": "2.0.30",
         "source-map-js": "^1.0.1"
@@ -4701,7 +4702,6 @@
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
       "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -6707,8 +6707,7 @@
     "node_modules/mdn-data": {
       "version": "2.0.30",
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
-      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
-      "dev": true
+      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
     },
     "node_modules/meow": {
       "version": "13.2.0",
@@ -8727,7 +8726,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
       "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -11850,7 +11848,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
       "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
-      "dev": true,
       "requires": {
         "mdn-data": "2.0.30",
         "source-map-js": "^1.0.1"
@@ -13287,8 +13284,7 @@
     "is-plain-object": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
-      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
-      "dev": true
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
     },
     "is-promise": {
       "version": "2.2.2",
@@ -14763,8 +14759,7 @@
     "mdn-data": {
       "version": "2.0.30",
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
-      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
-      "dev": true
+      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
     },
     "meow": {
       "version": "13.2.0",
@@ -16099,8 +16094,7 @@
     "source-map-js": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
-      "dev": true
+      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg=="
     },
     "source-map-support": {
       "version": "0.5.13",
diff --git a/package.json b/package.json
index 2ba43ab..f46cfcd 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,8 @@
     "stylelint": "^16.0.2"
   },
   "dependencies": {
+    "css-tree": "2.3.1",
+    "is-plain-object": "5.0.0",
     "known-css-properties": "^0.34.0",
     "postcss-media-query-parser": "^0.2.3",
     "postcss-resolve-nested-selector": "^0.1.4",
diff --git a/src/rules/declaration-property-value-no-unknown/README.md b/src/rules/declaration-property-value-no-unknown/README.md
new file mode 100644
index 0000000..80a45a2
--- /dev/null
+++ b/src/rules/declaration-property-value-no-unknown/README.md
@@ -0,0 +1,141 @@
+# declaration-property-value-no-unknown
+
+Disallow unknown values for properties within declarations.
+
+<!-- prettier-ignore -->
+```css
+a { top: unknown; }
+/** ↑    ↑
+ * property and value pairs like these */
+```
+
+This rule considers values for properties defined within the CSS specifications to be known. You can use the `propertiesSyntax` and `typesSyntax` secondary options to extend the syntax.
+
+This rule is only appropriate for CSS. You should not turn it on for CSS-like languages, such as Sass or Less, as they have their own syntaxes.
+
+This rule is experimental with some false negatives that we'll patch in minor releases.
+
+It sometimes overlaps with:
+
+- [`color-no-invalid-hex`](../color-no-invalid-hex/README.md)
+- [`function-no-unknown`](../function-no-unknown/README.md)
+- [`string-no-newline`](../string-no-newline/README.md)
+- [`unit-no-unknown`](../unit-no-unknown/README.md)
+
+If duplicate problems are flagged, you can turn off the corresponding rule.
+
+## Options
+
+### `true`
+
+The following patterns are considered problems:
+
+<!-- prettier-ignore -->
+```css
+a { top: red; }
+```
+
+<!-- prettier-ignore -->
+```css
+a { top: unknown; }
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```css
+a { top: 0; }
+```
+
+<!-- prettier-ignore -->
+```css
+a { top: var(--foo); }
+```
+
+## Optional secondary options
+
+### `ignoreProperties: { "property": ["/regex/", /regex/, "non-regex"]|"/regex/"|/regex/|"non-regex" }`
+
+Ignore the specified property and value pairs. Keys in the object indicate property names. If a string in the object is surrounded with `"/"`, it's interpreted as a regular expression. For example, `"/.+/"` matches any strings.
+
+Given:
+
+```json
+{
+  "top": ["unknown"],
+  "/^margin-/": "/^--foo/",
+  "padding": "/.+/",
+  "/.+/": "--unknown-value"
+}
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```css
+a { top: unknown; }
+```
+
+<!-- prettier-ignore -->
+```css
+a { margin-top: --foo-bar; }
+```
+
+<!-- prettier-ignore -->
+```css
+a { padding: invalid; }
+```
+
+<!-- prettier-ignore -->
+```css
+a { width: --unknown-value; }
+```
+
+### `propertiesSyntax: { property: syntax }`
+
+Extend or alter the properties syntax dictionary. [CSS Value Definition Syntax](https://github.com/csstree/csstree/blob/master/docs/definition-syntax.md) is used to define a value's syntax. If a definition starts with `|` it is added to the [existing definition value](https://csstree.github.io/docs/syntax/) if any.
+
+Given:
+
+```json
+{ "size": "<length-percentage>" }
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```css
+a { size: 0; }
+```
+
+<!-- prettier-ignore -->
+```css
+a { size: 10px }
+```
+
+### `typesSyntax: { type: syntax }`
+
+Extend or alter the types syntax dictionary. [CSS Value Definition Syntax](https://github.com/csstree/csstree/blob/master/docs/definition-syntax.md) is used to define a value's syntax. If a definition starts with `|` it is added to the [existing definition value](https://csstree.github.io/docs/syntax/) if any.
+
+Types are something like a preset which allows you to reuse a definition across other definitions. So, you'll likely want to also use the `propertiesSyntax` option when using this option.
+
+Given:
+
+```json
+{
+  "propertiesSyntax": { "top": "| <--foo()>" },
+  "typesSyntax": { "--foo()": "--foo( <length-percentage> )" }
+}
+```
+
+The following patterns are _not_ considered problems:
+
+<!-- prettier-ignore -->
+```css
+a { top: --foo(0); }
+```
+
+<!-- prettier-ignore -->
+```css
+a { top: --foo(10px); }
+```
\ No newline at end of file
diff --git a/src/rules/declaration-property-value-no-unknown/index.js b/src/rules/declaration-property-value-no-unknown/index.js
new file mode 100644
index 0000000..78af8d8
--- /dev/null
+++ b/src/rules/declaration-property-value-no-unknown/index.js
@@ -0,0 +1,297 @@
+"use strict";
+
+const cssTree = require("css-tree");
+const isPlainObject = require("is-plain-object");
+const typeGuards = require("../../utils/typeGuards.js");
+const declarationValueIndex = require("../../utils/declarationValueIndex.js");
+const isCustomProperty = require("../../utils/isCustomPropertySet.js");
+const isStandardSyntaxDeclaration = require("../../utils/isStandardSyntaxDeclaration.js");
+const isStandardSyntaxProperty = require("../../utils/isStandardSyntaxProperty.js");
+const isStandardSyntaxValue = require("../../utils/isStandardSyntaxValue.js");
+const matchesStringOrRegExp = require("../../utils/matchesStringOrRegExp.js");
+const atKeywords = require("../../utils/atKeywords.js");
+const validateObjectWithArrayProps = require("../../utils/validateObjectWithArrayProps.js");
+const { utils } = require("stylelint");
+const {
+  isDollarVar,
+  isIfStatement,
+  isNestedProperty
+} = require("../../utils/validateTypes");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+
+const ruleName = namespace("declaration-property-value-no-unknown");
+
+const messages = utils.ruleMessages(ruleName, {
+  rejected: (property, value) =>
+    `Unexpected unknown value "${value}" for property "${property}"`,
+  rejectedParseError: (property, value) =>
+    `Cannot parse property value "${value}" for property "${property}"`
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+const SYNTAX_PROPERTY = /^syntax$/i;
+
+function extractFunctionName(inputString) {
+  const matches = [...inputString.matchAll(/(?:\s*([\w\-$]+)\s*)?\(/g)].flat();
+  return matches;
+}
+
+const unsupportedFunctions = ["clamp", "min", "max", "env"];
+
+function rule(primary, secondaryOptions) {
+  return (root, result) => {
+    const validOptions = utils.validateOptions(
+      result,
+      ruleName,
+      { actual: primary },
+      {
+        actual: secondaryOptions,
+        possible: {
+          ignoreProperties: [validateObjectWithArrayProps],
+          propertiesSyntax: [isPlainObject.isPlainObject],
+          typesSyntax: [isPlainObject.isPlainObject]
+        },
+        optional: true
+      }
+    );
+
+    if (!validOptions) {
+      return;
+    }
+
+    const ignoreProperties = Array.from(
+      Object.entries(secondaryOptions?.ignoreProperties ?? {})
+    );
+
+    /** @type {(name: string, propValue: string) => boolean} */
+    const isPropIgnored = (name, value) => {
+      const [, valuePattern] =
+        ignoreProperties.find(([namePattern]) =>
+          matchesStringOrRegExp(name, namePattern)
+        ) || [];
+
+      return valuePattern && matchesStringOrRegExp(value, valuePattern);
+    };
+
+    const propertiesSyntax = {
+      // Take a shallow clone as this object will be appended to.
+      ...(secondaryOptions?.propertiesSyntax ?? {})
+    };
+    const typesSyntax = secondaryOptions?.typesSyntax ?? {};
+
+    /** @type {Map<string, string>} */
+    const typedCustomPropertyNames = new Map();
+
+    // Unless we tracked return values of declared functions, they're all valid.
+    root.walkAtRules("function", atRule => {
+      unsupportedFunctions.push(extractFunctionName(atRule.params)[1]);
+    });
+
+    root.walkAtRules(/^property$/i, atRule => {
+      const propName = atRule.params.trim();
+
+      if (!propName || !atRule.nodes || !isCustomProperty(propName)) return;
+
+      for (const node of atRule.nodes) {
+        if (typeGuards.isDeclaration(node) && SYNTAX_PROPERTY.test(node.prop)) {
+          const value = node.value.trim();
+          const unquoted = cssTree.string.decode(value);
+
+          // Only string values are valid.
+          // We can not check the syntax of this property.
+          if (unquoted === value) continue;
+
+          // Any value is allowed in this custom property.
+          // We don't need to check this property.
+          if (unquoted === "*") continue;
+
+          // https://github.com/csstree/csstree/pull/256
+          // We can circumvent this issue by prefixing the property name,
+          // making it a vendor-prefixed property instead of a custom property.
+          // No one should be using `-stylelint--` as a property prefix.
+          //
+          // When this is resolved `typedCustomPropertyNames` can become a `Set<string>`
+          // and the prefix can be removed.
+          const prefixedPropName = `-stylelint${propName}`;
+
+          typedCustomPropertyNames.set(propName, prefixedPropName);
+          propertiesSyntax[prefixedPropName] = unquoted;
+        }
+      }
+    });
+
+    const forkedLexer = cssTree.fork({
+      properties: propertiesSyntax,
+      types: typesSyntax
+    }).lexer;
+
+    root.walkDecls(decl => {
+      const { prop, value, parent } = decl;
+
+      // NOTE: CSSTree's `fork()` doesn't support `-moz-initial`, but it may be possible in the future.
+      // See https://github.com/stylelint/stylelint/pull/6511#issuecomment-1412921062
+      if (/^-moz-initial$/i.test(value)) return;
+
+      if (!isStandardSyntaxDeclaration(decl)) return;
+
+      if (!isStandardSyntaxProperty(prop)) return;
+
+      if (!isStandardSyntaxValue(value)) return;
+
+      if (isCustomProperty(prop) && !typedCustomPropertyNames.has(prop)) return;
+
+      if (isPropIgnored(prop, value)) return;
+
+      // Unless we tracked values of variables, they're all valid.
+      if (value.split(" ").some(val => isDollarVar(val))) return;
+
+      // https://github.com/mdn/data/pull/674
+      // `initial-value` has an incorrect syntax definition.
+      // In reality everything is valid.
+      if (
+        /^initial-value$/i.test(prop) &&
+        decl.parent &&
+        typeGuards.isAtRule(decl.parent) &&
+        /^property$/i.test(decl.parent.name)
+      ) {
+        return;
+      }
+
+      /** @type {import('css-tree').CssNode} */
+      let cssTreeValueNode;
+
+      try {
+        cssTreeValueNode = cssTree.parse(value, { context: "value" });
+
+        if (containsCustomFunction(cssTreeValueNode)) return;
+        if (containsUnsupportedFunction(cssTreeValueNode)) return;
+      } catch (e) {
+        const index = declarationValueIndex(decl);
+        const endIndex = index + value.length;
+
+        // Hidden declarations
+        if (isIfStatement(value)) return;
+
+        utils.report({
+          message: messages.rejectedParseError(prop, value),
+          node: decl,
+          index,
+          endIndex,
+          result,
+          ruleName
+        });
+
+        return;
+      }
+
+      const { error } =
+        parent &&
+        typeGuards.isAtRule(parent) &&
+        !atKeywords.nestingSupportedAtKeywords.has(parent.name.toLowerCase())
+          ? forkedLexer.matchAtruleDescriptor(
+              parent.name,
+              prop,
+              cssTreeValueNode
+            )
+          : forkedLexer.matchProperty(
+              typedCustomPropertyNames.get(prop) ?? prop,
+              cssTreeValueNode
+            );
+
+      if (!error) return;
+
+      if (!("mismatchLength" in error)) return;
+
+      const { mismatchLength, mismatchOffset, name, rawMessage } = error;
+
+      if (name !== "SyntaxMatchError") return;
+
+      if (rawMessage !== "Mismatch") return;
+
+      const mismatchValue = value.slice(
+        mismatchOffset,
+        mismatchOffset + mismatchLength
+      );
+      const index = declarationValueIndex(decl) + mismatchOffset;
+      const endIndex = index + mismatchLength;
+
+      utils.report({
+        message: messages.rejected(prop, mismatchValue),
+        node: decl,
+        index,
+        endIndex,
+        result,
+        ruleName
+      });
+    });
+
+    // Handle nested properties
+    root.walkRules(node => {
+      if (!isNestedProperty(node.selector)) return;
+      node.each(decl => {
+        let pointer = node;
+        let prop = String(decl.prop);
+        while (
+          pointer &&
+          pointer.selector &&
+          pointer.selector[pointer.selector.length - 1] === ":" &&
+          pointer.selector.substring(0, 2) !== "--"
+        ) {
+          prop = pointer.selector.replace(":", "") + "-" + prop;
+          pointer = pointer.parent;
+        }
+        const { error } = forkedLexer.matchProperty(prop, String(decl.value));
+
+        if (!error) return;
+
+        utils.report({
+          message: messages.rejected(prop, decl.value),
+          node,
+          result,
+          ruleName
+        });
+      });
+    });
+  };
+}
+
+/**
+ *
+ * @see csstree/csstree#164 min, max, clamp
+ * @see csstree/csstree#245 env
+ * @see https://github.com/stylelint/stylelint/pull/6511#issuecomment-1412921062
+ * @see https://github.com/stylelint/stylelint/issues/6635#issuecomment-1425787649
+ *
+ * @param {import('css-tree').CssNode} cssTreeNode
+ * @returns {boolean}
+ */
+function containsUnsupportedFunction(cssTreeNode) {
+  return Boolean(
+    cssTree.find(
+      cssTreeNode,
+      node =>
+        node.type === "Function" &&
+        ["clamp", "min", "max", "env"].includes(node.name)
+    )
+  );
+}
+
+function containsCustomFunction(cssTreeNode) {
+  return Boolean(
+    cssTree.find(
+      cssTreeNode,
+      node =>
+        node.type === "Function" && unsupportedFunctions.includes(node.name)
+    )
+  );
+}
+
+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 21a218a..b8afe6b 100644
--- a/src/rules/index.js
+++ b/src/rules/index.js
@@ -36,6 +36,7 @@ const rules = {
   "comment-no-loud": require("./comment-no-loud"),
   "declaration-nested-properties-no-divided-groups": require("./declaration-nested-properties-no-divided-groups"),
   "declaration-nested-properties": require("./declaration-nested-properties"),
+  "declaration-property-value-no-unknown": require("./declaration-property-value-no-unknown"),
   "dimension-no-non-numeric-values": require("./dimension-no-non-numeric-values"),
   "dollar-variable-colon-newline-after": require("./dollar-variable-colon-newline-after"),
   "dollar-variable-colon-space-after": require("./dollar-variable-colon-space-after"),
diff --git a/src/utils/atKeywords.js b/src/utils/atKeywords.js
new file mode 100644
index 0000000..3b5147a
--- /dev/null
+++ b/src/utils/atKeywords.js
@@ -0,0 +1,62 @@
+"use strict";
+
+const uniteSets = require("../utils/uniteSets.js");
+
+// https://www.w3.org/TR/css-nesting-1/#conditionals
+const nestingSupportedAtKeywords = new Set([
+  "container",
+  "layer",
+  "media",
+  "scope",
+  "starting-style",
+  "supports"
+]);
+
+// https://www.w3.org/TR/css-page-3/#syntax-page-selector
+const pageMarginAtKeywords = new Set([
+  "top-left-corner",
+  "top-left",
+  "top-center",
+  "top-right",
+  "top-right-corner",
+  "bottom-left-corner",
+  "bottom-left",
+  "bottom-center",
+  "bottom-right",
+  "bottom-right-corner",
+  "left-top",
+  "left-middle",
+  "left-bottom",
+  "right-top",
+  "right-middle",
+  "right-bottom"
+]);
+
+// https://developer.mozilla.org/en/docs/Web/CSS/At-rule
+const atKeywords = uniteSets(nestingSupportedAtKeywords, pageMarginAtKeywords, [
+  "annotation",
+  "apply",
+  "character-variant",
+  "charset",
+  "counter-style",
+  "custom-media",
+  "custom-selector",
+  "document",
+  "font-face",
+  "font-feature-values",
+  "import",
+  "keyframes",
+  "namespace",
+  "nest",
+  "ornaments",
+  "page",
+  "property",
+  "scroll-timeline",
+  "styleset",
+  "stylistic",
+  "swash",
+  "viewport"
+]);
+
+exports.atKeywords = atKeywords;
+exports.nestingSupportedAtKeywords = nestingSupportedAtKeywords;
diff --git a/src/utils/isStandardSyntaxValue.js b/src/utils/isStandardSyntaxValue.js
new file mode 100644
index 0000000..1371802
--- /dev/null
+++ b/src/utils/isStandardSyntaxValue.js
@@ -0,0 +1,56 @@
+// NOTICE: This file is generated by Rollup. To modify it,
+// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
+"use strict";
+
+const hasInterpolation = require("./hasInterpolation.js");
+
+/**
+ * Check whether a value is standard
+ *
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isStandardSyntaxValue(value) {
+  let normalizedValue = value;
+
+  // Ignore operators before variables (example -$variable)
+  if (/^[-+*/]/.test(value.charAt(0))) {
+    normalizedValue = normalizedValue.slice(1);
+  }
+
+  // SCSS variable (example $variable)
+  if (normalizedValue.startsWith("$")) {
+    return false;
+  }
+
+  // SCSS namespace (example namespace.$variable)
+  if (/^.+\.\$/.test(value)) {
+    return false;
+  }
+
+  // SCSS namespace (example namespace.function-name())
+  if (/^.+\.[-\w]+\(/.test(value)) {
+    return false;
+  }
+
+  // Less variable
+  if (normalizedValue.startsWith("@")) {
+    return false;
+  }
+
+  // SCSS or Less interpolation
+  if (hasInterpolation(normalizedValue)) {
+    return false;
+  }
+
+  // WebExtension replacement keyword used by Chrome/Firefox
+  // more information: https://developer.chrome.com/extensions/i18n
+  // and https://github.com/stylelint/stylelint/issues/4707
+  if (/__MSG_\S+__/.test(value)) {
+    return false;
+  }
+
+  return true;
+}
+
+module.exports = isStandardSyntaxValue;
diff --git a/src/utils/typeGuards.js b/src/utils/typeGuards.js
new file mode 100644
index 0000000..7f79d3a
--- /dev/null
+++ b/src/utils/typeGuards.js
@@ -0,0 +1,106 @@
+// NOTICE: This file is generated by Rollup. To modify it,
+// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
+"use strict";
+
+/** @typedef {import('postcss').Node} Node */
+/** @typedef {import('postcss').Source} NodeSource */
+
+/**
+ * @param {Node} node
+ * @returns {node is import('postcss').Root}
+ */
+function isRoot(node) {
+  return node.type === "root";
+}
+
+/**
+ * @param {Node} node
+ * @returns {node is import('postcss').Rule}
+ */
+function isRule(node) {
+  return node.type === "rule";
+}
+
+/**
+ * @param {Node} node
+ * @returns {node is import('postcss').AtRule}
+ */
+function isAtRule(node) {
+  return node.type === "atrule";
+}
+
+/**
+ * @param {Node} node
+ * @returns {node is import('postcss').Comment}
+ */
+function isComment(node) {
+  return node.type === "comment";
+}
+
+/**
+ * @param {Node} node
+ * @returns {node is import('postcss').Declaration}
+ */
+function isDeclaration(node) {
+  return node.type === "decl";
+}
+
+/**
+ * @param {Node} node
+ * @returns {node is import('postcss').Document}
+ */
+function isDocument(node) {
+  return node.type === "document";
+}
+
+/**
+ * @param {import('postcss-value-parser').Node} node
+ * @returns {node is import('postcss-value-parser').DivNode}
+ */
+function isValueDiv(node) {
+  return node.type === "div";
+}
+
+/**
+ * @param {import('postcss-value-parser').Node} node
+ * @returns {node is import('postcss-value-parser').FunctionNode}
+ */
+function isValueFunction(node) {
+  return node.type === "function";
+}
+
+/**
+ * @param {import('postcss-value-parser').Node} node
+ * @returns {node is import('postcss-value-parser').SpaceNode}
+ */
+function isValueSpace(node) {
+  return node.type === "space";
+}
+
+/**
+ * @param {import('postcss-value-parser').Node} node
+ * @returns {node is import('postcss-value-parser').WordNode}
+ */
+function isValueWord({ type }) {
+  return type === "word";
+}
+
+/**
+ * @param {Node} node
+ * @returns {node is (Node & {source: NodeSource})}
+ */
+function hasSource(node) {
+  return Boolean(node.source);
+}
+
+exports.hasSource = hasSource;
+exports.isAtRule = isAtRule;
+exports.isComment = isComment;
+exports.isDeclaration = isDeclaration;
+exports.isDocument = isDocument;
+exports.isRoot = isRoot;
+exports.isRule = isRule;
+exports.isValueDiv = isValueDiv;
+exports.isValueFunction = isValueFunction;
+exports.isValueSpace = isValueSpace;
+exports.isValueWord = isValueWord;
diff --git a/src/utils/uniteSets.js b/src/utils/uniteSets.js
new file mode 100644
index 0000000..722619f
--- /dev/null
+++ b/src/utils/uniteSets.js
@@ -0,0 +1,13 @@
+"use strict";
+
+/**
+ * Unite two or more sets
+ *
+ * @param {Iterable<string>[]} args
+ * @see {@link https://github.com/microsoft/TypeScript/issues/57228|GitHub}
+ */
+function uniteSets(...args) {
+  return new Set([...args].reduce((result, set) => [...result, ...set], []));
+}
+
+module.exports = uniteSets;
diff --git a/src/utils/validateObjectWithArrayProps.js b/src/utils/validateObjectWithArrayProps.js
new file mode 100644
index 0000000..2cebaa5
--- /dev/null
+++ b/src/utils/validateObjectWithArrayProps.js
@@ -0,0 +1,35 @@
+// NOTICE: This file is generated by Rollup. To modify it,
+// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
+"use strict";
+
+const isPlainObject = require("is-plain-object");
+
+/**
+ * Check whether the variable is an object and all its properties are one or more values
+ * that satisfy the specified validator(s):
+ *
+ * @example
+ * ignoreProperties = {
+ *   value1: ["item11", "item12", "item13"],
+ *   value2: "item2",
+ * };
+ * validateObjectWithArrayProps(isString)(ignoreProperties);
+ * //=> true
+ *
+ * @typedef {(value: unknown) => boolean} Validator
+ * @param {...Validator} validators
+ * @returns {Validator}
+ */
+function validateObjectWithArrayProps(...validators) {
+  return value => {
+    if (isPlainObject.isPlainObject(value) || !value) {
+      return false;
+    }
+
+    return Object.values(value)
+      .flat()
+      .every(item => validators.some(v => v(item)));
+  };
+}
+
+module.exports = validateObjectWithArrayProps;
diff --git a/src/utils/validateTypes.js b/src/utils/validateTypes.js
index 0610c75..bdc4d38 100644
--- a/src/utils/validateTypes.js
+++ b/src/utils/validateTypes.js
@@ -36,9 +36,47 @@ function isString(value) {
   return typeof value === "string" || value instanceof String;
 }
 
+/**
+ * Checks if the value is a function
+ * @param {unknown} value
+ * @returns {value is Function}
+ */
+function isFunctionCall(value) {
+  const functionCallPattern = /[a-zA-Z0-9_-]+\s*\(\s*(.*)\s*\)/g;
+  return functionCallPattern.test(value);
+}
+
+/**
+ * Checks if the value is a if statement.
+ * @param {unknown} value
+ * @returns {value is IfStatement}
+ */
+function isIfStatement(value) {
+  const ifStatementPattern = /if\s*\(\s*(.*)\s*\)/g;
+  return ifStatementPattern.test(value);
+}
+
+/**
+ * Checks if the value is a ${var-name}.
+ * @param {unknown} value
+ * @returns {value is variable}
+ */
+const isDollarVar = value => value.length > 0 && value[0] === "$";
+
+/**
+ * Checks if the selector is nested property.
+ * @param {unknown} selector
+ * @returns {selector is variable}
+ */
+const isNestedProperty = selector => selector[selector.length - 1] === ":";
+
 module.exports = {
   isBoolean,
   isNumber,
   isRegExp,
-  isString
+  isString,
+  isFunctionCall,
+  isDollarVar,
+  isIfStatement,
+  isNestedProperty
 };


Test patch:
diff --git a/src/rules/declaration-property-value-no-unknown/__tests__/index.js b/src/rules/declaration-property-value-no-unknown/__tests__/index.js
new file mode 100644
index 0000000..3471be6
--- /dev/null
+++ b/src/rules/declaration-property-value-no-unknown/__tests__/index.js
@@ -0,0 +1,326 @@
+"use strict";
+
+const { ruleName, messages } = require("..");
+
+testRule({
+  ruleName,
+  config: true,
+
+  accept: [
+    {
+      code: "a { top: 0; }"
+    },
+    {
+      code: "a { @starting-style { opacity: 0; } }"
+    },
+    {
+      code: "a { @media screen { top: 0; } }"
+    },
+    {
+      code: "a { margin: auto 10em; }"
+    },
+    {
+      code: "a { margin-inline: calc(100% - 10px); }"
+    },
+    {
+      code: "a { top: var(--foo); }"
+    },
+    {
+      code: "a { top: env(titlebar-area-height); }"
+    },
+    {
+      code: "a { foo: 1px; }"
+    },
+    {
+      code: "a { --foo: 1px; }"
+    },
+    {
+      code: "a { color: lightgrey; }"
+    },
+    {
+      code: "a { color: lightgray; }"
+    },
+    {
+      code: "a { display: -moz-inline-stack; }"
+    },
+    {
+      code: "a { color: -moz-initial; }"
+    },
+    {
+      code: "a { font-size: clamp(1rem, 1vw + 1rem, 3rem); }"
+    },
+    {
+      code: "a { font-size: min(1rem, 3rem); }"
+    },
+    {
+      code: "a { font-size: max(1rem, 3rem); }"
+    },
+    {
+      code: `
+      @function get-color() {
+        @return #336699;
+      }
+      .element {
+        color: get-color();
+      }
+    `,
+      description: "Function call as value"
+    },
+    {
+      code: `
+      $var1: 1px;
+      a {
+        margin: 2px $var1;
+      }
+    `,
+      description: "SCSS variable as value"
+    },
+    {
+      code: `
+      $rounded-corners: false;
+      .button {
+        border-radius: if($rounded-corners, 5px, null);
+      }
+    `,
+      description: "Hidden declarations as value"
+    }
+  ],
+
+  reject: [
+    {
+      code: "a { top: unknown; }",
+      message: messages.rejected("top", "unknown"),
+      line: 1,
+      column: 10,
+      endLine: 1,
+      endColumn: 17
+    },
+    {
+      code: "a { @media screen { top: unknown; } }",
+      message: messages.rejected("top", "unknown"),
+      line: 1,
+      column: 26,
+      endLine: 1,
+      endColumn: 33
+    },
+    {
+      code: "a { @starting-style { padding: auto; } }",
+      message: messages.rejected("padding", "auto"),
+      line: 1,
+      column: 32,
+      endLine: 1,
+      endColumn: 36
+    },
+    {
+      code: "a { top: red; }",
+      message: messages.rejected("top", "red"),
+      line: 1,
+      column: 10,
+      endLine: 1,
+      endColumn: 13
+    },
+    {
+      code: "a { color: ; }",
+      message: messages.rejected("color", ""),
+      line: 1,
+      column: 11,
+      endLine: 1,
+      endColumn: 12
+    },
+    {
+      code: "a { padding: auto; }",
+      message: messages.rejected("padding", "auto"),
+      line: 1,
+      column: 14,
+      endLine: 1,
+      endColumn: 18
+    },
+    {
+      code: "a { display: fixed }",
+      message: messages.rejected("display", "fixed"),
+      line: 1,
+      column: 14,
+      endLine: 1,
+      endColumn: 19
+    },
+    {
+      code: "a { color: rgb(67 67 67 70%); }",
+      message: messages.rejected("color", "70%"),
+      line: 1,
+      column: 25,
+      endLine: 1,
+      endColumn: 28
+    },
+    {
+      code: "a { transition: background-color 0.6s ease-in-out 0; }",
+      message: messages.rejected("transition", "0"),
+      line: 1,
+      column: 51,
+      endLine: 1,
+      endColumn: 52
+    },
+    {
+      code: 'a { margin-top: "10px"; }',
+      message: messages.rejected("margin-top", '"10px"'),
+      line: 1,
+      column: 17,
+      endLine: 1,
+      endColumn: 23
+    },
+    {
+      code: "a { color: greem; }",
+      message: messages.rejected("color", "greem"),
+      line: 1,
+      column: 12,
+      endLine: 1,
+      endColumn: 17
+    },
+    {
+      code: "a { filter: alpha(opacity=30); }",
+      message: messages.rejectedParseError("filter", "alpha(opacity=30)"),
+      line: 1,
+      column: 13,
+      endLine: 1,
+      endColumn: 30
+    },
+    {
+      code: `
+      a {
+        color: red;
+        font: {
+          size: red;
+        }
+      }
+    `,
+      message: messages.rejected("font-size", "red"),
+      line: 4,
+      column: 9,
+      endLine: 6,
+      endColumn: 10
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [
+    true,
+    {
+      ignoreProperties: {
+        top: "/.+/",
+        padding: ["auto"],
+        "/.+/": /--foo/
+      }
+    }
+  ],
+
+  accept: [
+    {
+      code: "a { top: 0; }"
+    },
+    {
+      code: "a { top: red; }"
+    },
+    {
+      code: "a { padding: auto; }"
+    },
+    {
+      code: "a { margin: 10px --foo; }"
+    }
+  ],
+
+  reject: [
+    {
+      code: "a { color: 1; }",
+      message: messages.rejected("color", "1"),
+      line: 1,
+      column: 12,
+      endLine: 1,
+      endColumn: 13
+    },
+    {
+      code: "a { padding-left: auto; }",
+      message: messages.rejected("padding-left", "auto"),
+      line: 1,
+      column: 19,
+      endLine: 1,
+      endColumn: 23
+    },
+    {
+      code: "a { padding: 10px --bar; }",
+      message: messages.rejected("padding", "--bar"),
+      line: 1,
+      column: 19,
+      endLine: 1,
+      endColumn: 24
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [true, { propertiesSyntax: { size: "<length-percentage>" } }],
+
+  accept: [
+    {
+      code: "a { size: 10px; }"
+    }
+  ],
+
+  reject: [
+    {
+      code: "a { size: red; }",
+      message: messages.rejected("size", "red"),
+      line: 1,
+      column: 11,
+      endLine: 1,
+      endColumn: 14
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: [
+    true,
+    {
+      propertiesSyntax: { top: "| <--foo()>" },
+      typesSyntax: { "--foo()": "--foo( <length-percentage> )" }
+    }
+  ],
+
+  accept: [
+    {
+      code: "a { top: 10px; }"
+    },
+    {
+      code: "a { top: --foo(5px); }"
+    }
+  ],
+
+  reject: [
+    {
+      code: "a { top: red; }",
+      message: messages.rejected("top", "red"),
+      line: 1,
+      column: 10,
+      endLine: 1,
+      endColumn: 13
+    }
+  ]
+});
+
+testRule({
+  ruleName,
+  config: true,
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: "a { $foo: bar; }"
+    },
+    {
+      code: "a { top: #{foo}; }"
+    }
+  ]
+});