From d1ce4f45c59c428afae14733d4c1a7b15c765ffa Mon Sep 17 00:00:00 2001 From: Drew Hays Date: Tue, 19 Jan 2021 07:05:58 -0800 Subject: [PATCH] Fix false negatives for css-in-js object notation in color-hex-case (#5101) * Validate color-hex-case with postcss-value-parser As discussed in #4826, `style-search` has false positives when used with `css-in-js` object notation. This change updates the `color-hex-case` rule to use `postcss-value-parser` instead, which has support for `css-in-js` as a syntax. The unit tests now also include a suite for testing both `upper` and `lower` options for `color-hex-case` with the `css-in-js` syntax. * Update color-hex-case with requested changes * Remove unnecessary import from tests * Clean up whitespace. * Update the column number for the tests * Remove unnecessary return --- lib/rules/color-hex-case/__tests__/index.js | 178 ++++++++++++++++++++ lib/rules/color-hex-case/index.js | 76 ++++----- 2 files changed, 213 insertions(+), 41 deletions(-) diff --git a/lib/rules/color-hex-case/__tests__/index.js b/lib/rules/color-hex-case/__tests__/index.js index a01f9379f0..3845e29963 100644 --- a/lib/rules/color-hex-case/__tests__/index.js +++ b/lib/rules/color-hex-case/__tests__/index.js @@ -164,3 +164,181 @@ testRule( ], }), ); + +testRule({ + ruleName, + config: ['lower'], + syntax: 'css-in-js', + fix: true, + accept: [ + { + code: ` + export const s = styled.a({ + color: "#aaa", + }); + `, + }, + { + code: ` + export const s = styled.a({ + stroke: "url(#AAA)", + }); + `, + description: 'href with location', + }, + { + code: ` + export const s = styled.a({ + color: "PINK", + }); + `, + }, + { + code: ` + export const s = styled.a({ + background: "linear-gradient(#aaa, #ffff, #0000ffcc)", + }); + `, + }, + { + code: ` + export const s = styled("a::before")({ + content: '"#ABABAB"', + }); + `, + }, + { + code: ` + export const s = styled.a({ + color: "white /* #FFF */", + }); + `, + }, + ], + + reject: [ + { + code: ` + export const s = styled.a({ + color: "#aBABAA", + }); + `, + fixed: ` + export const s = styled.a({ + color: "#ababaa", + }); + `, + + message: messages.expected('#aBABAA', '#ababaa'), + line: 3, + column: 13, + }, + { + code: ` + export const s = styled.a({ + background: "linear-gradient(#AAA, #FaFa, #0000FFcc)", + }); + `, + fixed: ` + export const s = styled.a({ + background: "linear-gradient(#aaa, #fafa, #0000ffcc)", + }); + `, + + warnings: [ + { message: messages.expected('#AAA', '#aaa'), line: 3, column: 34 }, + { message: messages.expected('#FaFa', '#fafa'), line: 3, column: 40 }, + { message: messages.expected('#0000FFcc', '#0000ffcc'), line: 3, column: 47 }, + ], + }, + ], +}); + +testRule({ + ruleName, + config: ['upper'], + syntax: 'css-in-js', + fix: true, + accept: [ + { + code: ` + export const s = styled.a({ + color: "#AAA", + }); + `, + }, + { + code: ` + export const s = styled.a({ + stroke: "url(#aaa)", + }); + `, + description: 'href with location', + }, + { + code: ` + export const s = styled.a({ + color: "pink", + }); + `, + }, + { + code: ` + export const s = styled.a({ + background: "linear-gradient(#AAA, #FFFF, #0000FFCC)", + }); + `, + }, + { + code: ` + export const s = styled("a::before")({ + content: '"#ababab"', + }); + `, + }, + { + code: ` + export const s = styled.a({ + color: "white /* #fff */", + }); + `, + }, + ], + + reject: [ + { + code: ` + export const s = styled.a({ + color: "#aBABAB", + }); + `, + fixed: ` + export const s = styled.a({ + color: "#ABABAB", + }); + `, + + message: messages.expected('#aBABAB', '#ABABAB'), + line: 3, + column: 13, + }, + { + code: ` + export const s = styled.a({ + background: "linear-gradient(#aaa, #FaFa, #0000FFcc)", + }); + `, + fixed: ` + export const s = styled.a({ + background: "linear-gradient(#AAA, #FAFA, #0000FFCC)", + }); + `, + + warnings: [ + { message: messages.expected('#aaa', '#AAA'), line: 3, column: 34 }, + { message: messages.expected('#FaFa', '#FAFA'), line: 3, column: 40 }, + { message: messages.expected('#0000FFcc', '#0000FFCC'), line: 3, column: 47 }, + ], + }, + ], +}); diff --git a/lib/rules/color-hex-case/index.js b/lib/rules/color-hex-case/index.js index 3416439030..2d55ce67e8 100644 --- a/lib/rules/color-hex-case/index.js +++ b/lib/rules/color-hex-case/index.js @@ -2,10 +2,11 @@ 'use strict'; -const blurFunctionArguments = require('../../utils/blurFunctionArguments'); +const valueParser = require('postcss-value-parser'); + +const declarationValueIndex = require('../../utils/declarationValueIndex'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); -const styleSearch = require('style-search'); const validateOptions = require('../../utils/validateOptions'); const ruleName = 'color-hex-case'; @@ -14,6 +15,9 @@ const messages = ruleMessages(ruleName, { expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`, }); +const HEX = /^#[0-9A-Za-z]+/; +const IGNORED_FUNCTIONS = new Set(['url']); + function rule(expectation, options, context) { return (root, result) => { const validOptions = validateOptions(result, ruleName, { @@ -26,68 +30,58 @@ function rule(expectation, options, context) { } root.walkDecls((decl) => { - const declString = blurFunctionArguments(decl.toString(), 'url'); - const fixPositions = []; + const parsedValue = valueParser(getValue(decl)); + let needsFix = false; - styleSearch({ source: declString, target: '#' }, (match) => { - const hexMatch = /^#[0-9A-Za-z]+/.exec(declString.substr(match.startIndex)); + parsedValue.walk((node) => { + const { value } = node; - if (!hexMatch) { - return; - } + if (isIgnoredFunction(node)) return false; - const hexValue = hexMatch[0]; - const hexValueLower = hexValue.toLowerCase(); - const hexValueUpper = hexValue.toUpperCase(); - const expectedHex = expectation === 'lower' ? hexValueLower : hexValueUpper; + if (!isHexColor(node)) return; - if (hexValue === expectedHex) { - return; - } + const expected = expectation === 'lower' ? value.toLowerCase() : value.toUpperCase(); + + if (value === expected) return; if (context.fix) { - fixPositions.unshift({ - expectedHex, - currentHex: hexValue, - startIndex: match.startIndex, - }); + node.value = expected; + needsFix = true; return; } report({ - message: messages.expected(hexValue, expectedHex), + message: messages.expected(value, expected), node: decl, - index: match.startIndex, + index: declarationValueIndex(decl) + node.sourceIndex, result, ruleName, }); }); - if (fixPositions.length) { - const declProp = decl.prop; - const declBetween = decl.raws.between; - - fixPositions.forEach((fixPosition) => { - // 1 — it's a # length - decl.value = replaceHex( - decl.value, - fixPosition.currentHex, - fixPosition.expectedHex, - fixPosition.startIndex - declProp.length - declBetween.length - 1, - ); - }); + if (needsFix) { + setValue(decl, parsedValue.toString()); } }); }; } -function replaceHex(input, searchString, replaceString, startIndex) { - const offset = startIndex + 1; - const stringStart = input.slice(0, offset); - const stringEnd = input.slice(offset + searchString.length); +function isIgnoredFunction({ type, value }) { + return type === 'function' && IGNORED_FUNCTIONS.has(value.toLowerCase()); +} + +function isHexColor({ type, value }) { + return type === 'word' && HEX.test(value); +} + +function getValue(decl) { + return decl.raws.value ? decl.raws.value.raw : decl.value; +} - return stringStart + replaceString + stringEnd; +function setValue(decl, value) { + if (decl.raws.value) decl.raws.value.raw = value; + else decl.value = value; } rule.ruleName = ruleName;