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-length #5106

Merged
merged 2 commits into from Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
192 changes: 192 additions & 0 deletions lib/rules/color-hex-length/__tests__/index.js
Expand Up @@ -186,3 +186,195 @@ testRule(
],
}),
);

testRule({
ruleName,
config: ['short'],
syntax: 'css-in-js',
fix: true,

accept: [
{
code: `
export const s = styled.a({
color: "#aaa",
});
`,
},
{
code: `
export const s = styled.a({
color: "#ababab",
});
`,
},
{
code: `
export const s = styled.a({
stroke: "url(#aaaaaa)",
});
`,
description: 'href with location',
},
{
code: `
export const s = styled.a({
color: "darkred",
});
`,
},
{
code: `
export const s = styled.a({
background: "linear-gradient(#aaa, #ffff, #01234567)",
});
`,
},
{
code: `
export const s = styled("a::before")({
content: '"#aabbcc"',
});
`,
},
{
code: `
export const s = styled.a({
color: "white /* #ffffff */",
});
`,
},
],

reject: [
{
code: `
export const s = styled.a({
color: "#aaaaaa",
});
`,
fixed: `
export const s = styled.a({
color: "#aaa",
});
`,

message: messages.expected('#aaaaaa', '#aaa'),
line: 3,
column: 13,
},
{
code: `
export const s = styled.a({
background: "linear-gradient(#aabbcc, #0000ffcc)",
});
`,
fixed: `
export const s = styled.a({
background: "linear-gradient(#abc, #00fc)",
});
`,

warnings: [
{ message: messages.expected('#aabbcc', '#abc'), line: 3, column: 34 },
{ message: messages.expected('#0000ffcc', '#00fc'), line: 3, column: 43 },
],
},
],
});

testRule({
ruleName,
config: ['long'],
syntax: 'css-in-js',
fix: true,

accept: [
{
code: `
export const s = styled.a({
color: "#aaaaaa",
});
`,
},
{
code: `
export const s = styled.a({
color: "#abababab",
});
`,
},
{
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(#aaaaaa, #ffffffff, #01234567)",
});
`,
},
{
code: `
export const s = styled("a::before")({
content: '"#abc"',
});
`,
},
{
code: `
export const s = styled.a({
color: "white /* #fff */",
});
`,
},
],

reject: [
{
code: `
export const s = styled.a({
color: "#aaa",
});
`,
fixed: `
export const s = styled.a({
color: "#aaaaaa",
});
`,

message: messages.expected('#aaa', '#aaaaaa'),
line: 3,
column: 13,
},
{
code: `
export const s = styled.a({
background: "linear-gradient(#abc, #00fc)",
});
`,
fixed: `
export const s = styled.a({
background: "linear-gradient(#aabbcc, #0000ffcc)",
});
`,

warnings: [
{ message: messages.expected('#abc', '#aabbcc'), line: 3, column: 34 },
{ message: messages.expected('#00fc', '#0000ffcc'), line: 3, column: 40 },
],
},
],
});
67 changes: 33 additions & 34 deletions lib/rules/color-hex-length/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-length';
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, _, context) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
Expand All @@ -26,17 +30,15 @@ function rule(expectation, _, 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: hexValue } = node;

if (!hexMatch) {
return;
}
if (isIgnoredFunction(node)) return false;

const hexValue = hexMatch[0];
if (!isHexColor(node)) return;

if (expectation === 'long' && hexValue.length !== 4 && hexValue.length !== 5) {
return;
Expand All @@ -50,37 +52,23 @@ function rule(expectation, _, context) {
const expectedHex = variant(hexValue);

if (context.fix) {
fixPositions.unshift({
expectedHex,
currentHex: hexValue,
startIndex: match.startIndex,
});
node.value = expectedHex;
needsFix = true;

return;
}

report({
message: messages.expected(hexValue, expectedHex),
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());
}
});
};
Expand Down Expand Up @@ -117,12 +105,23 @@ function longer(hex) {
return hexVariant;
}

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.

Suggested change
return decl;

We can remove this redundant return as per the other pull request.

At some point later down the line, we should refactor to move at least the getValue and setValue functions out to /utils. For now, let's duplicate them as you are doing, though.

}

rule.ruleName = ruleName;
Expand Down