Skip to content

Commit

Permalink
Fix false negatives for css-in-js object notation in color-hex-case (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
Dru89 committed Jan 19, 2021
1 parent dcaac08 commit d1ce4f4
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 41 deletions.
178 changes: 178 additions & 0 deletions lib/rules/color-hex-case/__tests__/index.js
Expand Up @@ -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 },
],
},
],
});
76 changes: 35 additions & 41 deletions lib/rules/color-hex-case/index.js
Expand Up @@ -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';
Expand All @@ -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, {
Expand All @@ -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;
Expand Down

0 comments on commit d1ce4f4

Please sign in to comment.