From 6e6db1376fd60a4a96ebf8c4ae78b8cbe3fd0ce9 Mon Sep 17 00:00:00 2001 From: Pamela Lozano Date: Tue, 29 Aug 2023 14:18:30 -0700 Subject: [PATCH] Add function-no-interpolation rule --- CHANGELOG.md | 6 ++ README.md | 5 +- src/rules/function-no-interpolation/README.md | 64 +++++++++++++++ .../__tests__/index.js | 82 +++++++++++++++++++ src/rules/function-no-interpolation/index.js | 56 +++++++++++++ src/rules/index.js | 1 + 6 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 src/rules/function-no-interpolation/README.md create mode 100644 src/rules/function-no-interpolation/__tests__/index.js create mode 100644 src/rules/function-no-interpolation/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e07e624b..ad44fd18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 5.2.0 + +- Added: `function-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: `function-disallowed-list` rule support to ban specific built-in functions (#422, #844). diff --git a/README.md b/README.md index dee7946a..9697c556 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, @@ -145,6 +143,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-no-interpolation`](./src/rules/function-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/function-no-interpolation/README.md b/src/rules/function-no-interpolation/README.md new file mode 100644 index 00000000..20b505f7 --- /dev/null +++ b/src/rules/function-no-interpolation/README.md @@ -0,0 +1,64 @@ +# function-no-interpolation + +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 forbids interpolation in `calc()`, `clamp()`, `min()`, and `max()` +functions to avoid extra verbose or even invalid CSS. + + +```scss +.a { .b: calc(#{$c} + 1); } +/** ↑ + * This argument */ +``` + +## Options + +### `true` + +The following patterns are considered warnings: + + +```scss +$c: 1; +.a { .b: calc(#{$c + 1}); } +``` + + +```scss +$c: 1; +.a { .b: calc(max(#{$c})); } +``` + + +```scss +$c: 1; +.a { .b: min(#{$c}); } +``` + + +```scss +$c: 1; +.a { .b: clamp(#{$c} + 2px); } +``` + +The following patterns are _not_ considered warnings: + + +```scss +.a { .b: calc(1 + 1); } +``` + + +```scss +$c: 1; +.a { .b: abc(#{$c} + 1px); } +``` + + +```scss +$c: 1; +.a { .b: calc(abc(#{$c})); } +``` diff --git a/src/rules/function-no-interpolation/__tests__/index.js b/src/rules/function-no-interpolation/__tests__/index.js new file mode 100644 index 00000000..0cfa45ac --- /dev/null +++ b/src/rules/function-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: "`max` 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/function-no-interpolation/index.js b/src/rules/function-no-interpolation/index.js new file mode 100644 index 00000000..75a6c221 --- /dev/null +++ b/src/rules/function-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-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 fcdbe995..659da98c 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -43,6 +43,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-no-interpolation": require("./function-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"),