Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix false negatives for css-in-js object notation in color-hex-case #5101

Merged
merged 6 commits into from Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
178 changes: 178 additions & 0 deletions lib/rules/color-hex-case/__tests__/index.js
Expand Up @@ -164,3 +164,181 @@ testRule(
],
}),
);

testRule({
Dru89 marked this conversation as resolved.
Show resolved Hide resolved
ruleName,
config: ['lower'],
syntax: 'css-in-js',
fix: true,
accept: [
{
code: `
Dru89 marked this conversation as resolved.
Show resolved Hide resolved
export const s = styled.a({
color: "#aaa",
});
`,
},
{
code: `
Dru89 marked this conversation as resolved.
Show resolved Hide resolved
export const s = styled.a({
stroke: "url(#AAA)",
});
`,
description: 'href with location',
},
{
code: `
export const s = styled.a({
color: "PINK",
});
`,
},
{
code: `
Dru89 marked this conversation as resolved.
Show resolved Hide resolved
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: `
Dru89 marked this conversation as resolved.
Show resolved Hide resolved
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({
Dru89 marked this conversation as resolved.
Show resolved Hide resolved
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 },
],
},
],
});
78 changes: 37 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]+/;
hudochenkov marked this conversation as resolved.
Show resolved Hide resolved
const IGNORED_FUNCTIONS = new Set(['url']);

function rule(expectation, options, context) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
Expand All @@ -26,68 +30,60 @@ 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;
}

function setValue(decl, value) {
if (decl.raws.value) decl.raws.value.raw = value;
else decl.value = value;

return stringStart + replaceString + stringEnd;
return decl;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return is not used anywhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dru89 It looks like this is the only change we need to make for a second approval.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dru89 It looks like this is the only change we need to make for a second approval.

}

rule.ruleName = ruleName;
Expand Down