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 all 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
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 },
],
},
],
});
65 changes: 31 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,21 @@ 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;
}

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