Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dark-bees-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Fixed: `shorthand-property-no-redundant-values` false negatives for additional radius
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,15 @@ testRule({
},
{
code: 'a { border-radius: 1px / 1px; }',
description: 'ignore ellipse',
},
{
code: 'a { border-radius: 1px 1px / 1px; }',
description: 'ignore ellipse',
},
{
code: 'a { margin: var(--margin) var(--margin); }',
description: 'ignore variables',
},
{
code: 'a { border-radius: var(--foo) var(--foo) / var(--foo) var(--foo); }',
description: 'ignore variables with slash in shorthand',
},
{
code: 'a { border-color: #FFFFFF transparent transparent; }',
description: 'ignore upper case value',
Expand Down Expand Up @@ -587,6 +586,77 @@ testRule({
endLine: 1,
endColumn: 62,
},
{
code: 'a { border-radius: 1px 1px / 1px; }',
fixed: 'a { border-radius: 1px / 1px; }',
fix: {
range: [23, 27],
text: '',
},
message: messages.expected('1px 1px / 1px', '1px / 1px'),
line: 1,
column: 20,
endLine: 1,
endColumn: 33,
},
{
code: 'a { border-radius: 1px 1px 1px 1px / 2px 2px 2px 2px; }',
fixed: 'a { border-radius: 1px / 2px; }',
fix: {
range: [23, 48],
text: '/',
},
message: messages.expected('1px 1px 1px 1px / 2px 2px 2px 2px', '1px / 2px'),
line: 1,
column: 20,
endLine: 1,
endColumn: 53,
},
{
code: 'a { border-radius: 1px 1px 1px 1px / 2px; }',
fixed: 'a { border-radius: 1px / 2px; }',
description: 'Horizontal side redundant - vertical already shortest',
fix: {
range: [23, 35],
text: '',
},
message: messages.expected('1px 1px 1px 1px / 2px', '1px / 2px'),
line: 1,
column: 20,
endLine: 1,
endColumn: 41,
},
{
code: 'a { border-radius: 1px 2px / 2px 2px 2px 2px; }',
fixed: 'a { border-radius: 1px 2px / 2px; }',
description: 'Vertical side redundant - horizontal mixed',
fix: {
range: [32, 44],
text: '',
},
message: messages.expected('1px 2px / 2px 2px 2px 2px', '1px 2px / 2px'),
line: 1,
column: 20,
endLine: 1,
endColumn: 45,
},
{
code: 'a { border-radius: calc(1px + 1px) calc(1px + 1px) / calc(1px + 1px) calc(1px + 1px); }',
fixed: 'a { border-radius: calc(1px + 1px) / calc(1px + 1px); }',
description: 'slash with value functions',
fix: {
range: [35, 68],
text: '/',
},
message: messages.expected(
'calc(1px + 1px) calc(1px + 1px) / calc(1px + 1px) calc(1px + 1px)',
'calc(1px + 1px) / calc(1px + 1px)',
),
line: 1,
column: 20,
endLine: 1,
endColumn: 85,
},
],
});

Expand Down
92 changes: 92 additions & 0 deletions lib/rules/shorthand-property-no-redundant-values/index.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions lib/rules/shorthand-property-no-redundant-values/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,47 @@
return right === left;
}

/**
* Split the `border-radius` value into horizontal and vertical radius arrays.
*
* @param {string} value
* @returns {{ horizontal: string[]; vertical: string[]; sawSlash: boolean }}
*/
function splitBorderRadius(value) {
/** @type {string[]} */
const horizontal = [];
/** @type {string[]} */
const vertical = [];
let sawSlash = false;

for (const node of valueParser(value).nodes) {
// Handle dividers: we only care about the slash
if (isValueDiv(node)) {
if (node.value === '/') {
sawSlash = true;
continue;
}

// Any other divider means we cannot safely lint
return { horizontal: [], vertical: [], sawSlash: false };
}

if (isVarFunction(node)) return { horizontal: [], vertical: [], sawSlash: false };

if (isValueWord(node) || isValueFunction(node)) {

Check warning on line 142 in lib/rules/shorthand-property-no-redundant-values/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/shorthand-property-no-redundant-values/index.mjs#L140-L142

Added lines #L140 - L142 were not covered by tests
const token = valueParser.stringify(node);

if (sawSlash) {
vertical.push(token);
} else {
horizontal.push(token);
}
}
}

return { horizontal, vertical, sawSlash };
}

/** @type {import('stylelint').CoreRules[ruleName]} */
const rule = (primary) => {
return (root, result) => {
Expand All @@ -134,6 +175,57 @@

if (!isSupportedShorthand(normalizedProp)) return;

// Special handling for `border-radius` when a slash is present
if (normalizedProp === 'border-radius') {
const { horizontal, vertical, sawSlash } = splitBorderRadius(value);

if (sawSlash) {
if (
horizontal.length >= 1 &&
horizontal.length <= 4 &&
vertical.length >= 1 &&
vertical.length <= 4
) {
const [h1 = '', h2 = '', h3 = '', h4 = ''] = horizontal;
const [v1 = '', v2 = '', v3 = '', v4 = ''] = vertical;

const condensedHorizontal = canCondense(h1, h2, h3, h4).filter(Boolean).join(' ');
const condensedVertical = canCondense(v1, v2, v3, v4).filter(Boolean).join(' ');

const shortestFormString = `${condensedHorizontal} / ${condensedVertical}`.trim();
const valueLower = value.trim().toLowerCase().replace(/\s+/g, ' ');
const shortestLower = shortestFormString.toLowerCase();

if (valueLower === shortestLower) return; // Already optimal

const fix = () => {
decl.value = shortestFormString;
};

const index = declarationValueIndex(decl);
const endIndex = index + decl.value.length;

report({
message: messages.expected,
messageArgs: [value, shortestFormString],
node: decl,
index,
endIndex,
result,
ruleName,
fix: {
apply: fix,
node: decl,
},
});

return;
}

return;
}
}

/** @type {string[]} */
const valuesToShorthand = [];

Expand Down