From 0f3126941caec4d91601d1078c54b022d2f8ada4 Mon Sep 17 00:00:00 2001 From: Krister Kari Date: Sun, 3 Mar 2024 01:57:28 +0200 Subject: [PATCH] dollar-variable-pattern: add support for functions and mixins --- .../__tests__/index.js | 388 ++++++++++++++++++ src/rules/dollar-variable-pattern/index.js | 91 +++- src/utils/__tests__/parseFunctionArguments.js | 10 + 3 files changed, 482 insertions(+), 7 deletions(-) diff --git a/src/rules/dollar-variable-pattern/__tests__/index.js b/src/rules/dollar-variable-pattern/__tests__/index.js index a997d8db..926f4aa7 100644 --- a/src/rules/dollar-variable-pattern/__tests__/index.js +++ b/src/rules/dollar-variable-pattern/__tests__/index.js @@ -264,10 +264,56 @@ testRule({ code: "a { $oo-bar: 0; }", description: "Ignore local variable (not matching the pattern)." }, + { + code: "a { font-size: myFontSize($oo-bar: 4px); }", + description: + "Ignore local variable (function call argument, not matching the pattern)." + }, + { + code: "a { @include myFontSize($oo-bar: 4px); }", + description: + "Ignore local variable (include call with keyword argument, not matching the pattern)." + }, { code: "$Foo-barBaz: 0;", description: "Ignore local variable (passing global, matching the pattern)." + }, + { + code: ` + @mixin myFontSize($oo-bar) { + font-size: $oo-bar; + } + `, + description: + "Ignore local variable (mixin argument, not matching the pattern)" + }, + { + code: ` + @mixin myFontSize($oo-bar...) { + font-size: $oo-bar; + } + `, + description: + "Ignore local variable (mixin arbitrary arguments, not matching the pattern)" + }, + { + code: ` + @function myFontSize($oo-bar) { + font-size: $oo-bar; + } + `, + description: + "Ignore local variable (function argument, not matching the pattern)" + }, + { + code: ` + @function myFontSize($oo-bar...) { + font-size: $oo-bar; + } + `, + description: + "Ignore local variable (function arbitrary arguments, not matching the pattern)" } ], @@ -275,9 +321,41 @@ testRule({ { code: "$boo-Foo-bar: 0;", line: 1, + endLine: 1, + endColumn: 13, message: messages.expected, description: "Ignore local variable (passing global, not matching the pattern)." + }, + { + code: `font-size: myFontSize($oo-bar: 4px);`, + description: + "Ignore local variable (function call with keyword argument, global, not matching the pattern)", + message: messages.expected, + line: 1, + column: 23, + endLine: 1, + endColumn: 30 + }, + { + code: `font-size: myFontSize($oo-bar...);`, + description: + "Ignore local variable (function call with arbitrary arguments, global, not matching the pattern)", + message: messages.expected, + line: 1, + column: 23, + endLine: 1, + endColumn: 33 + }, + { + code: `@include myFontSize($oo-bar: 4px);`, + description: + "Ignore local variable (include call with keyword argument, global, not matching the pattern)", + message: messages.expected, + line: 1, + column: 21, + endLine: 1, + endColumn: 28 } ] }); @@ -296,6 +374,44 @@ testRule({ code: "a { $Foo-barBaz: 0; }", description: "Ignore global variable (passing local, matching the pattern)." + }, + { + code: ` + @mixin myFontSize($Foo-barBaz) { + font-size: $Foo-barBaz; + } + `, + description: + "Ignore global variable (mixin with argument, local, matching the pattern)" + }, + { + code: ` + @function myFontSize($Foo-barBaz) { + font-size: $Foo-barBaz; + } + `, + description: + "Ignore global variable (function with argument, local, matching the pattern)" + }, + { + code: `@include myFontSize($oo-bar: 4px);`, + description: + "Ignore global variable (include call with keyword argument, global, not matching the pattern)" + }, + { + code: `@include myFontSize($Foo-barBaz: 4px);`, + description: + "Ignore global variable (include call with keyword argument, global, matching the pattern)" + }, + { + code: `font-size: myFontSize($oo-bar: 4px);`, + description: + "Ignore global variable (function call with keyword argument, global, not matching the pattern)" + }, + { + code: `font-size: myFontSize($Foo-barBaz: 4px);`, + description: + "Ignore global variable (function call with keyword argument, global, matching the pattern)" } ], @@ -306,6 +422,278 @@ testRule({ message: messages.expected, description: "Ignore global variable (passing local, not matching the pattern)." + }, + { + code: ` + @mixin myFontSize($oo-bar) { + font-size: $oo-bar; + } + `, + description: + "Ignore global variable (mixin with argument, not matching the pattern)", + message: messages.expected, + line: 2, + column: 25, + endLine: 2, + endColumn: 32 + }, + { + code: ` + @function myFontSize($oo-bar) { + font-size: $oo-bar; + } + `, + description: + "Ignore global variable (function with argument, not matching the pattern)", + message: messages.expected, + line: 2, + column: 28, + endLine: 2, + endColumn: 35 + }, + { + code: `div { @include myFontSize($oo-bar: 4px) };`, + description: + "Ignore global variable (include call with keyword argument, local, not matching the pattern)", + message: messages.expected, + line: 1, + column: 27, + endLine: 1, + endColumn: 34 + }, + { + code: `div { font-size: myFontSize($oo-bar: 4px); }`, + description: + "Ignore global variable (function call with keyword argument, local, not matching the pattern)", + message: messages.expected, + line: 1, + column: 29, + endLine: 1, + endColumn: 36 + }, + { + code: `div { font-size: myFontSize($oo-bar...); }`, + description: + "Ignore global variable (function call with arbitrary arguments, local, not matching the pattern)", + message: messages.expected, + line: 1, + column: 29, + endLine: 1, + endColumn: 39 + } + ] +}); + +testRule({ + ruleName, + config: [/^[a-z]+([A-Z][a-z]+)+$/], + customSyntax: "postcss-scss", + + accept: [ + { + code: `$fontSize: 1;`, + description: "Sass variable (valid pattern)" + }, + { + code: ` + @mixin myFontSize($fontSize) { + font-size: $fontSize; + } + `, + description: "mixin with argument (valid pattern)" + }, + { + code: ` + @mixin myFontSize($fontSize: 1) { + font-size: $fontSize; + } + `, + description: "mixin with optional argument (valid pattern)" + }, + { + code: ` + @mixin myFontSize($mySizes...) {} + `, + description: "mixin with arbitrary arguments (valid pattern)" + }, + { + code: ` + @mixin myFontSize($fontSize, $mySizes...) {} + `, + description: + "mixin with arbitrary arguments as the second argument (valid pattern)" + }, + { + code: ` + @function myFontSize($fontSize) { + font-size: $fontSize; + } + `, + description: "function with argument (valid pattern)" + }, + { + code: ` + @function myFontSize($fontSize: 1) { + font-size: $fontSize; + } + `, + description: "function with optional argument (valid pattern)" + }, + { + code: ` + @function myFontSize($mySizes...) {} + `, + description: "function with arbitrary arguments (valid pattern)" + }, + { + code: ` + @include myFontSize($fontSize: 4px); + `, + description: "include call with keyword argument (valid pattern)" + }, + { + code: `div { font-size: myFontSize($fontSize: 4px); }`, + description: "function call with keyword argument (valid pattern)" + }, + { + code: `div { font-size: myFontSize($mySizes...); }`, + description: "function call with arbitrary arguments (valid pattern)" + } + ], + + reject: [ + { + code: `$font-size: 1;`, + description: "Sass variable (invalid pattern)", + message: messages.expected, + line: 1, + column: 1, + endLine: 1, + endColumn: 11 + }, + { + code: ` + @mixin my-font-size($font-size) { + font-size: $font-size; + } + `, + description: "mixin with argument (invalid pattern)", + message: messages.expected, + line: 2, + column: 27, + endLine: 2, + endColumn: 37 + }, + { + code: ` + @mixin my-font-size($font-size: 1) { + font-size: $font-size; + } + `, + description: "mixin with optional argument (invalid pattern)", + message: messages.expected, + line: 2, + column: 27, + endLine: 2, + endColumn: 37 + }, + { + code: ` + @mixin myFontSize($my-sizes...) {} + `, + description: "mixin with arbitrary arguments (invalid pattern)", + message: messages.expected, + line: 2, + column: 25, + endLine: 2, + endColumn: 37 + }, + { + code: ` + @mixin myFontSize($font-size, $my-sizes...) {} + `, + description: + "mixin with arbitrary arguments as the second argument (2 warnings, invalid pattern)", + warnings: [ + { + message: messages.expected, + line: 2, + column: 25, + endLine: 2, + endColumn: 35 + }, + { + message: messages.expected, + line: 2, + column: 37, + endLine: 2, + endColumn: 49 + } + ] + }, + { + code: ` + @function my-font-size($font-size) { + font-size: $font-size; + } + `, + description: "function with argument (invalid pattern)", + message: messages.expected, + line: 2, + column: 30, + endLine: 2, + endColumn: 40 + }, + { + code: ` + @function my-font-size($font-size: 1) { + font-size: $font-size; + } + `, + description: "function with optional argument (invalid pattern)", + message: messages.expected, + line: 2, + column: 30, + endLine: 2, + endColumn: 40 + }, + { + code: ` + @function myFontSize($my-sizes...) {} + `, + description: "function with arbitrary arguments (invalid pattern)", + message: messages.expected, + line: 2, + column: 28, + endLine: 2, + endColumn: 40 + }, + { + code: `@include myFontSize($font-size: 4px);`, + description: "include call with keyword argument (invalid pattern)", + message: messages.expected, + line: 1, + column: 21, + endLine: 1, + endColumn: 31 + }, + { + code: `div { font-size: myFontSize($font-size: 4px); }`, + description: "function call with keyword argument (invalid pattern)", + message: messages.expected, + line: 1, + column: 29, + endLine: 1, + endColumn: 39 + }, + { + code: `div { font-size: myFontSize($my-sizes...); }`, + description: "function call with arbitrary arguments (invalid pattern)", + message: messages.expected, + line: 1, + column: 29, + endLine: 1, + endColumn: 41 } ] }); diff --git a/src/rules/dollar-variable-pattern/index.js b/src/rules/dollar-variable-pattern/index.js index f4ecdea3..fc945b59 100644 --- a/src/rules/dollar-variable-pattern/index.js +++ b/src/rules/dollar-variable-pattern/index.js @@ -5,6 +5,10 @@ const { isRegExp, isString } = require("../../utils/validateTypes"); const namespace = require("../../utils/namespace"); const optionsHaveIgnored = require("../../utils/optionsHaveIgnored"); const ruleUrl = require("../../utils/ruleUrl"); +const { + parseFunctionArguments +} = require("../../utils/parseFunctionArguments"); +const valueParser = require("postcss-value-parser"); const ruleName = namespace("dollar-variable-pattern"); @@ -16,6 +20,14 @@ const meta = { url: ruleUrl(ruleName) }; +function ignoreLocal(options, node) { + return optionsHaveIgnored(options, "local") && node.parent.type !== "root"; +} + +function ignoreGlobal(options, node) { + return optionsHaveIgnored(options, "global") && node.parent.type === "root"; +} + function rule(pattern, options) { return (root, result) => { const validOptions = utils.validateOptions( @@ -40,19 +52,51 @@ function rule(pattern, options) { const regexpPattern = isString(pattern) ? new RegExp(pattern) : pattern; + function checkFunctionArgs(args, node) { + args.forEach(arg => { + const invalidOptionalArg = + arg.value && arg.key && !regexpPattern.test(arg.key.slice(1)); + + if (invalidOptionalArg) { + utils.report({ + message: messages.expected, + node, + result, + ruleName, + word: arg.key + }); + } + + const arbitraryArgumentRegex = /\.\.\.$/; + const invalidNormalArg = + arg.value && + !arg.key && + !regexpPattern.test( + arg.value.replace(arbitraryArgumentRegex, "").slice(1) + ); + + if (invalidNormalArg) { + utils.report({ + message: messages.expected, + node, + result, + ruleName, + word: arg.value + }); + } + }); + } + + // variables root.walkDecls(decl => { const { prop } = decl; + const isVar = prop[0] === "$"; - if (prop[0] !== "$") { + if (!isVar) { return; } - // If local or global variables need to be ignored - if ( - (optionsHaveIgnored(options, "global") && - decl.parent.type === "root") || - (optionsHaveIgnored(options, "local") && decl.parent.type !== "root") - ) { + if (ignoreGlobal(options, decl) || ignoreLocal(options, decl)) { return; } @@ -68,6 +112,39 @@ function rule(pattern, options) { word: prop }); }); + + // function calls + root.walkDecls(decl => { + if (ignoreGlobal(options, decl) || ignoreLocal(options, decl)) { + return; + } + + valueParser(decl.value).walk(node => { + if (node.type !== "function" || node.value.trim() === "") { + return; + } + + checkFunctionArgs(parseFunctionArguments(decl.value), decl); + }); + }); + + // @include calls + root.walkAtRules("include", atRule => { + if (ignoreGlobal(options, atRule) || ignoreLocal(options, atRule)) { + return; + } + + checkFunctionArgs(parseFunctionArguments(atRule.params), atRule); + }); + + // @function and @mixin + root.walkAtRules(/function|mixin/, atRule => { + if (optionsHaveIgnored(options, "local")) { + return; + } + + checkFunctionArgs(parseFunctionArguments(atRule.params), atRule); + }); }; } diff --git a/src/utils/__tests__/parseFunctionArguments.js b/src/utils/__tests__/parseFunctionArguments.js index ae16df19..a1d71d90 100644 --- a/src/utils/__tests__/parseFunctionArguments.js +++ b/src/utils/__tests__/parseFunctionArguments.js @@ -273,6 +273,16 @@ describe("parseFunctionArguments", () => { ]); }); + it("parses arbitrary arguments", () => { + expect(parseFunctionArguments("func($args...)")).toEqual([ + { + value: "$args...", + index: 5, + endIndex: 13 + } + ]); + }); + it("parses 2 key value parameters", () => { expect(parseFunctionArguments("func($var: 1, $foo: bar)")).toEqual([ {