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

Add declaration-block-no-redundant-longhand-properties autofix #6580

Merged
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
5 changes: 5 additions & 0 deletions .changeset/pretty-seals-heal.md
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `declaration-block-no-redundant-longhand-properties` autofix
2 changes: 1 addition & 1 deletion docs/user-guide/rules.md
Expand Up @@ -250,7 +250,7 @@ Require or disallow quotes with these `quotes` rules.

Disallow redundancy with these `no-redundant` rules.

- [`declaration-block-no-redundant-longhand-properties`](../../lib/rules/declaration-block-no-redundant-longhand-properties/README.md): Disallow redundant longhand properties within declaration-block (Ⓢ).
- [`declaration-block-no-redundant-longhand-properties`](../../lib/rules/declaration-block-no-redundant-longhand-properties/README.md): Disallow redundant longhand properties within declaration-block (Autofixable) (Ⓢ).
- [`shorthand-property-no-redundant-values`](../../lib/rules/shorthand-property-no-redundant-values/README.md): Disallow redundant values within shorthand properties (Autofixable).

### Whitespace inside
Expand Down
36 changes: 24 additions & 12 deletions lib/reference/properties.js
Expand Up @@ -19,6 +19,7 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'animation',
new Set([
// prettier-ignore
'animation-name',
'animation-duration',
'animation-timing-function',
Expand All @@ -32,6 +33,7 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'background',
new Set([
// prettier-ignore
'background-image',
'background-size',
'background-position',
Expand All @@ -45,18 +47,19 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'border',
new Set([
// prettier-ignore
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-right-width',
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'border-right-style',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-right-color',
]),
],
[
Expand Down Expand Up @@ -91,14 +94,15 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
new Set([
// prettier-ignore
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-right-color',
]),
],
[
'border-image',
new Set([
// prettier-ignore
'border-image-source',
'border-image-slice',
'border-image-width',
Expand All @@ -118,6 +122,7 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'border-inline-start',
new Set([
// prettier-ignore
'border-inline-start-width',
'border-inline-start-style',
'border-inline-start-color',
Expand All @@ -135,6 +140,7 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'border-radius',
new Set([
// prettier-ignore
'border-top-right-radius',
'border-top-left-radius',
'border-bottom-right-radius',
Expand All @@ -155,9 +161,9 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
new Set([
// prettier-ignore
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'border-right-style',
]),
],
[
Expand Down Expand Up @@ -216,18 +222,20 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'font',
new Set([
// prettier-ignore
'font-style',
'font-variant',
'font-weight',
'font-stretch',
'font-size',
'font-family',
'line-height',
'font-family',
]),
],
[
'grid',
new Set([
// prettier-ignore
'grid-template-rows',
'grid-template-columns',
'grid-template-areas',
Expand Down Expand Up @@ -295,14 +303,15 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
new Set([
// prettier-ignore
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-right',
]),
],
[
'mask',
new Set([
// prettier-ignore
'mask-image',
'mask-mode',
'mask-position',
Expand All @@ -327,18 +336,20 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
new Set([
// prettier-ignore
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'padding-right',
]),
],
[
'text-decoration',
new Set([
// prettier-ignore
'text-decoration-color',
'text-decoration-style',
'text-decoration-line',
'text-decoration-style',
'text-decoration-color',
// TODO: add support for text-decoration-thickness, Level 4
// https://w3c.github.io/csswg-drafts/css-text-decor-4/#text-decoration-width-property
]),
],
[
Expand All @@ -352,10 +363,11 @@ const longhandSubPropertiesOfShorthandProperties = new Map([
[
'transition',
new Set([
'transition-delay',
'transition-duration',
// prettier-ignore
'transition-property',
'transition-duration',
'transition-timing-function',
'transition-delay',
]),
],
]);
Expand Down
Expand Up @@ -66,6 +66,8 @@ This rule complains when the following shorthand properties can be used:

Flexbox-related properties can be ignored using `ignoreShorthands: ["/flex/"]` (see below).

The [`fix` option](../../../docs/user-guide/options.md#fix) can automatically fix most of the problems reported by this rule.

## Options

### `true`
Expand Down
Expand Up @@ -5,6 +5,7 @@ const { messages, ruleName } = require('..');
testRule({
ruleName,
config: [true],
fix: true,
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved

accept: [
{
Expand Down Expand Up @@ -35,6 +36,7 @@ testRule({
reject: [
{
code: 'a { margin-left: 10px; margin-right: 10px; margin-top: 20px; margin-bottom: 30px; }',
fixed: 'a { margin: 20px 10px 30px 10px; }',
message: messages.expected('margin'),
line: 1,
column: 62,
Expand All @@ -43,6 +45,7 @@ testRule({
},
{
code: 'a { MARGIN-LEFT: 10px; MARGIN-RIGHT: 10px; margin-top: 20px; margin-bottom: 30px; }',
fixed: 'a { margin: 20px 10px 30px 10px; }',
message: messages.expected('margin'),
line: 1,
column: 62,
Expand All @@ -51,6 +54,7 @@ testRule({
},
{
code: 'a { MARGIN-LEFT: 10px; MARGIN-RIGHT: 10px; MARGIN-TOP: 20px; MARGIN-BOTTOM: 30px; }',
fixed: 'a { margin: 20px 10px 30px 10px; }',
message: messages.expected('margin'),
line: 1,
column: 62,
Expand All @@ -59,6 +63,7 @@ testRule({
},
{
code: 'a { padding-left: 10px; padding-right: 10px; padding-top: 20px; padding-bottom: 30px; }',
fixed: 'a { padding: 20px 10px 30px 10px; }',
message: messages.expected('padding'),
line: 1,
column: 65,
Expand All @@ -67,6 +72,7 @@ testRule({
},
{
code: 'a { font-style: italic; font-variant: normal; font-weight: bold; font-size: .8em; font-family: Arial; line-height: 1.2; font-stretch: normal; }',
fixed: 'a { font: italic normal bold normal .8em 1.2 Arial; }',
message: messages.expected('font'),
line: 1,
column: 121,
Expand All @@ -75,6 +81,7 @@ testRule({
},
{
code: 'a { border-top-width: 7px; border-top-style: double; border-top-color: green; }',
fixed: 'a { border-top: 7px double green; }',
message: messages.expected('border-top'),
line: 1,
column: 54,
Expand All @@ -83,6 +90,7 @@ testRule({
},
{
code: 'a { -webkit-transition-delay: 0.5s; -webkit-transition-duration: 2s; -webkit-transition-property: top; -webkit-transition-timing-function: ease; }',
fixed: 'a { -webkit-transition: top 2s ease 0.5s; }',
message: messages.expected('-webkit-transition'),
line: 1,
column: 104,
Expand All @@ -91,6 +99,7 @@ testRule({
},
{
code: 'a { -WEBKIT-transition-delay: 0.5s; -webKIT-transition-duration: 2s; -webkit-TRANSITION-property: top; -webkit-transition-timing-function: ease; }',
fixed: 'a { -webkit-transition: top 2s ease 0.5s; }',
message: messages.expected('-webkit-transition'),
line: 1,
column: 104,
Expand All @@ -99,6 +108,7 @@ testRule({
},
{
code: 'a { border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; }',
fixed: 'a { border-width: 1px 1px 1px 1px; }',
message: messages.expected('border-width'),
},
],
Expand Down
Expand Up @@ -18,12 +18,15 @@ const messages = ruleMessages(ruleName, {

const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-no-redundant-longhand-properties',
fixable: true,
};

const IGNORED_VALUES = new Set(['inherit']);

/** @typedef {import('postcss').Declaration} Declaration */

/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
Expand All @@ -42,7 +45,7 @@ const rule = (primary, secondaryOptions) => {
return;
}

/** @type {Map<string, string[]>} */
/** @type {Map<string, import('stylelint').ShorthandProperties[]>} */
Copy link
Member Author

Choose a reason for hiding this comment

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

Pretty sure the previous type was slightly incorrect, but could be wrong!

const longhandToShorthands = new Map();

for (const [shorthand, longhandProps] of longhandSubPropertiesOfShorthandProperties.entries()) {
Expand All @@ -61,6 +64,8 @@ const rule = (primary, secondaryOptions) => {
eachDeclarationBlock(root, (eachDecl) => {
/** @type {Map<string, string[]>} */
const longhandDeclarations = new Map();
/** @type {Map<string, Declaration[]>} */
const longhandDeclarationNodes = new Map();

eachDecl((decl) => {
if (IGNORED_VALUES.has(decl.value)) {
Expand All @@ -80,21 +85,52 @@ const rule = (primary, secondaryOptions) => {
for (const shorthandProperty of shorthandProperties) {
const prefixedShorthandProperty = prefix + shorthandProperty;
const longhandDeclaration = longhandDeclarations.get(prefixedShorthandProperty) || [];
const longhandDeclarationNode =
longhandDeclarationNodes.get(prefixedShorthandProperty) || [];

longhandDeclaration.push(prop);
longhandDeclarations.set(prefixedShorthandProperty, longhandDeclaration);

const shorthandProps = /** @type {Map<string, Set<string>>} */ (
longhandSubPropertiesOfShorthandProperties
).get(shorthandProperty);
longhandDeclarationNode.push(decl);
longhandDeclarationNodes.set(prefixedShorthandProperty, longhandDeclarationNode);

const shorthandProps = longhandSubPropertiesOfShorthandProperties.get(shorthandProperty);
const prefixedShorthandData = Array.from(shorthandProps || []).map(
(item) => prefix + item,
);

if (!arrayEqual(prefixedShorthandData.sort(), longhandDeclaration.sort())) {
const copiedPrefixedShorthandData = [...prefixedShorthandData];

if (!arrayEqual(copiedPrefixedShorthandData.sort(), longhandDeclaration.sort())) {
continue;
}

if (context.fix) {
const declNodes = longhandDeclarationNodes.get(prefixedShorthandProperty) || [];
const [firstDeclNode] = declNodes;

if (firstDeclNode) {
const transformedDeclarationNodes = new Map(
declNodes.map((d) => [d.prop.toLowerCase(), d]),
);
const resolvedShorthandValue = prefixedShorthandData
.map((p) => transformedDeclarationNodes.get(p)?.value.trim())
.filter(Boolean)
.join(' ');

const newShorthandDeclarationNode = firstDeclNode.clone({
prop: prefixedShorthandProperty,
value: resolvedShorthandValue,
});

firstDeclNode.replaceWith(newShorthandDeclarationNode);

declNodes.forEach((node) => node.remove());

return;
}
}

report({
ruleName,
result,
Expand Down
8 changes: 6 additions & 2 deletions types/stylelint/index.d.ts
Expand Up @@ -424,7 +424,7 @@ declare module 'stylelint' {
};

/** @internal */
export type LonghandSubPropertiesOfShorthandProperties = ReadonlyMap<
export type ShorthandProperties =
| 'animation'
| 'background'
| 'border'
Expand Down Expand Up @@ -459,7 +459,11 @@ declare module 'stylelint' {
| 'padding'
| 'text-decoration'
| 'text-emphasis'
| 'transition',
| 'transition';

/** @internal */
export type LonghandSubPropertiesOfShorthandProperties = ReadonlyMap<
ShorthandProperties,
ReadonlySet<string>
>;

Expand Down