diff --git a/.changeset/silent-flies-float.md b/.changeset/silent-flies-float.md new file mode 100644 index 0000000000..771509b885 --- /dev/null +++ b/.changeset/silent-flies-float.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Fixed: `declaration-block-no-shorthand-property-overrides` false negatives for `font` and `border` diff --git a/lib/reference/properties.cjs b/lib/reference/properties.cjs index 4cc6aa7c8a..0299d9e3d4 100644 --- a/lib/reference/properties.cjs +++ b/lib/reference/properties.cjs @@ -15,6 +15,40 @@ const acceptCustomIdentsProperties = new Set([ 'list-style-type', ]); +const shorthandToResetToInitialProperty = new Map([ + [ + 'border', + new Set([ + 'border-image', + 'border-image-outset', + 'border-image-repeat', + 'border-image-slice', + 'border-image-source', + 'border-image-width', + ]), + ], + [ + /** @see https://www.w3.org/TR/css-fonts-4/#font-prop */ + 'font', + new Set([ + // prettier-ignore + 'font-feature-settings', + 'font-kerning', + 'font-language-override', + 'font-optical-sizing', + 'font-size-adjust', + 'font-variant-alternates', + 'font-variant-caps', + 'font-variant-east-asian', + 'font-variant-emoji', + 'font-variant-ligatures', + 'font-variant-numeric', + 'font-variant-position', + 'font-variation-settings', + ]), + ], +]); + /** @type {import('stylelint').LonghandSubPropertiesOfShorthandProperties} */ const longhandSubPropertiesOfShorthandProperties = new Map([ // Sort alphabetically @@ -247,6 +281,13 @@ const longhandSubPropertiesOfShorthandProperties = new Map([ new Set([ // prettier-ignore 'font-style', + /** + * reset explicitly: normal | small-caps + * reset implicitly: all-small-caps | petite-caps | all-petite-caps | unicase | titling-caps + * i.e. either way it will be reset + * @todo add font-variant shorthand to longhandSubPropertiesOfShorthandProperties + * {@link https://www.w3.org/TR/css-fonts-4/#font-variant-prop World Wide Web Consortium} + */ 'font-variant', 'font-weight', 'font-stretch', @@ -573,3 +614,4 @@ exports.acceptCustomIdentsProperties = acceptCustomIdentsProperties; exports.longhandSubPropertiesOfShorthandProperties = longhandSubPropertiesOfShorthandProperties; exports.longhandTimeProperties = longhandTimeProperties; exports.shorthandTimeProperties = shorthandTimeProperties; +exports.shorthandToResetToInitialProperty = shorthandToResetToInitialProperty; diff --git a/lib/reference/properties.mjs b/lib/reference/properties.mjs index 3bd426482d..c1547e3e8e 100644 --- a/lib/reference/properties.mjs +++ b/lib/reference/properties.mjs @@ -11,6 +11,40 @@ export const acceptCustomIdentsProperties = new Set([ 'list-style-type', ]); +export const shorthandToResetToInitialProperty = new Map([ + [ + 'border', + new Set([ + 'border-image', + 'border-image-outset', + 'border-image-repeat', + 'border-image-slice', + 'border-image-source', + 'border-image-width', + ]), + ], + [ + /** @see https://www.w3.org/TR/css-fonts-4/#font-prop */ + 'font', + new Set([ + // prettier-ignore + 'font-feature-settings', + 'font-kerning', + 'font-language-override', + 'font-optical-sizing', + 'font-size-adjust', + 'font-variant-alternates', + 'font-variant-caps', + 'font-variant-east-asian', + 'font-variant-emoji', + 'font-variant-ligatures', + 'font-variant-numeric', + 'font-variant-position', + 'font-variation-settings', + ]), + ], +]); + /** @type {import('stylelint').LonghandSubPropertiesOfShorthandProperties} */ export const longhandSubPropertiesOfShorthandProperties = new Map([ // Sort alphabetically @@ -243,6 +277,13 @@ export const longhandSubPropertiesOfShorthandProperties = new Map([ new Set([ // prettier-ignore 'font-style', + /** + * reset explicitly: normal | small-caps + * reset implicitly: all-small-caps | petite-caps | all-petite-caps | unicase | titling-caps + * i.e. either way it will be reset + * @todo add font-variant shorthand to longhandSubPropertiesOfShorthandProperties + * {@link https://www.w3.org/TR/css-fonts-4/#font-variant-prop World Wide Web Consortium} + */ 'font-variant', 'font-weight', 'font-stretch', diff --git a/lib/rules/declaration-block-no-shorthand-property-overrides/__tests__/index.mjs b/lib/rules/declaration-block-no-shorthand-property-overrides/__tests__/index.mjs index db5393677c..d3cbd47a54 100644 --- a/lib/rules/declaration-block-no-shorthand-property-overrides/__tests__/index.mjs +++ b/lib/rules/declaration-block-no-shorthand-property-overrides/__tests__/index.mjs @@ -80,6 +80,22 @@ testRule({ endLine: 1, endColumn: 33, }, + { + code: 'a { border-image: url("foo.png"); border: 1px solid black; }', + message: messages.rejected('border', 'border-image'), + line: 1, + column: 35, + endLine: 1, + endColumn: 41, + }, + { + code: 'a { border-image-source: url("foo.png"); border: 1px solid black; }', + message: messages.rejected('border', 'border-image-source'), + line: 1, + column: 42, + endLine: 1, + endColumn: 48, + }, { code: 'a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; }', message: messages.rejected('pAdDiNg', 'pAdDiNg-lEfT'), @@ -161,6 +177,39 @@ testRule({ endLine: 1, endColumn: 61, }, + { + code: 'a { font-variant: small-caps; font: sans-serif; }', + message: messages.rejected('font', 'font-variant'), + description: 'CSS2 explicit reset', + line: 1, + column: 31, + endLine: 1, + endColumn: 35, + }, + { + code: 'a { font-variant: all-small-caps; font: sans-serif; }', + message: messages.rejected('font', 'font-variant'), + description: 'CSS3 implicit reset', + line: 1, + column: 35, + endLine: 1, + endColumn: 39, + }, + { + code: 'a { font-size-adjust: 0.545; font: Verdana; }', + message: messages.rejected('font', 'font-size-adjust'), + line: 1, + column: 30, + endLine: 1, + endColumn: 34, + }, + { + code: 'a { font-variant-caps: small-caps; font-variant: normal; }', + message: messages.rejected('font-variant', 'font-variant-caps'), + line: 1, + endLine: 1, + skip: true, + }, ], }); diff --git a/lib/rules/declaration-block-no-shorthand-property-overrides/index.cjs b/lib/rules/declaration-block-no-shorthand-property-overrides/index.cjs index 2e2c72dd7b..4a716e7a32 100644 --- a/lib/rules/declaration-block-no-shorthand-property-overrides/index.cjs +++ b/lib/rules/declaration-block-no-shorthand-property-overrides/index.cjs @@ -3,11 +3,12 @@ 'use strict'; const eachDeclarationBlock = require('../../utils/eachDeclarationBlock.cjs'); -const properties = require('../../reference/properties.cjs'); const report = require('../../utils/report.cjs'); const ruleMessages = require('../../utils/ruleMessages.cjs'); +const uniteSets = require('../../utils/uniteSets.cjs'); const validateOptions = require('../../utils/validateOptions.cjs'); const vendor = require('../../utils/vendor.cjs'); +const properties = require('../../reference/properties.cjs'); const ruleName = 'declaration-block-no-shorthand-property-overrides'; @@ -36,19 +37,18 @@ const rule = (primary) => { const prop = decl.prop; const unprefixedProp = vendor.unprefixed(prop).toLowerCase(); const prefix = vendor.prefix(prop).toLowerCase(); - - const overrideables = /** @type {Map>} */ ( + const subProperties = /** @type {Map>} */ ( properties.longhandSubPropertiesOfShorthandProperties ).get(unprefixedProp); + const resettables = properties.shorthandToResetToInitialProperty.get(unprefixedProp); + const union = uniteSets(subProperties ?? [], resettables ?? []); declarations.set(prop.toLowerCase(), prop); - if (!overrideables) { - return; - } + if (union.size === 0) return; - for (const longhandProp of overrideables) { - const declaration = declarations.get(prefix + longhandProp); + for (const property of union) { + const declaration = declarations.get(prefix + property); if (!declaration) { continue; diff --git a/lib/rules/declaration-block-no-shorthand-property-overrides/index.mjs b/lib/rules/declaration-block-no-shorthand-property-overrides/index.mjs index 22628706c4..6e13e27a4b 100644 --- a/lib/rules/declaration-block-no-shorthand-property-overrides/index.mjs +++ b/lib/rules/declaration-block-no-shorthand-property-overrides/index.mjs @@ -1,10 +1,15 @@ import eachDeclarationBlock from '../../utils/eachDeclarationBlock.mjs'; -import { longhandSubPropertiesOfShorthandProperties } from '../../reference/properties.mjs'; import report from '../../utils/report.mjs'; import ruleMessages from '../../utils/ruleMessages.mjs'; +import uniteSets from '../../utils/uniteSets.mjs'; import validateOptions from '../../utils/validateOptions.mjs'; import vendor from '../../utils/vendor.mjs'; +import { + longhandSubPropertiesOfShorthandProperties, + shorthandToResetToInitialProperty, +} from '../../reference/properties.mjs'; + const ruleName = 'declaration-block-no-shorthand-property-overrides'; const messages = ruleMessages(ruleName, { @@ -32,19 +37,18 @@ const rule = (primary) => { const prop = decl.prop; const unprefixedProp = vendor.unprefixed(prop).toLowerCase(); const prefix = vendor.prefix(prop).toLowerCase(); - - const overrideables = /** @type {Map>} */ ( + const subProperties = /** @type {Map>} */ ( longhandSubPropertiesOfShorthandProperties ).get(unprefixedProp); + const resettables = shorthandToResetToInitialProperty.get(unprefixedProp); + const union = uniteSets(subProperties ?? [], resettables ?? []); declarations.set(prop.toLowerCase(), prop); - if (!overrideables) { - return; - } + if (union.size === 0) return; - for (const longhandProp of overrideables) { - const declaration = declarations.get(prefix + longhandProp); + for (const property of union) { + const declaration = declarations.get(prefix + property); if (!declaration) { continue; diff --git a/lib/utils/uniteSets.cjs b/lib/utils/uniteSets.cjs index 2b786a81c3..17c8b02dd5 100644 --- a/lib/utils/uniteSets.cjs +++ b/lib/utils/uniteSets.cjs @@ -6,6 +6,7 @@ * Unite two or more sets * * @param {Iterable[]} args + * @see {@link https://github.com/microsoft/TypeScript/issues/57228|GitHub} */ function uniteSets(...args) { return new Set([...args].reduce((result, set) => [...result, ...set], [])); diff --git a/lib/utils/uniteSets.mjs b/lib/utils/uniteSets.mjs index 3e3c1583c8..2fc9522be6 100644 --- a/lib/utils/uniteSets.mjs +++ b/lib/utils/uniteSets.mjs @@ -2,6 +2,7 @@ * Unite two or more sets * * @param {Iterable[]} args + * @see {@link https://github.com/microsoft/TypeScript/issues/57228|GitHub} */ export default function uniteSets(...args) { return new Set([...args].reduce((result, set) => [...result, ...set], []));