diff --git a/lib/rules/unit-no-unknown/__tests__/index.js b/lib/rules/unit-no-unknown/__tests__/index.js index 41319c467c..6ddbc6c644 100644 --- a/lib/rules/unit-no-unknown/__tests__/index.js +++ b/lib/rules/unit-no-unknown/__tests__/index.js @@ -437,6 +437,19 @@ testRule(rule, { code: 'a { margin: calc(100% - #{$margin * 2}); }', description: 'work with interpolation', }, + { + code: '$foo: ( 1prop: 10px, 2prop: 12px, 3prop: /* comment */ 14px )', + description: 'ignore map property name', + }, + { + code: '$breakpoints: ( small: /* comment */ 767px, 1medium: 992px, large: 1200px );', + description: 'ignore map property name', + }, + { + code: + '$breakpoints: ( small: /* comment */ 767px, medium: ( 1prop: 992px, 2prop: ( 1prop: 1200px ) ) );', + description: 'ignore map property name in nested maps', + }, ], reject: [ @@ -470,6 +483,19 @@ testRule(rule, { line: 1, column: 39, }, + { + code: '$breakpoints: ( small: 767px, 1medium: 992pix, large: 1200px );', + message: messages.rejected('pix'), + line: 1, + column: 40, + }, + { + code: + '$breakpoints: ( small: /* comment */ 767px, medium: ( 1prop: 992pix, 2prop: ( 1prop: 1200px ) ) );', + message: messages.rejected('pix'), + line: 1, + column: 49, + }, { code: 'a { font: (italic bold 10px/8pix) }', message: messages.rejected('pix'), diff --git a/lib/rules/unit-no-unknown/index.js b/lib/rules/unit-no-unknown/index.js index 763257bf33..c6ad74c292 100644 --- a/lib/rules/unit-no-unknown/index.js +++ b/lib/rules/unit-no-unknown/index.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const atRuleParamIndex = require('../../utils/atRuleParamIndex'); const declarationValueIndex = require('../../utils/declarationValueIndex'); const getUnitFromValueNode = require('../../utils/getUnitFromValueNode'); +const isMap = require('../../utils/isMap'); const keywordSets = require('../../reference/keywordSets'); const mediaParser = require('postcss-media-query-parser').default; const optionsMatches = require('../../utils/optionsMatches'); @@ -18,6 +19,10 @@ const messages = ruleMessages(ruleName, { rejected: (unit) => `Unexpected unknown unit "${unit}"`, }); +// The map property name (in map cleared from comments and spaces) always +// has index that being divided by 4 gives remainder equals 0 +const mapPropertyNameIndexOffset = 4; + const rule = function(actual, options) { return (root, result) => { const validOptions = validateOptions( @@ -43,6 +48,7 @@ const rule = function(actual, options) { // by postcss-value-parser value = value.replace(/\*/g, ','); const parsedValue = valueParser(value); + const ignoredMapProperties = []; parsedValue.walk(function(valueNode) { // Ignore wrong units within `url` function @@ -55,6 +61,18 @@ const rule = function(actual, options) { return false; } + if (isMap(valueNode)) { + valueNode.nodes.forEach((node, index) => { + if (!(index % mapPropertyNameIndexOffset)) { + ignoredMapProperties.push(node.sourceIndex); + } + }); + } + + if (ignoredMapProperties.includes(valueNode.sourceIndex)) { + return; + } + const unit = getUnitFromValueNode(valueNode); if (!unit) { diff --git a/lib/utils/__tests__/isMap.test.js b/lib/utils/__tests__/isMap.test.js new file mode 100644 index 0000000000..8d03db1d4f --- /dev/null +++ b/lib/utils/__tests__/isMap.test.js @@ -0,0 +1,66 @@ +'use strict'; + +const isMap = require('../isMap'); +const sass = require('postcss-sass'); +const scss = require('postcss-scss'); +const valueParser = require('postcss-value-parser'); + +describe('isMap', () => { + const simpleMaps = [ + ['$map: (prop: "flex");', true], + ['$font: (italic bold 10px/8pix)', false], + ['$map: (prop: /* comment */ 0);', true], + ['$calc: calc(100% / 2px);', false], + ['$url: url();', false], + ]; + const nestedMaps = [ + ['$map: (prop: 0, prop2: (prop3: "normal"), prop4: 2px);', [0, 17]], + ['$map: (prop: 0, prop2: (prop3: "normal", prop4: (prop5: "grid")), prop6: 2px);', [0, 17, 42]], + ]; + + test.each(simpleMaps)('simple maps', (css, expected) => { + runTests(css, (decl) => { + const parsedValue = valueParser(decl.value).nodes[0]; + + expect(isMap(parsedValue)).toBe(expected); + }); + }); + + test.each(nestedMaps)('nested maps', (css, expected) => { + runTests(css, (decl) => { + const parsedValue = valueParser(decl.value); + + parsedValue.walk(function(valueNode) { + if (expected.includes(valueNode.sourceIndex)) { + expect(isMap(valueNode)).toBeTruthy(); + } else { + expect(isMap(valueNode)).toBeFalsy(); + } + }); + }); + }); + + it('empty map returns `false`', () => { + const emptyMap = '$map: ();'; + + runTests(emptyMap, (decl) => { + const parsedValue = valueParser(decl.value); + + expect(isMap(parsedValue)).toBeFalsy(); + }); + }); +}); + +function sassDecls(css, cb) { + sass.parse(css).walkDecls(cb); +} + +function scssDecls(css, cb) { + scss.parse(css).walkDecls(cb); +} + +function runTests(css, cb) { + [sassDecls, scssDecls].forEach((fn) => { + fn(css, cb); + }); +} diff --git a/lib/utils/isMap.js b/lib/utils/isMap.js new file mode 100644 index 0000000000..57bfec546b --- /dev/null +++ b/lib/utils/isMap.js @@ -0,0 +1,44 @@ +'use strict'; + +/** @typedef {import('postcss-value-parser').Node} ValueNode */ + +/** + * @param {ValueNode | undefined} valueNode + * @returns {boolean} + */ +module.exports = function(valueNode) { + if (!valueNode) { + return false; + } + + if (valueNode.type !== 'function' || !valueNode.nodes || valueNode.value) { + return false; + } + + // It's necessary to remove comments and spaces if they are present + const cleanNodes = valueNode.nodes.filter( + (node) => node.type !== 'comment' && node.type !== 'space', + ); + + // Map without comments and spaces will have the structure like $map (prop: value, prop2: value) + // ↑ ↑ ↑ ↑ + // 0 1 2 3 + if (cleanNodes[0] && cleanNodes[0].type !== 'word' && cleanNodes[0].type !== 'string') { + return false; + } + + if (cleanNodes[1] && cleanNodes[1].value !== ':') { + return false; + } + + // There is no need to check type or value of this node since it could be anything + if (!cleanNodes[2]) { + return false; + } + + if (cleanNodes[3] && cleanNodes[3].value !== ',') { + return false; + } + + return true; +};