diff --git a/lib/rules/declaration-block-no-duplicate-properties/README.md b/lib/rules/declaration-block-no-duplicate-properties/README.md index df178e3d83..161ce2dbe1 100644 --- a/lib/rules/declaration-block-no-duplicate-properties/README.md +++ b/lib/rules/declaration-block-no-duplicate-properties/README.md @@ -108,6 +108,43 @@ p { } ``` +### `ignore: ["consecutive-duplicates-with-same-prefixless-values"]` + +Ignore consecutive duplicated properties with identical values, when ignoring their prefix. + +This option is useful to deal with draft CSS values while still being future proof. E.g. using `fit-content` and `-moz-fit-content`. + +The following patterns are considered violations: + + +```css +/* nonconsecutive duplicates */ +p { + width: fit-content; + height: 32px; + width: -moz-fit-content; +} +``` + + +```css +/* properties with different prefixless values */ +p { + width: -moz-fit-content; + width: 100%; +} +``` + +The following patterns are _not_ considered violations: + + +```css +p { + width: -moz-fit-content; + width: fit-content; +} +``` + ### `ignoreProperties: ["/regex/", "non-regex"]` Ignore duplicates of specific properties. 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 c9e83018a3..a62f299a95 100644 --- a/lib/rules/declaration-block-no-duplicate-properties/__tests__/index.js +++ b/lib/rules/declaration-block-no-duplicate-properties/__tests__/index.js @@ -168,7 +168,43 @@ testRule({ }, { code: 'p { font-size: 16px; font-size: 16px; font-weight: 400; }', - message: messages.rejected('16px'), + message: messages.rejected('font-size'), + }, + ], +}); + +testRule({ + ruleName, + config: [true, { ignore: ['consecutive-duplicates-with-same-prefixless-values'] }], + skipBasicChecks: true, + + accept: [ + { + code: 'p { width: -moz-fit-content; width: fit-content; }', + }, + { + code: 'p { width: fit-content; width: -moz-fit-content; }', + }, + { + code: 'p { width: -MOZ-fit-content; width: fit-content; }', + }, + { + code: 'p { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; }', + }, + ], + + reject: [ + { + code: 'p { width: fit-content; height: 32px; width: -moz-fit-content; }', + message: messages.rejected('width'), + }, + { + code: 'p { width: 100%; width: -moz-fit-content; height: 32px; }', + message: messages.rejected('width'), + }, + { + code: 'p { width: -moz-fit-content; width: -moz-fit-content; }', + message: messages.rejected('width'), }, ], }); diff --git a/lib/rules/declaration-block-no-duplicate-properties/index.js b/lib/rules/declaration-block-no-duplicate-properties/index.js index b6a0ae4441..b7fea7e1a4 100644 --- a/lib/rules/declaration-block-no-duplicate-properties/index.js +++ b/lib/rules/declaration-block-no-duplicate-properties/index.js @@ -8,6 +8,7 @@ const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); const { isString } = require('../../utils/validateTypes'); +const vendor = require('../../utils/vendor'); const ruleName = 'declaration-block-no-duplicate-properties'; @@ -25,7 +26,11 @@ const rule = (primary, secondaryOptions) => { { actual: secondaryOptions, possible: { - ignore: ['consecutive-duplicates', 'consecutive-duplicates-with-different-values'], + ignore: [ + 'consecutive-duplicates', + 'consecutive-duplicates-with-different-values', + 'consecutive-duplicates-with-same-prefixless-values', + ], ignoreProperties: [isString], }, optional: true, @@ -36,6 +41,18 @@ const rule = (primary, secondaryOptions) => { return; } + const ignoreDuplicates = optionsMatches(secondaryOptions, 'ignore', 'consecutive-duplicates'); + const ignoreDiffValues = optionsMatches( + secondaryOptions, + 'ignore', + 'consecutive-duplicates-with-different-values', + ); + const ignorePrefixlessSameValues = optionsMatches( + secondaryOptions, + 'ignore', + 'consecutive-duplicates-with-same-prefixless-values', + ); + eachDeclarationBlock(root, (eachDecl) => { /** @type {string[]} */ const decls = []; @@ -67,14 +84,8 @@ const rule = (primary, secondaryOptions) => { const indexDuplicate = decls.indexOf(prop.toLowerCase()); if (indexDuplicate !== -1) { - if ( - optionsMatches( - secondaryOptions, - 'ignore', - 'consecutive-duplicates-with-different-values', - ) - ) { - // if duplicates are not consecutive + if (ignoreDiffValues || ignorePrefixlessSameValues) { + // fails if duplicates are not consecutive if (indexDuplicate !== decls.length - 1) { report({ message: messages.rejected(prop), @@ -86,10 +97,26 @@ const rule = (primary, secondaryOptions) => { return; } - // if values of consecutive duplicates are equal - if (value === values[indexDuplicate]) { + const duplicateValue = values[indexDuplicate]; + + if (ignorePrefixlessSameValues) { + // fails if values of consecutive, unprefixed duplicates are equal + if (vendor.unprefixed(value) !== vendor.unprefixed(duplicateValue)) { + report({ + message: messages.rejected(prop), + node: decl, + result, + ruleName, + }); + + return; + } + } + + // fails if values of consecutive duplicates are equal + if (value === duplicateValue) { report({ - message: messages.rejected(value), + message: messages.rejected(prop), node: decl, result, ruleName, @@ -101,10 +128,7 @@ const rule = (primary, secondaryOptions) => { return; } - if ( - optionsMatches(secondaryOptions, 'ignore', 'consecutive-duplicates') && - indexDuplicate === decls.length - 1 - ) { + if (ignoreDuplicates && indexDuplicate === decls.length - 1) { return; }