From bae80925633978ac7e2489bc1f4143555f526248 Mon Sep 17 00:00:00 2001 From: Masafumi Koba <473530+ybiquitous@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:23:39 +0900 Subject: [PATCH] Fix `function-no-unknown` false negatives for functions with namespace Since Stylelint 15.8.0, the built-in `function-no-unknown` rule has ignored SCSS functions with namespace. For example, the following code unexpectedly passes with Stylelint 15.8.0: ```scss a { color: color.unknown(#fff); } ``` Notes: - This change keeps backward compatibility. This means not to change `peerDependencies.stylelint` in `package.json`. - This change may bring performance penalty to keep backward compatibility. - Run `npm i 'stylelint@^15.8.0' && npm t` to test this change with newer versions of Stylelint. See also: - https://github.com/stylelint/stylelint/releases/tag/15.8.0 - https://github.com/stylelint/stylelint/pull/6921 --- .../function-no-unknown/__tests__/index.js | 6 +- src/rules/function-no-unknown/index.js | 92 ++++++++++++------- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/rules/function-no-unknown/__tests__/index.js b/src/rules/function-no-unknown/__tests__/index.js index 57b49b94..52acc62a 100644 --- a/src/rules/function-no-unknown/__tests__/index.js +++ b/src/rules/function-no-unknown/__tests__/index.js @@ -111,7 +111,7 @@ testRule({ code: "a { color: color.unknown(#6b717f, $red: 15); }", message: messages.rejected("color.unknown"), line: 1, - column: 18 + column: 12 }, { code: ` @@ -123,7 +123,7 @@ testRule({ `, message: messages.rejected("othermodule.myfunction"), line: 5, - column: 31, + column: 19, description: "non-matching @use namespace" }, { @@ -136,7 +136,7 @@ testRule({ `, message: messages.rejected("c.myfunction"), line: 5, - column: 21, + column: 19, description: "non-matching @use namespace, 'as' keyword" }, { diff --git a/src/rules/function-no-unknown/index.js b/src/rules/function-no-unknown/index.js index 97a17dfb..33c25b01 100644 --- a/src/rules/function-no-unknown/index.js +++ b/src/rules/function-no-unknown/index.js @@ -23,9 +23,9 @@ const meta = { url: ruleUrl(ruleName) }; -function isNamespacedFunction(fn) { - const namespacedFunc = /^\w+\.\w+$/; - return namespacedFunc.test(fn); +function extractNamespaceFromFunction(fn) { + const matched = fn.match(/^(\w+)\.\w+$/); + return matched ? matched[1] : undefined; } function isAtUseAsSyntax(nodes) { @@ -78,6 +78,14 @@ function rule(primaryOption, secondaryOptions) { ignoreFunctions }); + const atUseNamespaces = new Set(); + root.walkAtRules(/^use$/i, atRule => { + const { nodes } = valueParser(atRule.params); + atUseNamespaces.add(getAtUseNamespace(nodes)); + }); + + const namespaceWarnings = new Set(); + utils.checkAgainstRule( { ruleName: ruleToCheckAgainst, @@ -85,44 +93,60 @@ function rule(primaryOption, secondaryOptions) { root }, warning => { - const { node, index } = warning; + const { node: decl } = warning; // NOTE: Using `valueParser` is necessary for extracting a function name. This may be a performance waste. - valueParser(node.value).walk(valueNode => { + valueParser(decl.value).walk(valueNode => { const { type, value: funcName } = valueNode; - if (type !== "function" || funcName.trim() === "") { - return; - } - - if (isNamespacedFunction(funcName)) { - const atUseNamespaces = []; - - root.walkAtRules(/^use$/i, atRule => { - const { nodes } = valueParser(atRule.params); - atUseNamespaces.push(getAtUseNamespace(nodes)); - }); - - if (atUseNamespaces.length) { - const [namespace] = funcName.split("."); - if (atUseNamespaces.includes(namespace)) { - return; - } - } - } - - if (!ignoreFunctionsAsSet.has(funcName)) { - utils.report({ - message: messages.rejected(funcName), - ruleName, - result, - node, - index - }); - } + if (type !== "function" || funcName.trim() === "") return; + + // TODO: For backward compatibility with Stylelint 15.7.0 or less. + // We can remove this code when dropping support for old version. + const namespace = extractNamespaceFromFunction(funcName); + if (namespace && atUseNamespaces.has(namespace)) return; + + if (ignoreFunctionsAsSet.has(funcName)) return; + + utils.report({ + message: messages.rejected(funcName), + ruleName, + result, + node: decl, + word: funcName + }); + + namespaceWarnings.add(warning); }); } ); + + // NOTE: Since Stylelint 15.8.0, the built-in `function-no-unknown` rule has ignored SCSS functions with namespace. + // See https://github.com/stylelint/stylelint/releases/tag/15.8.0 + // See https://github.com/stylelint/stylelint/pull/6921 + if (namespaceWarnings.size === 0) { + root.walkDecls(decl => { + valueParser(decl.value).walk(valueNode => { + const { type, value: funcName } = valueNode; + + if (type !== "function" || funcName.trim() === "") return; + + const namespace = extractNamespaceFromFunction(funcName); + if (!namespace) return; + if (atUseNamespaces.has(namespace)) return; + + if (ignoreFunctionsAsSet.has(funcName)) return; + + utils.report({ + message: messages.rejected(funcName), + ruleName, + result, + node: decl, + word: funcName + }); + }); + }); + } }; }