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

Issue:
Add a lint to ban specific built-in functions
The random() function exists primarily for Sass demos to produce cool effects—it's pretty much always a bad idea to use in real stylesheets. We should add an (off-by-default) lint to forbid this function, whether from the global namespace or from `sass:math`.

cc @rambleraptor 

Pull request description:
Add function-disallowed-list rule
Fix #422 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e75655a..6c6b7ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 5.0.2
+
+- Added: `function-disallowed-list` rule support to ban specific built-in functions (#422).
+
 # 5.0.1
 
 - Fixed: `function-no-unknown` ignore empty function names (#826).
diff --git a/src/rules/function-disallowed-list/README.md b/src/rules/function-disallowed-list/README.md
new file mode 100644
index 0000000..8e7239e
--- /dev/null
+++ b/src/rules/function-disallowed-list/README.md
@@ -0,0 +1,60 @@
+# function-disallowed-list
+
+Specify a list of disallowed functions.
+
+<!-- prettier-ignore -->
+```css
+a { margin-left: math.random(100); }
+/**                   ↑
+              * This function */
+```
+
+## Options
+
+`array|string|regex`: `["array", "of", "unprefixed", /functions/, "regex"]|"function"|"/regex/"|/regex/`
+
+If a string is surrounded with `"/"` (e.g. `"/^rgb/"`), it is interpreted as a regular expression.
+
+Given:
+
+```json
+["random", /double/]
+```
+
+The following patterns are considered warnings:
+
+<!-- prettier-ignore -->
+```scss
+a { margin-left: math.random(100); }
+```
+
+<!-- prettier-ignore -->
+```scss
+@function double($num) {@return $num * 2;}
+a {
+  margin-left: double($num);
+}
+```
+
+<!-- prettier-ignore -->
+```scss
+@function random($num) {@return $num;}
+a {
+  margin-left: random($num);
+}
+```
+
+The following patterns are _not_ considered warnings:
+
+<!-- prettier-ignore -->
+```scss
+a { margin-left: math.abs(100); }
+```
+
+<!-- prettier-ignore -->
+```scss
+@function timesTwo($num) {@return $num * 2;}
+a {
+  margin-left: timesTwo($num);
+}
+```
diff --git a/src/rules/function-disallowed-list/index.js b/src/rules/function-disallowed-list/index.js
new file mode 100644
index 0000000..fd9cb9c
--- /dev/null
+++ b/src/rules/function-disallowed-list/index.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const { utils } = require("stylelint");
+const { isRegExp, isString } = require("../../utils/validateTypes");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+const valueParser = require("postcss-value-parser");
+const isNativeCssFunction = require("../../utils/isNativeCssFunction");
+
+const ruleName = namespace("function-disallowed-list");
+
+const messages = utils.ruleMessages(ruleName, {
+  rejected: func => `Unexpected function "${func}"`
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+function rule(disallowedOption) {
+  const disallowedFunctions = [].concat(disallowedOption);
+
+  return (root, result) => {
+    const validOptions = utils.validateOptions(result, ruleName, {
+      actual: disallowedOption,
+      possible: [isString, isRegExp]
+    });
+
+    if (!validOptions) {
+      return;
+    }
+
+    root.walkDecls(decl => {
+      valueParser(decl.value).walk(node => {
+        if (
+          node.type !== "function" ||
+          isNativeCssFunction(node.value) ||
+          node.value === ""
+        ) {
+          return;
+        }
+
+        const hasNamespace = node.value.indexOf(".");
+        const nameWithoutNamespace =
+          hasNamespace > -1 ? node.value.slice(hasNamespace + 1) : node.value;
+        disallowedFunctions.forEach(functionName => {
+          if (
+            (isString(functionName) && nameWithoutNamespace === functionName) ||
+            (isRegExp(functionName) && nameWithoutNamespace.match(functionName))
+          ) {
+            utils.report({
+              message: messages.rejected(nameWithoutNamespace),
+              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 7d99314..fcdbe99 100644
--- a/src/rules/index.js
+++ b/src/rules/index.js
@@ -42,6 +42,7 @@ const rules = {
   "double-slash-comment-empty-line-before": require("./double-slash-comment-empty-line-before"),
   "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-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"),


Test patch:
diff --git a/src/rules/function-disallowed-list/__tests__/index.js b/src/rules/function-disallowed-list/__tests__/index.js
new file mode 100644
index 0000000..8913524
--- /dev/null
+++ b/src/rules/function-disallowed-list/__tests__/index.js
@@ -0,0 +1,88 @@
+"use strict";
+
+const { messages, ruleName } = require("..");
+
+// Testing single value
+testRule({
+  ruleName,
+  config: ["random"],
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: `
+        @use "sass:math"
+        .a { margin-left: math.round(100.5); }
+      `,
+      description: "Math library function, allowed."
+    }
+  ],
+  reject: [
+    {
+      code: `
+            @use "sass:math"
+            .a { margin-left: math.random(100); }
+          `,
+      line: 3,
+      column: 18,
+      message: messages.rejected("random"),
+      description: "Math library function, not allowed."
+    }
+  ]
+});
+
+// Testing an array
+testRule({
+  ruleName,
+  config: [["random", /test/]],
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: `
+      @use "sass:math"
+      .a { margin-left: math.round(100.5); }
+    `,
+      description: "Math library function, allowed."
+    },
+    {
+      code: `
+          @function funcName($num){@return $num;}
+          .a { margin-left: funcName(3); }
+        `,
+      description: "Declared function is allowed."
+    }
+  ],
+  reject: [
+    {
+      code: `
+          @use "sass:math"
+          .a { margin-left: math.random(100); }
+        `,
+      line: 3,
+      column: 16,
+      message: messages.rejected("random"),
+      description: "Math library function, not allowed."
+    },
+    {
+      code: `
+        @function test($num){@return $num;}
+        .a { margin-left: test(3); }
+      `,
+      line: 3,
+      column: 14,
+      message: messages.rejected("test"),
+      description: "Declared function is not allowed (regex)."
+    },
+    {
+      code: `
+        @function random($num){@return $num;}
+        .a { margin-left: random(5); }
+      `,
+      line: 3,
+      column: 14,
+      message: messages.rejected("random"),
+      description: "Declared function is not allowed."
+    }
+  ]
+});