diff --git a/src/rules/function-unquote-no-unquoted-strings-inside/README.md b/src/rules/function-unquote-no-unquoted-strings-inside/README.md new file mode 100644 index 00000000..1db35354 --- /dev/null +++ b/src/rules/function-unquote-no-unquoted-strings-inside/README.md @@ -0,0 +1,46 @@ +# function-unquote-no-unquoted-strings-inside + +Disallow unquoted strings inside the [unquote function](https://sass-lang.com/documentation/functions/string#unquote) + +```scss +p { + font-family: unquote(Helvetica); + /** ↑ ↑ + * This function call is unnecessary + */ +} +``` + +## Options + +### `true` + +The following patterns are considered violations: + +```scss +a { + font-family: unquote(Helvetica); +} +``` + +```scss +$font: Helvetica; +p { + font-family: quote($font); +} +``` + +The following patterns are _not_ considered violations: + +```scss +a { + color: unquote("blue"); +} +``` + +```scss +$font: "Helvetica"; +p { + font-family: unquote($font); +} +``` diff --git a/src/rules/function-unquote-no-unquoted-strings-inside/__tests__/index.js b/src/rules/function-unquote-no-unquoted-strings-inside/__tests__/index.js new file mode 100644 index 00000000..5404c8be --- /dev/null +++ b/src/rules/function-unquote-no-unquoted-strings-inside/__tests__/index.js @@ -0,0 +1,64 @@ +import rule, { ruleName, messages } from ".."; + +// always-intermediate +testRule(rule, { + ruleName, + config: [true], + syntax: "scss", + fix: true, + + accept: [ + { + code: ` + p { + font-family: unquote("Helvetica"); + } + `, + description: "accepts strings with quotes" + }, + { + code: ` + $font: "Helvetica"; + p { + font-family: unquote($font); + } + `, + description: "accepts variables representing strings that are quoted." + } + ], + + reject: [ + { + code: ` + p { + font-family: unquote(Helvetica); + } + `, + description: "does not accept strings without quotes", + message: messages.rejected, + line: 3, + fixed: ` + p { + font-family: Helvetica; + } + ` + }, + { + code: ` + $font: Helvetica; + p { + font-family: unquote($font); + } + `, + description: + "does not accept variables representing strings that are quoted.", + line: 4, + fixed: ` + $font: Helvetica; + p { + font-family: $font; + } + ` + } + ] +}); diff --git a/src/rules/function-unquote-no-unquoted-strings-inside/index.js b/src/rules/function-unquote-no-unquoted-strings-inside/index.js new file mode 100644 index 00000000..a031a637 --- /dev/null +++ b/src/rules/function-unquote-no-unquoted-strings-inside/index.js @@ -0,0 +1,76 @@ +import { utils } from "stylelint"; +import { namespace, isNativeCssFunction } from "../../utils"; +import valueParser from "postcss-value-parser"; + +export const ruleName = namespace( + "function-unquote-no-unquoted-strings-inside" +); + +export const messages = utils.ruleMessages(ruleName, { + rejected: "Unquote function used with an already-unquoted string" +}); + +function rule(primary, _, context) { + return (root, result) => { + const validOptions = utils.validateOptions(result, ruleName, { + actual: primary + }); + + if (!validOptions) { + return; + } + + // Setup variable naming. + const vars = {}; + + root.walkDecls(decl => { + if (decl.prop[0] !== "$") { + return; + } + + valueParser(decl.value).walk(node => { + vars[decl.prop] = node.type; + }); + }); + + root.walkDecls(decl => { + valueParser(decl.value).walk(node => { + // Verify that we're only looking at functions. + if ( + node.type !== "function" || + isNativeCssFunction(node.value) || + node.value === "" + ) { + return; + } + + // Verify we're only looking at quote() calls. + if (node.value !== "unquote") { + return; + } + + // Report error if first character is a quote. + // postcss-value-parser represents quoted strings as type 'string' (as opposed to word) + if ( + (!node.nodes[0].quote && node.nodes[0].value[0] !== "$") || + vars[node.nodes[0].value] === "word" + ) { + if (context.fix) { + const contents = /unquote\((.*)\)/.exec(decl.value); + + decl.value = contents[1]; + } else { + utils.report({ + message: messages.rejected, + node: decl, + result, + ruleName + }); + } + } + }); + }); + }; +} + +export default rule; diff --git a/src/rules/index.js b/src/rules/index.js index f316b694..73a1bf75 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -30,6 +30,7 @@ import doubleSlashCommentEmptyLineBefore from "./double-slash-comment-empty-line import doubleSlashCommentInline from "./double-slash-comment-inline"; import doubleSlashCommentWhitespaceInside from "./double-slash-comment-whitespace-inside"; import functionNoQuotedStrings from "./function-quote-no-quoted-strings-inside"; +import functionNoUnquotedStrings from "./function-unquote-no-unquoted-strings-inside"; import mediaFeatureValueDollarVariable from "./media-feature-value-dollar-variable"; import noDollarVariables from "./no-dollar-variables"; import noDuplicateDollarVariables from "./no-duplicate-dollar-variables"; @@ -74,6 +75,7 @@ export default { "double-slash-comment-inline": doubleSlashCommentInline, "double-slash-comment-whitespace-inside": doubleSlashCommentWhitespaceInside, "function-quote-no-quoted-strings-inside": functionNoQuotedStrings, + "function-unquote-no-unquoted-strings-inside": functionNoUnquotedStrings, "media-feature-value-dollar-variable": mediaFeatureValueDollarVariable, "no-dollar-variables": noDollarVariables, "no-duplicate-dollar-variables": noDuplicateDollarVariables,