From 2b890d1806362e4787b1f04654fd7e2915e24352 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Thu, 29 Jun 2023 18:41:18 +0200 Subject: [PATCH 1/3] Fix `no-duplicate-at-import-rules` false negatives for imports with `supports` and `layer` conditions --- .../__tests__/index.js | 70 +++++++++++++++++++ .../no-duplicate-at-import-rules/index.js | 23 +++++- 2 files changed, 91 insertions(+), 2 deletions(-) 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..b716638566 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,12 @@ 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) tv; @import "a.css" supports(display: grid) tv;', + }, ], reject: [ @@ -114,5 +120,69 @@ 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, + }, ], }); diff --git a/lib/rules/no-duplicate-at-import-rules/index.js b/lib/rules/no-duplicate-at-import-rules/index.js index 59419bb712..007f6b2db7 100644 --- a/lib/rules/no-duplicate-at-import-rules/index.js +++ b/lib/rules/no-duplicate-at-import-rules/index.js @@ -80,7 +80,9 @@ const rule = (primary) => { function listImportConditions(params) { if (!params.length) return []; - /** @type {Array} */ + /** @type {Array} */ + let sharedConditions = []; + /** @type {Array} */ let media = []; /** @type {Array} */ let lastMediaQuery = []; @@ -91,6 +93,21 @@ 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(valueParser.stringify(param)); + continue; + } + + // @import url(...) layer + if (param.type === 'word' && param.value === 'layer') { + sharedConditions.push(valueParser.stringify(param)); + continue; + } + } + if (param.type === 'div' && param.value === ',') { media.push(valueParser.stringify(lastMediaQuery)); lastMediaQuery = []; @@ -104,8 +121,10 @@ function listImportConditions(params) { media.push(valueParser.stringify(lastMediaQuery)); } + const sharedConditionsString = sharedConditions.length ? `${sharedConditions.join(' ')} ` : ''; + // remove remaining whitespace to get a more consistent key - return media.map((m) => m.replace(/\s/g, '')); + return media.map((m) => sharedConditionsString + m.replace(/\s/g, '')); } rule.ruleName = ruleName; From 4abdfd72b97bde253876ca9df79d9b9aa5ebf00f Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Thu, 29 Jun 2023 18:42:48 +0200 Subject: [PATCH 2/3] Create nasty-shoes-bathe.md --- .changeset/nasty-shoes-bathe.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nasty-shoes-bathe.md 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 From 4214b8a607cd0769b07fc3127d0f7db296c419a9 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Thu, 29 Jun 2023 23:38:34 +0200 Subject: [PATCH 3/3] fixes --- .../__tests__/index.js | 19 +++++++++ .../no-duplicate-at-import-rules/index.js | 42 ++++++++++++++----- 2 files changed, 51 insertions(+), 10 deletions(-) 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 b716638566..b09d3cd17e 100644 --- a/lib/rules/no-duplicate-at-import-rules/__tests__/index.js +++ b/lib/rules/no-duplicate-at-import-rules/__tests__/index.js @@ -34,6 +34,9 @@ testRule({ { 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;', }, @@ -184,5 +187,21 @@ testRule({ 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 007f6b2db7..7ca365e6b2 100644 --- a/lib/rules/no-duplicate-at-import-rules/index.js +++ b/lib/rules/no-duplicate-at-import-rules/index.js @@ -70,20 +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 []; + const separator = ' '; /** @type {Array} */ - let sharedConditions = []; + const sharedConditions = []; /** @type {Array} */ - let media = []; + const media = []; /** @type {Array} */ let lastMediaQuery = []; @@ -97,19 +108,19 @@ function listImportConditions(params) { if (!media.length) { // @import url(...) layer(base) supports(display: flex) if (param.type === 'function' && (param.value === 'supports' || param.value === 'layer')) { - sharedConditions.push(valueParser.stringify(param)); + sharedConditions.push(stringifyCondition(param)); continue; } // @import url(...) layer if (param.type === 'word' && param.value === 'layer') { - sharedConditions.push(valueParser.stringify(param)); + sharedConditions.push(stringifyCondition(param)); continue; } } if (param.type === 'div' && param.value === ',') { - media.push(valueParser.stringify(lastMediaQuery)); + media.push(stringifyCondition(lastMediaQuery)); lastMediaQuery = []; continue; } @@ -118,13 +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; + } + + // Only layer and supports conditions + if (!media.length && sharedConditions.length) { + return [sharedConditions.join(separator)]; } - const sharedConditionsString = sharedConditions.length ? `${sharedConditions.join(' ')} ` : ''; + const sharedConditionsString = sharedConditions.join(separator); - // remove remaining whitespace to get a more consistent key - return media.map((m) => sharedConditionsString + m.replace(/\s/g, '')); + return media.map((m) => { + return sharedConditionsString + separator + m; + }); } rule.ruleName = ruleName;