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

Fix no-duplicate-at-import-rules false negatives for imports with supports and layer conditions #7001

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-shoes-bathe.md
@@ -0,0 +1,5 @@
---
"stylelint": patch
---

Fixed: `no-duplicate-at-import-rules` false negatives for imports with `supports` and `layer` conditions
89 changes: 89 additions & 0 deletions lib/rules/no-duplicate-at-import-rules/__tests__/index.js
Expand Up @@ -31,6 +31,15 @@ testRule({
{
code: '@IMPORT "a.css"; @ImPoRt "b.css";',
},
{
code: '@import "a.css" supports(display: flex) tv; @import "a.css" layer tv;',
},
{
code: '@import "a.css" supports(display: flex); @import "a.css" layer;',
},
{
code: '@import "a.css" supports(display: flex) tv; @import "a.css" supports(display: grid) tv;',
},
],

reject: [
Expand Down Expand Up @@ -114,5 +123,85 @@ testRule({
line: 1,
column: 18,
},
{
code: '@import url("a.css") layer; @import url("a.css") layer;',
message: messages.rejected(`a.css`),
line: 1,
column: 29,
endLine: 1,
endColumn: 55,
},
{
code: '@import url("a.css") layer(base); @import url("a.css") layer(base);',
message: messages.rejected(`a.css`),
line: 1,
column: 35,
endLine: 1,
endColumn: 67,
},
{
code: '@import url("a.css") layer(base) supports(display: grid); @import url("a.css") layer(base) supports(display: grid);',
message: messages.rejected(`a.css`),
line: 1,
column: 59,
endLine: 1,
endColumn: 115,
},
{
code: '@import url("a.css") supports(display: grid); @import url("a.css") supports(display: grid);',
message: messages.rejected(`a.css`),
line: 1,
column: 47,
endLine: 1,
endColumn: 91,
},
{
code: '@import url("a.css") layer tv; @import url("a.css") layer tv;',
message: messages.rejected(`a.css`),
line: 1,
column: 32,
endLine: 1,
endColumn: 61,
},
{
code: '@import url("a.css") layer(base) tv; @import url("a.css") layer(base) tv;',
message: messages.rejected(`a.css`),
line: 1,
column: 38,
endLine: 1,
endColumn: 73,
},
{
code: '@import url("a.css") layer(base) supports(display: grid) tv; @import url("a.css") layer(base) supports(display: grid) tv;',
message: messages.rejected(`a.css`),
line: 1,
column: 62,
endLine: 1,
endColumn: 121,
},
{
code: '@import url("a.css") layer(base) supports(display: grid) screen, tv; @import url("a.css") layer(base) supports(display: grid) tv;',
message: messages.rejected(`a.css`),
line: 1,
column: 70,
endLine: 1,
endColumn: 129,
},
{
code: '@import url("a.css") layer; @import url("a.css") layer;',
message: messages.rejected(`a.css`),
line: 1,
column: 29,
endLine: 1,
endColumn: 55,
},
{
code: '@import url("a.css") supports(display: grid); @import url("a.css") supports(display: grid);',
message: messages.rejected(`a.css`),
line: 1,
column: 47,
endLine: 1,
endColumn: 91,
},
],
});
55 changes: 48 additions & 7 deletions lib/rules/no-duplicate-at-import-rules/index.js
Expand Up @@ -70,18 +70,31 @@ const rule = (primary) => {
};
};

/** @typedef { import('postcss-value-parser').Node } Node */

/**
* @param {Node | Array<Node>} node
* @returns {string}
*/
function stringifyCondition(node) {
// remove whitespace to get a more consistent key
return valueParser.stringify(node).replace(/\s/g, '');
}

/**
* List the import conditions found in the prelude of an `@import` rule
*
* @param {Node[]} params
* @typedef {import('postcss-value-parser').Node} Node
* @returns {Array<string>}
*/
function listImportConditions(params) {
if (!params.length) return [];

/** @type {Array<String>} */
let media = [];
const separator = ' ';
/** @type {Array<string>} */
const sharedConditions = [];
/** @type {Array<string>} */
const media = [];
/** @type {Array<Node>} */
let lastMediaQuery = [];

Expand All @@ -91,8 +104,23 @@ function listImportConditions(params) {
continue;
}

// layer and supports conditions must precede media query conditions
if (!media.length) {
// @import url(...) layer(base) supports(display: flex)
if (param.type === 'function' && (param.value === 'supports' || param.value === 'layer')) {
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved
sharedConditions.push(stringifyCondition(param));
continue;
}

// @import url(...) layer
if (param.type === 'word' && param.value === 'layer') {
sharedConditions.push(stringifyCondition(param));
continue;
}
}

if (param.type === 'div' && param.value === ',') {
media.push(valueParser.stringify(lastMediaQuery));
media.push(stringifyCondition(lastMediaQuery));
lastMediaQuery = [];
continue;
}
Expand All @@ -101,11 +129,24 @@ function listImportConditions(params) {
}

if (lastMediaQuery.length) {
media.push(valueParser.stringify(lastMediaQuery));
media.push(stringifyCondition(lastMediaQuery));
}

// Only media query conditions
if (media.length && !sharedConditions.length) {
return media;
}

// remove remaining whitespace to get a more consistent key
return media.map((m) => m.replace(/\s/g, ''));
// Only layer and supports conditions
if (!media.length && sharedConditions.length) {
return [sharedConditions.join(separator)];
}

const sharedConditionsString = sharedConditions.join(separator);

return media.map((m) => {
return sharedConditionsString + separator + m;
});
}

rule.ruleName = ruleName;
Expand Down