diff --git a/src/rules/index.js b/src/rules/index.js index 3cedbd78..f65d2d86 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -33,6 +33,7 @@ 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 mapKeysQuotes from "./map-keys-quotes"; import noDuplicateDollarVariables from "./no-duplicate-dollar-variables"; import operatorNoNewlineAfter from "./operator-no-newline-after"; import operatorNoNewlineBefore from "./operator-no-newline-before"; @@ -77,6 +78,7 @@ export default { "double-slash-comment-whitespace-inside": doubleSlashCommentWhitespaceInside, "function-quote-no-quoted-strings-inside": functionNoQuotedStrings, "function-unquote-no-unquoted-strings-inside": functionNoUnquotedStrings, + "map-keys-quotes": mapKeysQuotes, "media-feature-value-dollar-variable": mediaFeatureValueDollarVariable, "no-dollar-variables": noDollarVariables, "no-duplicate-dollar-variables": noDuplicateDollarVariables, diff --git a/src/rules/map-keys-quotes/README.md b/src/rules/map-keys-quotes/README.md new file mode 100644 index 00000000..d59cbd88 --- /dev/null +++ b/src/rules/map-keys-quotes/README.md @@ -0,0 +1,26 @@ +# map-keys-quotes + +All map keys should be quoted. + +```scss +$test: (Helvetica: 14px, Arial: 25px); + /** ↑ ↑ + * These words should be quoted. + */ +``` + +## Options + +### `always` + +The following patterns are considered violations: + +```scss +$test: (Helvetica: 14px, Arial: 25px); +``` + +The following patterns are _not_ considered violations: + +```scss +$test: ("foo": 14px, "bar": 25px); +``` diff --git a/src/rules/map-keys-quotes/__tests__/index.js b/src/rules/map-keys-quotes/__tests__/index.js new file mode 100644 index 00000000..3c5ec988 --- /dev/null +++ b/src/rules/map-keys-quotes/__tests__/index.js @@ -0,0 +1,28 @@ +import rule, { ruleName, messages } from ".."; + +testRule(rule, { + ruleName, + config: ["always"], + syntax: "scss", + + accept: [ + { + code: ` + $test: ("foo": 14px, "bar": 25px); + `, + description: "accepts strings without quotes" + } + ], + + reject: [ + { + code: ` + $test: (Helvetica: 25px, Arial: 50px) + `, + message: messages.expected, + description: + "does not accept variables representing strings that are quoted.", + location: 1 + } + ] +}); diff --git a/src/rules/map-keys-quotes/index.js b/src/rules/map-keys-quotes/index.js new file mode 100644 index 00000000..cd08ff61 --- /dev/null +++ b/src/rules/map-keys-quotes/index.js @@ -0,0 +1,84 @@ +import { utils } from "stylelint"; +import { namespace } from "../../utils"; +import valueParser from "postcss-value-parser"; + +export const ruleName = namespace("map-keys-quotes"); + +export const messages = utils.ruleMessages(ruleName, { + expected: "Expected keys in map to be quoted." +}); + +function rule(primary) { + return (root, result) => { + const validOptions = utils.validateOptions(result, ruleName, { + actual: primary, + possible: ["always"] + }); + + if (!validOptions) { + return; + } + + root.walkDecls(decl => { + if (decl.prop[0] !== "$") { + return; + } + + valueParser(decl.value).walk(node => { + if ( + node.type === "function" && + node.value === "" && + isMap(node.nodes) + ) { + // Identify all of the map-keys and see if they're strings (not words). + const mapKeys = returnMapKeys(node.nodes); + + mapKeys.forEach(map_key => { + if (map_key.type === "word") { + utils.report({ + message: messages.expected, + node: decl, + result, + ruleName + }); + } + }); + } + }); + }); + }; +} + +// Takes in a list of map nodes and identifies if they are a map. +// A map is identified by the pattern: [string/word colon(div) anything comma(div) ...] +function isMap(nodes) { + if (nodes.length < 4) { + return false; + } + + if (nodes[0].type !== "word" && nodes[0].type !== "string") { + return false; + } + + if (nodes[1].value !== ":") { + return false; + } + + if (nodes[3].value !== ",") { + return false; + } + + return true; +} + +function returnMapKeys(array) { + const new_array = []; + + for (let i = 0; i < array.length; i += 4) { + new_array.push(array[i]); + } + + return new_array; +} + +export default rule;