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 {