From 4251f552199da7da0e63a38decc2b0786f7deffe Mon Sep 17 00:00:00 2001 From: Pamela Lozano De La Garza Date: Wed, 2 Aug 2023 18:41:49 -0700 Subject: [PATCH] Add function-disallowed-list rule (stylelint-scss#422) --- CHANGELOG.md | 4 + src/rules/function-disallowed-list/README.md | 60 ++++++++++++++ .../__tests__/index.js | 78 +++++++++++++++++++ src/rules/function-disallowed-list/index.js | 69 ++++++++++++++++ src/rules/index.js | 1 + 5 files changed, 212 insertions(+) create mode 100644 src/rules/function-disallowed-list/README.md create mode 100644 src/rules/function-disallowed-list/__tests__/index.js create mode 100644 src/rules/function-disallowed-list/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e75655aa..6c6b7bae 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 00000000..38066df0 --- /dev/null +++ b/src/rules/function-disallowed-list/README.md @@ -0,0 +1,60 @@ +# function-disallowed-list + +Specify a list of disallowed functions. + + +```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 +["math.random", "double"] +``` + +The following patterns are considered warnings: + + +```scss +a { margin-left: math.random(100); } +``` + + +```scss +@function double($num) {@return $num * 2;} +a { + margin-left: double($num); +} +``` + +The following patterns are _not_ considered warnings: + + +```scss +a { margin-left: math.abs(100); } +``` + + +```scss +@function timesTwo($num) {@return $num * 2;} +a { + margin-left: timesTwo($num); +} +``` + + +```scss +@function random($num) {@return $num;} +a { + margin-left: random($num); +} +``` 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 00000000..2cab00d9 --- /dev/null +++ b/src/rules/function-disallowed-list/__tests__/index.js @@ -0,0 +1,78 @@ +"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)." + } + ] +}); diff --git a/src/rules/function-disallowed-list/index.js b/src/rules/function-disallowed-list/index.js new file mode 100644 index 00000000..fd9cb9c4 --- /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 7d99314b..fcdbe995 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"),