diff --git a/lib/rules/color-function-notation/__tests__/index.js b/lib/rules/color-function-notation/__tests__/index.js index 341eaa5b57..0dde5f65dd 100644 --- a/lib/rules/color-function-notation/__tests__/index.js +++ b/lib/rules/color-function-notation/__tests__/index.js @@ -378,6 +378,9 @@ testRule({ { code: 'a { color: rgb($a $b $c / $d) }', }, + { + code: 'a { color: rgba(#fff, 0.85) }', + }, ], reject: [ diff --git a/lib/rules/color-function-notation/index.js b/lib/rules/color-function-notation/index.js index 5563c5317c..a797801985 100644 --- a/lib/rules/color-function-notation/index.js +++ b/lib/rules/color-function-notation/index.js @@ -4,9 +4,11 @@ const valueParser = require('postcss-value-parser'); const declarationValueIndex = require('../../utils/declarationValueIndex'); const getDeclarationValue = require('../../utils/getDeclarationValue'); +const isStandardSyntaxColorFunction = require('../../utils/isStandardSyntaxColorFunction'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const setDeclarationValue = require('../../utils/setDeclarationValue'); +const { isValueFunction } = require('../../utils/typeGuards'); const validateOptions = require('../../utils/validateOptions'); const ruleName = 'color-function-notation'; @@ -33,7 +35,9 @@ const rule = (primary, _secondaryOptions, context) => { const parsedValue = valueParser(getDeclarationValue(decl)); parsedValue.walk((node) => { - if (node.type !== 'function') return; + if (!isValueFunction(node)) return; + + if (!isStandardSyntaxColorFunction(node)) return; const { value, sourceIndex, nodes } = node; diff --git a/lib/utils/__tests__/isStandardSyntaxColorFunction.test.js b/lib/utils/__tests__/isStandardSyntaxColorFunction.test.js new file mode 100644 index 0000000000..83e83daac9 --- /dev/null +++ b/lib/utils/__tests__/isStandardSyntaxColorFunction.test.js @@ -0,0 +1,69 @@ +'use strict'; + +const isStandardSyntaxColorFunction = require('../isStandardSyntaxColorFunction'); +const postcss = require('postcss'); +const valueParser = require('postcss-value-parser'); + +describe('isStandardSyntaxColorFunction', () => { + it('legacy syntax', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgb(0, 0, 0) }'))).toBe(true); + }); + + it('legacy syntax alpha', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgba(0, 0, 0, 0) }'))).toBe(true); + }); + + it('modern syntax', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgba(0 0 0) }'))).toBe(true); + }); + + it('modern syntax alpha', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgba(0 0 0 / 50%) }'))).toBe(true); + }); + + it('space', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgba( 0 0 0 / 50% ) }'))).toBe(true); + }); + + it('comment', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgba(/* #aaa */ 0 0 0 / 50%) }'))).toBe( + true, + ); + }); + + it('custom property', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rgba(var(--red) 0 0 / 50%) }'))).toBe( + true, + ); + }); + + it('function', () => { + expect( + isStandardSyntaxColorFunction(func('a { color: rgba(calc(var(--red) + 50) 0 0 / 50%) }')), + ).toBe(true); + }); + + it('scss color function conversion', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rbga(#aaa, 0.5) }'))).toBe(false); + }); + + it('scss color function conversion with comment', () => { + expect(isStandardSyntaxColorFunction(func('a { color: rbga(/* comment */ #aaa, 0.5) }'))).toBe( + false, + ); + }); +}); + +function func(css) { + const functions = []; + + postcss.parse(css).walkDecls((decl) => { + valueParser(decl.value).walk((valueNode) => { + if (valueNode.type === 'function') { + functions.push(valueNode); + } + }); + }); + + return functions[0]; +} diff --git a/lib/utils/isStandardSyntaxColorFunction.js b/lib/utils/isStandardSyntaxColorFunction.js new file mode 100644 index 0000000000..fc9cbd265d --- /dev/null +++ b/lib/utils/isStandardSyntaxColorFunction.js @@ -0,0 +1,20 @@ +'use strict'; + +const isStandardSyntaxFunction = require('./isStandardSyntaxFunction'); + +/** + * Check whether a function is standard syntax color function + * + * @param {import('postcss-value-parser').FunctionNode} node + * @returns {boolean} + */ +module.exports = function isStandardSyntaxColorFunction(node) { + if (!isStandardSyntaxFunction(node)) return false; + + // scss rgba() function can accept a hex as the first param + for (const fnNode of node.nodes) { + if (fnNode.type === 'word' && fnNode.value.startsWith('#')) return false; + } + + return true; +}; diff --git a/lib/utils/typeGuards.js b/lib/utils/typeGuards.js index 7f41b2be1e..2bf6a4624e 100644 --- a/lib/utils/typeGuards.js +++ b/lib/utils/typeGuards.js @@ -43,6 +43,14 @@ module.exports.isDeclaration = function isDeclaration(node) { return node.type === 'decl'; }; +/** + * @param {import('postcss-value-parser').Node} node + * @returns {node is import('postcss-value-parser').FunctionNode} + */ +module.exports.isValueFunction = function isValueFunction(node) { + return node.type === 'function'; +}; + /** * @param {Node} node * @returns {node is (Node & {source: NodeSource})}