diff --git a/.changeset/nice-lemons-reflect.md b/.changeset/nice-lemons-reflect.md
new file mode 100644
index 0000000000..6f75e4c2c8
--- /dev/null
+++ b/.changeset/nice-lemons-reflect.md
@@ -0,0 +1,5 @@
+---
+"stylelint": minor
+---
+
+Added: `declaration-block-no-duplicate-properties` autofix
diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md
index 6b0b3e5e27..623ce68a89 100644
--- a/docs/user-guide/rules/list.md
+++ b/docs/user-guide/rules/list.md
@@ -61,7 +61,7 @@ Within each cateogory, the rules are grouped by the [_thing_](http://apps.workfl
### Declaration block
- [`declaration-block-no-duplicate-custom-properties`](../../../lib/rules/declaration-block-no-duplicate-custom-properties/README.md): Disallow duplicate custom properties within declaration blocks.
-- [`declaration-block-no-duplicate-properties`](../../../lib/rules/declaration-block-no-duplicate-properties/README.md): Disallow duplicate properties within declaration blocks.
+- [`declaration-block-no-duplicate-properties`](../../../lib/rules/declaration-block-no-duplicate-properties/README.md): Disallow duplicate properties within declaration blocks (Autofixable).
- [`declaration-block-no-shorthand-property-overrides`](../../../lib/rules/declaration-block-no-shorthand-property-overrides/README.md): Disallow shorthand properties that override related longhand properties.
### Block
diff --git a/lib/rules/declaration-block-no-duplicate-properties/README.md b/lib/rules/declaration-block-no-duplicate-properties/README.md
index a8bd6b7980..258f9d5cdd 100644
--- a/lib/rules/declaration-block-no-duplicate-properties/README.md
+++ b/lib/rules/declaration-block-no-duplicate-properties/README.md
@@ -11,6 +11,8 @@ a { color: pink; color: orange; }
This rule ignores variables (`$sass`, `@less`, `--custom-property`).
+The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix all of the problems reported by this rule.
+
## Options
### `true`
diff --git a/lib/rules/declaration-block-no-duplicate-properties/__tests__/index.js b/lib/rules/declaration-block-no-duplicate-properties/__tests__/index.js
index 15f4cec1e7..9049919421 100644
--- a/lib/rules/declaration-block-no-duplicate-properties/__tests__/index.js
+++ b/lib/rules/declaration-block-no-duplicate-properties/__tests__/index.js
@@ -5,6 +5,7 @@ const { messages, ruleName } = require('..');
testRule({
ruleName,
config: [true],
+ fix: true,
accept: [
{
@@ -48,6 +49,7 @@ testRule({
reject: [
{
code: 'a { color: pink; color: orange }',
+ fixed: 'a { color: orange }',
message: messages.rejected('color'),
line: 1,
column: 18,
@@ -56,6 +58,7 @@ testRule({
},
{
code: 'a { cOlOr: pink; color: orange }',
+ fixed: 'a { color: orange }',
message: messages.rejected('color'),
line: 1,
column: 18,
@@ -64,6 +67,7 @@ testRule({
},
{
code: 'a { color: pink; cOlOr: orange }',
+ fixed: 'a { cOlOr: orange }',
message: messages.rejected('cOlOr'),
line: 1,
column: 18,
@@ -72,6 +76,7 @@ testRule({
},
{
code: 'a { cOlOr: pink; cOlOr: orange }',
+ fixed: 'a { cOlOr: orange }',
message: messages.rejected('cOlOr'),
line: 1,
column: 18,
@@ -80,6 +85,7 @@ testRule({
},
{
code: 'a { COLOR: pink; color: orange }',
+ fixed: 'a { color: orange }',
message: messages.rejected('color'),
line: 1,
column: 18,
@@ -88,6 +94,7 @@ testRule({
},
{
code: 'a { color: pink; COLOR: orange }',
+ fixed: 'a { COLOR: orange }',
message: messages.rejected('COLOR'),
line: 1,
column: 18,
@@ -96,6 +103,7 @@ testRule({
},
{
code: 'a { color: pink; background: orange; color: orange }',
+ fixed: 'a { background: orange; color: orange }',
message: messages.rejected('color'),
line: 1,
column: 38,
@@ -104,6 +112,7 @@ testRule({
},
{
code: 'a { color: pink; background: orange; background: pink; }',
+ fixed: 'a { color: pink; background: pink; }',
message: messages.rejected('background'),
line: 1,
column: 38,
@@ -112,6 +121,7 @@ testRule({
},
{
code: 'a { color: pink; { &:hover { color: orange; color: black; } } }',
+ fixed: 'a { color: pink; { &:hover { color: black; } } }',
description: 'spec nested',
message: messages.rejected('color'),
line: 1,
@@ -121,6 +131,7 @@ testRule({
},
{
code: 'a { color: pink; @media { color: orange; color: black; } }',
+ fixed: 'a { color: pink; @media { color: black; } }',
description: 'nested',
message: messages.rejected('color'),
line: 1,
@@ -130,6 +141,7 @@ testRule({
},
{
code: '@media { color: orange; .foo { color: black; color: white; } }',
+ fixed: '@media { color: orange; .foo { color: white; } }',
description: 'nested',
message: messages.rejected('color'),
line: 1,
@@ -139,6 +151,7 @@ testRule({
},
{
code: 'a { color: pink; @media { color: orange; &::before { color: black; color: white; } } }',
+ fixed: 'a { color: pink; @media { color: orange; &::before { color: white; } } }',
description: 'double nested',
message: messages.rejected('color'),
line: 1,
@@ -148,6 +161,7 @@ testRule({
},
{
code: 'a { color: pink; @media { color: orange; .foo { color: black; color: white; } } }',
+ fixed: 'a { color: pink; @media { color: orange; .foo { color: white; } } }',
description: 'double nested again',
message: messages.rejected('color'),
line: 1,
@@ -157,6 +171,7 @@ testRule({
},
{
code: 'a { -webkit-border-radius: 12px; -webkit-border-radius: 10px; }',
+ fixed: 'a { -webkit-border-radius: 10px; }',
message: messages.rejected('-webkit-border-radius'),
line: 1,
column: 34,
@@ -165,6 +180,7 @@ testRule({
},
{
code: 'a { -WEBKIT-border-radius: 12px; -webkit-BORDER-radius: 10px; }',
+ fixed: 'a { -webkit-BORDER-radius: 10px; }',
message: messages.rejected('-webkit-BORDER-radius'),
line: 1,
column: 34,
@@ -177,6 +193,7 @@ testRule({
testRule({
ruleName,
config: [true, { ignore: ['consecutive-duplicates'] }],
+ fix: true,
accept: [
{
@@ -196,18 +213,22 @@ testRule({
reject: [
{
code: 'p { font-size: 16px; font-weight: 400; font-size: 1rem; }',
+ fixed: 'p { font-weight: 400; font-size: 1rem; }',
message: messages.rejected('font-size'),
},
{
code: 'p { display: inline-block; font-size: 16px; font-weight: 400; font-size: 1rem; }',
+ fixed: 'p { display: inline-block; font-weight: 400; font-size: 1rem; }',
message: messages.rejected('font-size'),
},
{
code: 'p { font-size: 16px; font-weight: 400; font-size: 1rem; color: red; }',
+ fixed: 'p { font-weight: 400; font-size: 1rem; color: red; }',
message: messages.rejected('font-size'),
},
{
code: 'p { display: inline-block; font-size: 16px; font-weight: 400; font-size: 1rem; color: red; }',
+ fixed: 'p { display: inline-block; font-weight: 400; font-size: 1rem; color: red; }',
message: messages.rejected('font-size'),
},
],
@@ -216,16 +237,19 @@ testRule({
testRule({
ruleName,
config: [true, { ignore: ['consecutive-duplicates-with-different-values'] }],
+ fix: true,
accept: [{ code: 'p { font-size: 16px; font-size: 1rem; font-weight: 400; }' }],
reject: [
{
code: 'p { font-size: 16px; font-weight: 400; font-size: 1rem; }',
+ fixed: 'p { font-weight: 400; font-size: 1rem; }',
message: messages.rejected('font-size'),
},
{
code: 'p { font-size: 16px; font-size: 16px; font-weight: 400; }',
+ fixed: 'p { font-size: 16px; font-weight: 400; }',
message: messages.rejected('font-size'),
},
],
@@ -234,6 +258,7 @@ testRule({
testRule({
ruleName,
config: [true, { ignore: ['consecutive-duplicates-with-same-prefixless-values'] }],
+ fix: true,
accept: [
{
@@ -253,14 +278,17 @@ testRule({
reject: [
{
code: 'p { width: fit-content; height: 32px; width: -moz-fit-content; }',
+ fixed: 'p { height: 32px; width: -moz-fit-content; }',
message: messages.rejected('width'),
},
{
code: 'p { width: 100%; width: -moz-fit-content; height: 32px; }',
+ fixed: 'p { width: -moz-fit-content; height: 32px; }',
message: messages.rejected('width'),
},
{
code: 'p { width: -moz-fit-content; width: -moz-fit-content; }',
+ fixed: 'p { width: -moz-fit-content; }',
message: messages.rejected('width'),
},
],
@@ -269,6 +297,7 @@ testRule({
testRule({
ruleName,
config: [true, { ignoreProperties: ['color'] }],
+ fix: true,
accept: [
{
@@ -285,10 +314,12 @@ testRule({
reject: [
{
code: 'p { color: pink; background: orange; background: white; }',
+ fixed: 'p { color: pink; background: white; }',
message: messages.rejected('background'),
},
{
code: 'p { background: orange; color: pink; background: white; }',
+ fixed: 'p { color: pink; background: white; }',
message: messages.rejected('background'),
},
],
@@ -297,6 +328,7 @@ testRule({
testRule({
ruleName,
config: [true, { ignoreProperties: ['/background-/'] }],
+ fix: true,
accept: [
{
@@ -310,10 +342,12 @@ testRule({
reject: [
{
code: 'p { color: pink; background: orange; background: white; }',
+ fixed: 'p { color: pink; background: white; }',
message: messages.rejected('background'),
},
{
code: 'p { background: orange; color: pink; background: white; }',
+ fixed: 'p { color: pink; background: white; }',
message: messages.rejected('background'),
},
],
@@ -323,6 +357,7 @@ testRule({
ruleName,
config: [true],
customSyntax: 'postcss-html',
+ fix: true,
accept: [
{
@@ -342,14 +377,17 @@ testRule({
reject: [
{
code: '',
+ fixed: '',
message: messages.rejected('color'),
},
{
code: '',
+ fixed: '',
message: messages.rejected('background'),
},
{
code: '',
+ fixed: '',
message: messages.rejected('background'),
},
],
diff --git a/lib/rules/declaration-block-no-duplicate-properties/index.js b/lib/rules/declaration-block-no-duplicate-properties/index.js
index 346c9dc4f8..81253bf7bf 100644
--- a/lib/rules/declaration-block-no-duplicate-properties/index.js
+++ b/lib/rules/declaration-block-no-duplicate-properties/index.js
@@ -18,10 +18,11 @@ const messages = ruleMessages(ruleName, {
const meta = {
url: 'https://stylelint.io/user-guide/rules/list/declaration-block-no-duplicate-properties',
+ fixable: true,
};
/** @type {import('stylelint').Rule} */
-const rule = (primary, secondaryOptions) => {
+const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
@@ -58,13 +59,12 @@ const rule = (primary, secondaryOptions) => {
);
eachDeclarationBlock(root, (eachDecl) => {
- /** @type {string[]} */
+ /** @type {import('postcss').Declaration[]} */
const decls = [];
- /** @type {string[]} */
- const values = [];
eachDecl((decl) => {
const prop = decl.prop;
+ const lowerProp = decl.prop.toLowerCase();
const value = decl.value;
if (!isStandardSyntaxProperty(prop)) {
@@ -81,16 +81,22 @@ const rule = (primary, secondaryOptions) => {
}
// Ignore the src property as commonly duplicated in at-fontface
- if (prop.toLowerCase() === 'src') {
+ if (lowerProp === 'src') {
return;
}
- const indexDuplicate = decls.indexOf(prop.toLowerCase());
+ const indexDuplicate = decls.findIndex((d) => d.prop.toLowerCase() === lowerProp);
if (indexDuplicate !== -1) {
if (ignoreDiffValues || ignorePrefixlessSameValues) {
// fails if duplicates are not consecutive
if (indexDuplicate !== decls.length - 1) {
+ if (context.fix) {
+ removePreviousDuplicate(decls, lowerProp);
+
+ return;
+ }
+
report({
message: messages.rejected(prop),
node: decl,
@@ -102,11 +108,18 @@ const rule = (primary, secondaryOptions) => {
return;
}
- const duplicateValue = values[indexDuplicate] || '';
+ const duplicateDecl = decls[indexDuplicate];
+ const duplicateValue = duplicateDecl ? duplicateDecl.value : '';
if (ignorePrefixlessSameValues) {
// fails if values of consecutive, unprefixed duplicates are equal
if (vendor.unprefixed(value) !== vendor.unprefixed(duplicateValue)) {
+ if (context.fix) {
+ removePreviousDuplicate(decls, lowerProp);
+
+ return;
+ }
+
report({
message: messages.rejected(prop),
node: decl,
@@ -121,6 +134,12 @@ const rule = (primary, secondaryOptions) => {
// fails if values of consecutive duplicates are equal
if (value === duplicateValue) {
+ if (context.fix) {
+ removePreviousDuplicate(decls, lowerProp);
+
+ return;
+ }
+
report({
message: messages.rejected(prop),
node: decl,
@@ -139,6 +158,12 @@ const rule = (primary, secondaryOptions) => {
return;
}
+ if (context.fix) {
+ removePreviousDuplicate(decls, lowerProp);
+
+ return;
+ }
+
report({
message: messages.rejected(prop),
node: decl,
@@ -148,13 +173,23 @@ const rule = (primary, secondaryOptions) => {
});
}
- decls.push(prop.toLowerCase());
- values.push(value.toLowerCase());
+ decls.push(decl);
});
});
};
};
+/**
+ * @param {import('postcss').Declaration[]} declarations
+ * @param {string} lowerProperty
+ * @returns {void}
+ * */
+function removePreviousDuplicate(declarations, lowerProperty) {
+ const declToRemove = declarations.find((d) => d.prop.toLowerCase() === lowerProperty);
+
+ if (declToRemove) declToRemove.remove();
+}
+
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
diff --git a/system-tests/001/__snapshots__/fs.test.js.snap b/system-tests/001/__snapshots__/fs.test.js.snap
index f1df3cf26e..324d5e9c75 100644
--- a/system-tests/001/__snapshots__/fs.test.js.snap
+++ b/system-tests/001/__snapshots__/fs.test.js.snap
@@ -110,6 +110,7 @@ Object {
"url": "https://stylelint.io/user-guide/rules/list/declaration-bang-space-before",
},
"declaration-block-no-duplicate-properties": Object {
+ "fixable": true,
"url": "https://stylelint.io/user-guide/rules/list/declaration-block-no-duplicate-properties",
},
"declaration-block-no-shorthand-property-overrides": Object {
diff --git a/system-tests/001/__snapshots__/no-fs.test.js.snap b/system-tests/001/__snapshots__/no-fs.test.js.snap
index f679ecc800..00c3a8368c 100644
--- a/system-tests/001/__snapshots__/no-fs.test.js.snap
+++ b/system-tests/001/__snapshots__/no-fs.test.js.snap
@@ -110,6 +110,7 @@ Object {
"url": "https://stylelint.io/user-guide/rules/list/declaration-bang-space-before",
},
"declaration-block-no-duplicate-properties": Object {
+ "fixable": true,
"url": "https://stylelint.io/user-guide/rules/list/declaration-block-no-duplicate-properties",
},
"declaration-block-no-shorthand-property-overrides": Object {
diff --git a/system-tests/003/__snapshots__/fs.test.js.snap b/system-tests/003/__snapshots__/fs.test.js.snap
index 5e45d3bfa7..9c8221fe41 100644
--- a/system-tests/003/__snapshots__/fs.test.js.snap
+++ b/system-tests/003/__snapshots__/fs.test.js.snap
@@ -130,6 +130,7 @@ Object {
"url": "https://stylelint.io/user-guide/rules/list/declaration-bang-space-before",
},
"declaration-block-no-duplicate-properties": Object {
+ "fixable": true,
"url": "https://stylelint.io/user-guide/rules/list/declaration-block-no-duplicate-properties",
},
"declaration-block-no-shorthand-property-overrides": Object {
diff --git a/system-tests/003/__snapshots__/no-fs.test.js.snap b/system-tests/003/__snapshots__/no-fs.test.js.snap
index b66e4de33d..89309b1463 100644
--- a/system-tests/003/__snapshots__/no-fs.test.js.snap
+++ b/system-tests/003/__snapshots__/no-fs.test.js.snap
@@ -322,6 +322,7 @@ footer a:visited {
"url": "https://stylelint.io/user-guide/rules/list/declaration-bang-space-before",
},
"declaration-block-no-duplicate-properties": Object {
+ "fixable": true,
"url": "https://stylelint.io/user-guide/rules/list/declaration-block-no-duplicate-properties",
},
"declaration-block-no-shorthand-property-overrides": Object {