Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignore: ["consecutive-duplicates-with-different-syntaxes"] to declaration-block-no-duplicate-properties #6772

Merged
merged 12 commits into from
Apr 12, 2023
5 changes: 5 additions & 0 deletions .changeset/kind-cobras-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `ignore: ["consecutive-duplicates-with-different-syntaxes"]` to `declaration-block-no-duplicate-properties`
27 changes: 27 additions & 0 deletions lib/rules/declaration-block-no-duplicate-properties/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,33 @@ p {
}
```

### `ignore: ["consecutive-duplicates-with-different-syntaxes"]`

Ignore consecutive duplicated properties with different value syntaxes (type and unit of value).

The following patterns are considered problems:

<!-- prettier-ignore -->
```css
/* properties with the same value syntax */
p {
font-size: 16px;
font-size: 14px;
font-weight: 400;
}
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
```css
p {
font-size: 16px;
font-size: 16rem;
font-weight: 400;
}
```

### `ignore: ["consecutive-duplicates-with-same-prefixless-values"]`

Ignore consecutive duplicated properties with identical values, when ignoring their prefix.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@ testRule({
config: [true, { ignore: ['consecutive-duplicates-with-different-values'] }],
fix: true,

accept: [{ code: 'p { font-size: 16px; font-size: 1rem; font-weight: 400; }' }],
accept: [
{ code: 'p { font-size: 16px; font-size: 1rem; font-weight: 400; }' },
{ code: 'p { font-size: 16px; font-size: 18px; font-weight: 400; }' },
],

reject: [
{
Expand All @@ -288,6 +291,73 @@ testRule({
],
});

testRule({
ruleName,
config: [true, { ignore: ['consecutive-duplicates-with-different-syntaxes'] }],
fix: true,

accept: [
{ code: 'p { width: 100vw; height: 100vh; }' },
{ code: 'p { width: 100vw; width: 100dvw; height: 100vh; }' },
{ code: 'p { margin: 10dvw 10dvw; margin: 10vw 10vw; padding: 0; }' },
{ code: 'p { margin: 10dvh 10dvw 10dvh; margin: 10vw 10vw; padding: 0; }' },
{ code: 'p { width: 100%; width: fit-content; }' },
{ code: 'p { width: calc(10px + 2px); width: calc(10px + 2rem); }' },
{ code: 'p { width: calc(10px + 2px); width: calc(10rem + 2rem); }' },
{ code: 'p { width: min(10px, 11px); width: max(10px, 11px); }' },
{ code: 'p { width: calc((10px + 2px)); width: calc((10rem + 2rem)); }' },
{ code: 'p { width: calc((10px + 2px) + 10px); width: calc((10rem + 2rem) + 10px); }' },
],

reject: [
{
code: 'p { width: 100vw; height: 100vh; width: 100dvw; }',
fixed: 'p { height: 100vh; width: 100dvw; }',
message: messages.rejected('width'),
},
{
code: 'p { width: 100vw; width: 100vw; height: 100vh; }',
fixed: 'p { width: 100vw; height: 100vh; }',
message: messages.rejected('width'),
},
{
code: 'p { margin: 10vw 10vw; margin: 10vw 10vw; padding: 0; }',
fixed: 'p { margin: 10vw 10vw; padding: 0; }',
message: messages.rejected('margin'),
},
{
code: 'p { width: 100vw; width: 50vw; height: 100vh; }',
fixed: 'p { width: 50vw; height: 100vh; }',
message: messages.rejected('width'),
},
{
code: 'p { width: 100vw !important; height: 100vh; width: 100dvw; }',
fixed: 'p { width: 100vw !important; height: 100vh; }',
message: messages.rejected('width'),
},
{
code: 'p { width: 100vw; height: 100vh; width: 100dvw !important; }',
fixed: 'p { height: 100vh; width: 100dvw !important; }',
message: messages.rejected('width'),
},
{
code: 'p { width: min-content; width: max-content; height: 100%; }',
fixed: 'p { width: max-content; height: 100%; }',
message: messages.rejected('width'),
},
{
code: 'p { width: calc(10px + 4rem); width: calc(10px + 2rem); }',
fixed: 'p { width: calc(10px + 2rem); }',
message: messages.rejected('width'),
},
{
code: 'p { width: CaLC(10px + 4rem); width: calc(10px + 2rem); }',
fixed: 'p { width: calc(10px + 2rem); }',
message: messages.rejected('width'),
},
],
});

testRule({
ruleName,
config: [true, { ignore: ['consecutive-duplicates-with-same-prefixless-values'] }],
Expand Down
81 changes: 80 additions & 1 deletion lib/rules/declaration-block-no-duplicate-properties/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const { parse, List } = require('css-tree');
const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
const isCustomProperty = require('../../utils/isCustomProperty');
const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
Expand All @@ -21,6 +22,66 @@ const meta = {
fixable: true,
};

/** @type {(node: import('css-tree').CssNode) => node is import('css-tree').CssNode & { children: List<import('css-tree').CssNode> }} */
const hasChildren = (node) => 'children' in node && node.children instanceof List;

/** @type {(node1: import('css-tree').CssNode[], node2: import('css-tree').CssNode[]) => boolean} */
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
const isEqualValueNodes = (nodes1, nodes2) => {
if (nodes1.length !== nodes2.length) {
return false;
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
}

for (let i = 0; i < nodes1.length; i++) {
const node1 = nodes1[i];
const node2 = nodes2[i];

if (typeof node1 === 'undefined' || typeof node2 === 'undefined' || node1.type !== node2.type) {
return false;
}

const node1Children = hasChildren(node1) ? node1.children.toArray() : null;
const node2Children = hasChildren(node2) ? node2.children.toArray() : null;

if (Array.isArray(node1Children) && Array.isArray(node2Children)) {
const node1Name = 'name' in node1 ? String(node1.name) : '';
const node2Name = 'name' in node2 ? String(node2.name) : '';

if (node1Name.toLowerCase() !== node2Name.toLowerCase()) {
return false;
}

if (isEqualValueNodes(node1Children, node2Children)) {
continue;
} else {
return false;
}
}

const node1Unit = 'unit' in node1 ? node1.unit : '';
const node2Unit = 'unit' in node2 ? node2.unit : '';

if (node1Unit !== node2Unit) {
return false;
}
}

return true;
};

/** @type {(value1: string, value2: string) => boolean} */
const isEqualValueSyntaxes = (value1, value2) => {
if (value1 === value2) {
return true;
}

const value1Node = parse(value1, { context: 'value' });
const value2Node = parse(value2, { context: 'value' });
const node1Children = hasChildren(value1Node) ? value1Node.children.toArray() : [];
const node2Children = hasChildren(value2Node) ? value2Node.children.toArray() : [];

return isEqualValueNodes(node1Children, node2Children);
};

/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
Expand All @@ -34,6 +95,7 @@ const rule = (primary, secondaryOptions, context) => {
ignore: [
'consecutive-duplicates',
'consecutive-duplicates-with-different-values',
'consecutive-duplicates-with-different-syntaxes',
'consecutive-duplicates-with-same-prefixless-values',
],
ignoreProperties: [isString, isRegExp],
Expand All @@ -52,6 +114,11 @@ const rule = (primary, secondaryOptions, context) => {
'ignore',
'consecutive-duplicates-with-different-values',
);
const ignoreDiffSyntaxes = optionsMatches(
secondaryOptions,
'ignore',
'consecutive-duplicates-with-different-syntaxes',
);
const ignorePrefixlessSameValues = optionsMatches(
secondaryOptions,
'ignore',
Expand Down Expand Up @@ -124,12 +191,24 @@ const rule = (primary, secondaryOptions, context) => {
return duplicateDecl.remove();
};

if (ignoreDiffValues || ignorePrefixlessSameValues) {
if (ignoreDiffValues || ignoreDiffSyntaxes || ignorePrefixlessSameValues) {
if (
!duplicatesAreConsecutive ||
(ignorePrefixlessSameValues && !unprefixedDuplicatesAreEqual)
) {
fixOrReport();

return;
}

if (ignoreDiffSyntaxes) {
const duplicateValueSyntaxesAreEqual = isEqualValueSyntaxes(value, duplicateValue);

if (duplicateValueSyntaxesAreEqual) {
fixOrReport();

return;
}
}

if (value !== duplicateValue) {
Expand Down