diff --git a/.changeset/selfish-plums-dream.md b/.changeset/selfish-plums-dream.md new file mode 100644 index 0000000000..a40c0f982f --- /dev/null +++ b/.changeset/selfish-plums-dream.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Fixed: `declaration-block-no-redundant-longhand-properties` autofix for `text-decoration` diff --git a/.changeset/sharp-buses-drop.md b/.changeset/sharp-buses-drop.md new file mode 100644 index 0000000000..f40d93703d --- /dev/null +++ b/.changeset/sharp-buses-drop.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Added: `ignoreLonghands: []` to `declaration-block-no-redundant-longhand-properties` diff --git a/lib/reference/properties.cjs b/lib/reference/properties.cjs index 0299d9e3d4..667e41ceff 100644 --- a/lib/reference/properties.cjs +++ b/lib/reference/properties.cjs @@ -579,6 +579,7 @@ const longhandSubPropertiesOfShorthandProperties = new Map([ 'text-decoration-line', 'text-decoration-style', 'text-decoration-color', + 'text-decoration-thickness', ]), ], [ diff --git a/lib/reference/properties.mjs b/lib/reference/properties.mjs index c1547e3e8e..1edbfbdbc2 100644 --- a/lib/reference/properties.mjs +++ b/lib/reference/properties.mjs @@ -575,6 +575,7 @@ export const longhandSubPropertiesOfShorthandProperties = new Map([ 'text-decoration-line', 'text-decoration-style', 'text-decoration-color', + 'text-decoration-thickness', ]), ], [ diff --git a/lib/rules/declaration-block-no-redundant-longhand-properties/README.md b/lib/rules/declaration-block-no-redundant-longhand-properties/README.md index 4b4810f2e3..0531041392 100644 --- a/lib/rules/declaration-block-no-redundant-longhand-properties/README.md +++ b/lib/rules/declaration-block-no-redundant-longhand-properties/README.md @@ -174,6 +174,60 @@ a { ## Optional secondary options +### `ignoreLonghands: ["string"]` + +Given: + + +```json +["text-decoration-thickness", "background-size", "background-origin", "background-clip"] +``` + +The following patterns are considered problems: + + +```css +a { + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-color: purple; +} +``` + + +```css +a { + background-repeat: repeat; + background-attachment: scroll; + background-position: 0% 0%; + background-color: transparent; + background-image: none; + background-size: contain; + background-origin: border-box; + background-clip: text; +} +``` + +The following patterns are _not_ considered problems: + + +```css +a { + text-decoration: underline solid purple; + text-decoration-thickness: 1px; +} +``` + + +```css +a { + background: none 0% 0% repeat scroll transparent; + background-size: contain; + background-origin: border-box; + background-clip: text; +} +``` + ### `ignoreShorthands: ["/regex/", /regex/, "string"]` Given: diff --git a/lib/rules/declaration-block-no-redundant-longhand-properties/__tests__/index.mjs b/lib/rules/declaration-block-no-redundant-longhand-properties/__tests__/index.mjs index 8f53365494..a0b32f33ee 100644 --- a/lib/rules/declaration-block-no-redundant-longhand-properties/__tests__/index.mjs +++ b/lib/rules/declaration-block-no-redundant-longhand-properties/__tests__/index.mjs @@ -59,6 +59,14 @@ testRule({ code: 'a { transition-delay: 500ms, 1s; transition-duration: 250ms,2s; transition-timing-function: ease-in-out; transition-property: inherit; }', description: 'transition property contains basic keyword (inherit)', }, + { + code: 'a { text-decoration-line: underline; text-decoration-style: solid; text-decoration-color: purple; }', + description: 'missing text-decoration-thickness', + }, + { + code: 'a { background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; background-color: transparent; background-image: none; background-origin: border-box; background-clip: text; }', + description: 'missing background-size', + }, ], reject: [ @@ -387,6 +395,50 @@ testRule({ 'the repeat() notation has non-trivial semantics and is currently not fixable (rows)', message: messages.expected('grid-template'), }, + { + code: 'a { text-decoration-color: purple; text-decoration-thickness: 1px; text-decoration-style: solid; text-decoration-line: underline; }', + fixed: 'a { text-decoration: underline solid purple 1px; }', + message: messages.expected('text-decoration'), + description: 'CSS Text Decoration Module Level 4', + }, + ], +}); + +testRule({ + ruleName, + config: [ + true, + { + ignoreLonghands: [ + 'background-size', + 'background-origin', + 'background-clip', + 'text-decoration-thickness', + ], + }, + ], + fix: true, + + reject: [ + { + code: 'a { text-decoration-line: underline; text-decoration-style: solid; text-decoration-color: purple; }', + fixed: 'a { text-decoration: underline solid purple; }', + description: 'CSS Text Decoration Module Level 3', + message: messages.expected('text-decoration'), + }, + { + code: 'a { text-decoration-color: purple; text-decoration-thickness: 1px; text-decoration-style: solid; text-decoration-line: underline; text-decoration-width: 1px; }', + fixed: + 'a { text-decoration: underline solid purple; text-decoration-thickness: 1px; text-decoration-width: 1px; }', + description: 'ignore text-decoration-width by default', + message: messages.expected('text-decoration'), + }, + { + code: 'a { background-repeat: repeat; background-attachment: scroll; background-position: 0% 0%; background-color: transparent; background-image: none; background-size: contain; background-origin: border-box; background-clip: text; }', + fixed: + 'a { background: none 0% 0% repeat scroll transparent; background-size: contain; background-origin: border-box; background-clip: text; }', + message: messages.expected('background'), + }, ], }); diff --git a/lib/rules/declaration-block-no-redundant-longhand-properties/index.cjs b/lib/rules/declaration-block-no-redundant-longhand-properties/index.cjs index 5423d9d68a..e9399b716a 100644 --- a/lib/rules/declaration-block-no-redundant-longhand-properties/index.cjs +++ b/lib/rules/declaration-block-no-redundant-longhand-properties/index.cjs @@ -219,6 +219,7 @@ const rule = (primary, secondaryOptions, context) => { actual: secondaryOptions, possible: { ignoreShorthands: [validateTypes.isString, validateTypes.isRegExp], + ignoreLonghands: [validateTypes.isString], }, optional: true, }, @@ -230,6 +231,8 @@ const rule = (primary, secondaryOptions, context) => { /** @type {Map} */ const longhandToShorthands = new Map(); + /** @type {string[]} */ + const ignoreLonghands = secondaryOptions?.ignoreLonghands ?? []; for (const [shorthand, longhandProps] of properties.longhandSubPropertiesOfShorthandProperties.entries()) { if (optionsMatches(secondaryOptions, 'ignoreShorthands', shorthand)) { @@ -237,6 +240,8 @@ const rule = (primary, secondaryOptions, context) => { } for (const longhand of longhandProps) { + if (ignoreLonghands.includes(longhand)) continue; + const shorthands = longhandToShorthands.get(longhand) || []; shorthands.push(shorthand); @@ -278,11 +283,15 @@ const rule = (primary, secondaryOptions, context) => { longhandDeclarationNode.push(decl); longhandDeclarationNodes.set(prefixedShorthandProperty, longhandDeclarationNode); - const shorthandProps = properties.longhandSubPropertiesOfShorthandProperties.get(shorthandProperty); - const prefixedShorthandData = Array.from(shorthandProps || [], (item) => prefix + item); + const shorthandProps = new Set( + properties.longhandSubPropertiesOfShorthandProperties.get(shorthandProperty), + ); + ignoreLonghands.forEach((value) => shorthandProps.delete(value)); + const prefixedShorthandData = Array.from(shorthandProps, (item) => prefix + item); const copiedPrefixedShorthandData = [...prefixedShorthandData]; + // TODO use toSorted in the next major that supports it if (!arrayEqual(copiedPrefixedShorthandData.sort(), longhandDeclaration.sort())) { continue; } diff --git a/lib/rules/declaration-block-no-redundant-longhand-properties/index.mjs b/lib/rules/declaration-block-no-redundant-longhand-properties/index.mjs index 95dae1526d..b40c472026 100644 --- a/lib/rules/declaration-block-no-redundant-longhand-properties/index.mjs +++ b/lib/rules/declaration-block-no-redundant-longhand-properties/index.mjs @@ -216,6 +216,7 @@ const rule = (primary, secondaryOptions, context) => { actual: secondaryOptions, possible: { ignoreShorthands: [isString, isRegExp], + ignoreLonghands: [isString], }, optional: true, }, @@ -227,6 +228,8 @@ const rule = (primary, secondaryOptions, context) => { /** @type {Map} */ const longhandToShorthands = new Map(); + /** @type {string[]} */ + const ignoreLonghands = secondaryOptions?.ignoreLonghands ?? []; for (const [shorthand, longhandProps] of longhandSubPropertiesOfShorthandProperties.entries()) { if (optionsMatches(secondaryOptions, 'ignoreShorthands', shorthand)) { @@ -234,6 +237,8 @@ const rule = (primary, secondaryOptions, context) => { } for (const longhand of longhandProps) { + if (ignoreLonghands.includes(longhand)) continue; + const shorthands = longhandToShorthands.get(longhand) || []; shorthands.push(shorthand); @@ -275,11 +280,15 @@ const rule = (primary, secondaryOptions, context) => { longhandDeclarationNode.push(decl); longhandDeclarationNodes.set(prefixedShorthandProperty, longhandDeclarationNode); - const shorthandProps = longhandSubPropertiesOfShorthandProperties.get(shorthandProperty); - const prefixedShorthandData = Array.from(shorthandProps || [], (item) => prefix + item); + const shorthandProps = new Set( + longhandSubPropertiesOfShorthandProperties.get(shorthandProperty), + ); + ignoreLonghands.forEach((value) => shorthandProps.delete(value)); + const prefixedShorthandData = Array.from(shorthandProps, (item) => prefix + item); const copiedPrefixedShorthandData = [...prefixedShorthandData]; + // TODO use toSorted in the next major that supports it if (!arrayEqual(copiedPrefixedShorthandData.sort(), longhandDeclaration.sort())) { continue; }