diff --git a/.changeset/nasty-shoes-bathe.md b/.changeset/nasty-shoes-bathe.md new file mode 100644 index 0000000000..c0f00d76f1 --- /dev/null +++ b/.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 diff --git a/lib/rules/no-duplicate-at-import-rules/__tests__/index.js b/lib/rules/no-duplicate-at-import-rules/__tests__/index.js index ca8f9c4e20..b09d3cd17e 100644 --- a/lib/rules/no-duplicate-at-import-rules/__tests__/index.js +++ b/lib/rules/no-duplicate-at-import-rules/__tests__/index.js @@ -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: [ @@ -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, + }, ], }); diff --git a/lib/rules/no-duplicate-at-import-rules/index.js b/lib/rules/no-duplicate-at-import-rules/index.js index 59419bb712..7ca365e6b2 100644 --- a/lib/rules/no-duplicate-at-import-rules/index.js +++ b/lib/rules/no-duplicate-at-import-rules/index.js @@ -70,18 +70,31 @@ const rule = (primary) => { }; }; +/** @typedef { import('postcss-value-parser').Node } Node */ + +/** + * @param {Node | Array} 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} */ function listImportConditions(params) { if (!params.length) return []; - /** @type {Array} */ - let media = []; + const separator = ' '; + /** @type {Array} */ + const sharedConditions = []; + /** @type {Array} */ + const media = []; /** @type {Array} */ let lastMediaQuery = []; @@ -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')) { + 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; } @@ -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;