From fc2335d3caa40accae2af5f5337d7104a10f0013 Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Thu, 29 Dec 2022 23:54:30 -0800 Subject: [PATCH 01/12] First pass: `media-feature-name-unit-allowed-list` Adds base rule, refactors slightly from @romainmenke's suggestion; adds docs, rudimentary tests. Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> --- docs/user-guide/rules.md | 1 + lib/rules/index.js | 3 + .../README.md | 69 ++++++++++++++ .../__tests__/index.js | 75 +++++++++++++++ .../index.js | 94 +++++++++++++++++++ package-lock.json | 65 ++++++++++++- package.json | 3 + 7 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 lib/rules/media-feature-name-unit-allowed-list/README.md create mode 100644 lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js create mode 100644 lib/rules/media-feature-name-unit-allowed-list/index.js diff --git a/docs/user-guide/rules.md b/docs/user-guide/rules.md index 48e2038e17..00a27915d5 100644 --- a/docs/user-guide/rules.md +++ b/docs/user-guide/rules.md @@ -136,6 +136,7 @@ Allow, disallow or require things with these `allowed-list`, `disallowed-list`, - [`media-feature-name-allowed-list`](../../lib/rules/media-feature-name-allowed-list/README.md): Specify a list of allowed media feature names. - [`media-feature-name-disallowed-list`](../../lib/rules/media-feature-name-disallowed-list/README.md): Specify a list of disallowed media feature names. - [`media-feature-name-no-vendor-prefix`](../../lib/rules/media-feature-name-no-vendor-prefix/README.md): Disallow vendor prefixes for media feature names (Autofixable). +- [`media-feature-name-unit-allowed-list`](../../lib/rules/media-feature-name-unit-allowed-list/README.md): Specify a list of allowed name and unit pairs within media features. - [`media-feature-name-value-allowed-list`](../../lib/rules/media-feature-name-value-allowed-list/README.md): Specify a list of allowed media feature name and value pairs. #### Property diff --git a/lib/rules/index.js b/lib/rules/index.js index e0118bd01d..f502a8781d 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -191,6 +191,9 @@ const rules = { 'media-feature-name-no-vendor-prefix': importLazy(() => require('./media-feature-name-no-vendor-prefix'), )(), + 'media-feature-name-unit-allowed-list': importLazy(() => + require('./media-feature-name-unit-allowed-list'), + )(), 'media-feature-name-value-allowed-list': importLazy(() => require('./media-feature-name-value-allowed-list'), )(), diff --git a/lib/rules/media-feature-name-unit-allowed-list/README.md b/lib/rules/media-feature-name-unit-allowed-list/README.md new file mode 100644 index 0000000000..338aee9330 --- /dev/null +++ b/lib/rules/media-feature-name-unit-allowed-list/README.md @@ -0,0 +1,69 @@ +# media-feature-name-unit-allowed-list + +Specify a list of allowed name and unit pairs within media features. + + +```css +@media (width : 50em) {} +/** ↑ ↑ + * This media feature name and these units */ +``` + +## Options + +`object`: `{ "name": ["array", "of", "units"]|"unit" }` + +If a property name is surrounded with `"/"` (e.g. `"/height/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/height/` will match `height`, `min-height`, `max-height`, etc. + +Given: + +```json +{ + "width": "em", + "/height/": ["em", "rem"] +} +``` + +The following patterns are considered problems: + + +```css +@media (width: 50rem) {} +``` + + +```css +@media (height: 1000px) { } +``` + + +```css +@media (min-height: 1000px) { } +``` + + +```css +@media (max-height: 1000px) { } +``` + +The following patterns are _not_ considered problems: + + +```css +@media (width: 50em) {} +``` + + +```css +@media (height: 50em) {} +``` + + +```css +@media (min-height: 50rem) {} +``` + + +```css +@media (max-width: 1000px) {} +``` diff --git a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js new file mode 100644 index 0000000000..90e071d530 --- /dev/null +++ b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js @@ -0,0 +1,75 @@ +'use strict'; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: { width: 'em', '/height/': ['em', 'rem'] }, + + accept: [ + { + code: '@media (width: 50em) { }', + }, + { + code: '@media (min-width: 50rem) { }', + }, + { + code: '@media (max-width: 50rem) { }', + }, + { + code: '@media print and (width: 50em) { }', + }, + { + code: '@media (height: 50rem) { }', + }, + { + code: '@media (min-height: 50rem) { }', + }, + { + code: '@media (max-height: 50rem) { }', + }, + ], + + reject: [ + { + code: '@media (width: 50rem) { }', + message: messages.rejected('rem', 'width'), + line: 1, + column: 9, + endLine: 1, + endColumn: 13, + }, + { + code: '@media (width: 1000px) { }', + message: messages.rejected('px', 'width'), + line: 1, + column: 9, + endLine: 1, + endColumn: 14, + }, + { + code: '@media (height: 1000px) { }', + message: messages.rejected('px', 'height'), + line: 1, + column: 10, + endLine: 1, + endColumn: 15, + }, + { + code: '@media (min-height: 1000px) { }', + message: messages.rejected('px', 'min-height'), + line: 1, + column: 14, + endLine: 1, + endColumn: 19, + }, + { + code: '@media (max-height: 1000px) { }', + message: messages.rejected('px', 'max-height'), + line: 1, + column: 14, + endLine: 1, + endColumn: 19, + }, + ], +}); diff --git a/lib/rules/media-feature-name-unit-allowed-list/index.js b/lib/rules/media-feature-name-unit-allowed-list/index.js new file mode 100644 index 0000000000..dc42b65928 --- /dev/null +++ b/lib/rules/media-feature-name-unit-allowed-list/index.js @@ -0,0 +1,94 @@ +'use strict'; + +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const { TokenType } = require('@csstools/css-tokenizer'); +const { isTokenNode } = require('@csstools/css-parser-algorithms'); +const { + isMediaFeaturePlain, + isMediaFeatureRange, + parse, +} = require('@csstools/media-query-list-parser'); +const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps'); +const { isString } = require('../../utils/validateTypes'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const flattenArray = require('../../utils/flattenArray'); + +const ruleName = 'media-feature-name-unit-allowed-list'; + +const messages = ruleMessages(ruleName, { + rejected: (unit, name) => `Unexpected unit "${unit}" for name "${name}"`, +}); + +const meta = { + url: 'https://stylelint.io/user-guide/rules/media-feature-name-unit-allowed-list', +}; + +/** @type {import('stylelint').Rule} */ +const rule = (primary) => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primary, + possible: [validateObjectWithArrayProps(isString)], + }); + + if (!validOptions) { + return; + } + + root.walkAtRules(/^media$/i, (atRule) => { + const mediaQueryList = parse(atRule.params); + + mediaQueryList.forEach((mediaQuery) => { + mediaQuery.walk((entry) => { + if (!isMediaFeaturePlain(entry.node) && !isMediaFeatureRange(entry.node)) { + return; + } + + const name = entry.node.name.toString(); + + const nameKey = Object.keys(primary).find((nameIdentifier) => + matchesStringOrRegExp(name, nameIdentifier), + ); + + if (!nameKey) { + return; + } + + const nameList = flattenArray(primary[nameKey]); + + if (!nameList) { + return; + } + + entry.node.walk((childEntry) => { + if (!isTokenNode(childEntry.node) || childEntry.node.value[0] !== TokenType.Dimension) { + return; + } + + const unit = childEntry.node.value[4].unit; + + if (nameList.includes(unit.toLowerCase())) { + return; + } + + report({ + message: messages.rejected(unit, name), + node: atRule, + index: childEntry.node.value[2], + endIndex: childEntry.node.value[3], + result, + ruleName, + }); + }); + }); + }); + }); + }; +}; + +rule.ruleName = ruleName; +rule.messages = messages; +rule.meta = meta; +module.exports = rule; diff --git a/package-lock.json b/package-lock.json index a355e5b995..402a90b98e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "14.16.1", "license": "MIT", "dependencies": { + "@csstools/css-parser-algorithms": "^1.0.0", + "@csstools/css-tokenizer": "^1.0.0", + "@csstools/media-query-list-parser": "^1.0.0", "@csstools/selector-specificity": "^2.0.2", "balanced-match": "^2.0.0", "colord": "^2.9.3", @@ -32,7 +35,6 @@ "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.20", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^6.0.0", @@ -83,6 +85,7 @@ "np": "^7.6.2", "npm-run-all": "^4.1.5", "patch-package": "^6.5.0", + "postcss": "^8.4.20", "postcss-html": "^1.5.0", "postcss-import": "^15.1.0", "postcss-less": "^6.0.0", @@ -1393,6 +1396,49 @@ "prettier": "^2.7.1" } }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-1.0.0.tgz", + "integrity": "sha512-lPphY34yfV15tEXiz/SYaU8hwqAhbAwqiTExv5tOfc7QZxT70VVYrsiPBaX1osdWZFowrDEAhHe4H3JnyzbjhA==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^1.0.0" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-1.0.0.tgz", + "integrity": "sha512-xdFjdQ+zqqkOsmee+kYRieZD9Cqh4hr01YBQ2/8NtTkMMxbtRX18MC50LX6cMrtaLryqmIdZHN9e16/l0QqnQw==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-1.0.0.tgz", + "integrity": "sha512-HsTj5ejI8NKKZ4IEd6kK2kQZA/JmIVlUV8+XvO/YS9ntrlYPnbmFT3rkqtbxOVfEafblYCNOpeNw1c+fKGkAqw==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^1.0.0", + "@csstools/css-tokenizer": "^1.0.0" + } + }, "node_modules/@csstools/selector-specificity": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", @@ -15084,6 +15130,23 @@ "prettier": "^2.7.1" } }, + "@csstools/css-parser-algorithms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-1.0.0.tgz", + "integrity": "sha512-lPphY34yfV15tEXiz/SYaU8hwqAhbAwqiTExv5tOfc7QZxT70VVYrsiPBaX1osdWZFowrDEAhHe4H3JnyzbjhA==", + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-1.0.0.tgz", + "integrity": "sha512-xdFjdQ+zqqkOsmee+kYRieZD9Cqh4hr01YBQ2/8NtTkMMxbtRX18MC50LX6cMrtaLryqmIdZHN9e16/l0QqnQw==" + }, + "@csstools/media-query-list-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-1.0.0.tgz", + "integrity": "sha512-HsTj5ejI8NKKZ4IEd6kK2kQZA/JmIVlUV8+XvO/YS9ntrlYPnbmFT3rkqtbxOVfEafblYCNOpeNw1c+fKGkAqw==", + "requires": {} + }, "@csstools/selector-specificity": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", diff --git a/package.json b/package.json index ec314a2c5b..cf84eb9de0 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,9 @@ ] }, "dependencies": { + "@csstools/css-parser-algorithms": "^1.0.0", + "@csstools/css-tokenizer": "^1.0.0", + "@csstools/media-query-list-parser": "^1.0.0", "@csstools/selector-specificity": "^2.0.2", "balanced-match": "^2.0.0", "colord": "^2.9.3", From 63afd4ceac9fe0ef3b3eb1c9d485713d0ce1967a Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Mon, 2 Jan 2023 23:04:16 -0800 Subject: [PATCH 02/12] Fix end positions Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> --- .../__tests__/index.js | 20 +++++++++---------- .../index.js | 7 +++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js index 90e071d530..fab016b857 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js @@ -35,41 +35,41 @@ testRule({ code: '@media (width: 50rem) { }', message: messages.rejected('rem', 'width'), line: 1, - column: 9, + column: 16, endLine: 1, - endColumn: 13, + endColumn: 21, }, { code: '@media (width: 1000px) { }', message: messages.rejected('px', 'width'), line: 1, - column: 9, + column: 16, endLine: 1, - endColumn: 14, + endColumn: 22, }, { code: '@media (height: 1000px) { }', message: messages.rejected('px', 'height'), line: 1, - column: 10, + column: 17, endLine: 1, - endColumn: 15, + endColumn: 23, }, { code: '@media (min-height: 1000px) { }', message: messages.rejected('px', 'min-height'), line: 1, - column: 14, + column: 21, endLine: 1, - endColumn: 19, + endColumn: 27, }, { code: '@media (max-height: 1000px) { }', message: messages.rejected('px', 'max-height'), line: 1, - column: 14, + column: 21, endLine: 1, - endColumn: 19, + endColumn: 27, }, ], }); diff --git a/lib/rules/media-feature-name-unit-allowed-list/index.js b/lib/rules/media-feature-name-unit-allowed-list/index.js index dc42b65928..8640831855 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/index.js @@ -14,6 +14,7 @@ const validateObjectWithArrayProps = require('../../utils/validateObjectWithArra const { isString } = require('../../utils/validateTypes'); const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); const flattenArray = require('../../utils/flattenArray'); +const atRuleParamIndex = require('../../utils/atRuleParamIndex'); const ruleName = 'media-feature-name-unit-allowed-list'; @@ -73,11 +74,13 @@ const rule = (primary) => { return; } + const atRuleIndex = atRuleParamIndex(atRule); + report({ message: messages.rejected(unit, name), node: atRule, - index: childEntry.node.value[2], - endIndex: childEntry.node.value[3], + index: atRuleIndex + childEntry.node.value[2], + endIndex: atRuleIndex + childEntry.node.value[3] + 1, result, ruleName, }); From cb042137da91cf02a457f99b703a5e4485a063cd Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Mon, 2 Jan 2023 23:13:52 -0800 Subject: [PATCH 03/12] add tests for feature names with no attached unit --- .../__tests__/index.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js index fab016b857..2b1a828945 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js @@ -73,3 +73,23 @@ testRule({ }, ], }); + +testRule({ + ruleName, + config: { grid: [], 'prefers-reduced-motion': [], 'video-dynamic-range': [] }, + + accept: [ + { + code: '@media (prefers-reduced-motion) { }', + description: 'name-only value', + }, + { + code: '@media (grid: 0) { }', + description: 'unitless value, number', + }, + { + code: '@media (video-dynamic-range: high) { }', + description: 'unitless value, keyword', + }, + ], +}); From 74160820c6634e357e6fe973a59bc02a61db090d Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Mon, 2 Jan 2023 23:43:56 -0800 Subject: [PATCH 04/12] Properly grabs `name` for range queries Required a bit of hardcoding? --- .../__tests__/index.js | 75 +++++++++++++++++++ .../index.js | 4 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js index 2b1a828945..01f0cc0bde 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js @@ -10,14 +10,23 @@ testRule({ { code: '@media (width: 50em) { }', }, + { + code: '@media (width <= 50em) { }', + description: 'media queries level 4 - inequality', + }, { code: '@media (min-width: 50rem) { }', }, { code: '@media (max-width: 50rem) { }', }, + { + code: '@media (30em <= width <= 50em) { }', + description: 'media queries level 4 - range', + }, { code: '@media print and (width: 50em) { }', + description: 'compound selector', }, { code: '@media (height: 50rem) { }', @@ -28,6 +37,10 @@ testRule({ { code: '@media (max-height: 50rem) { }', }, + { + code: '@media (30rem <= height <= 50rem) { }', + description: 'media queries level 4 - range', + }, ], reject: [ @@ -39,6 +52,35 @@ testRule({ endLine: 1, endColumn: 21, }, + { + code: '@media (width <= 50rem) { }', + message: messages.rejected('rem', 'width'), + description: 'media queries level 4 - inequality', + line: 1, + column: 18, + endLine: 1, + endColumn: 23, + }, + { + code: '@media (30rem <= width <= 50rem) { }', + description: 'media queries level 4 - range', + warnings: [ + { + message: messages.rejected('rem', 'width'), + line: 1, + column: 9, + endLine: 1, + endColumn: 14, + }, + { + message: messages.rejected('rem', 'width'), + line: 1, + column: 27, + endLine: 1, + endColumn: 32, + }, + ], + }, { code: '@media (width: 1000px) { }', message: messages.rejected('px', 'width'), @@ -55,6 +97,35 @@ testRule({ endLine: 1, endColumn: 23, }, + { + code: '@media (height <= 1000px) { }', + message: messages.rejected('px', 'height'), + description: 'media queries level 4 - inequality', + line: 1, + column: 19, + endLine: 1, + endColumn: 25, + }, + { + code: '@media (100px <= height <= 1000px) { }', + description: 'media queries level 4 - range', + warnings: [ + { + message: messages.rejected('px', 'height'), + line: 1, + column: 9, + endLine: 1, + endColumn: 14, + }, + { + message: messages.rejected('px', 'height'), + line: 1, + column: 28, + endLine: 1, + endColumn: 34, + }, + ], + }, { code: '@media (min-height: 1000px) { }', message: messages.rejected('px', 'min-height'), @@ -83,6 +154,10 @@ testRule({ code: '@media (prefers-reduced-motion) { }', description: 'name-only value', }, + { + code: '@media (color) { }', + description: 'name-only value', + }, { code: '@media (grid: 0) { }', description: 'unitless value, number', diff --git a/lib/rules/media-feature-name-unit-allowed-list/index.js b/lib/rules/media-feature-name-unit-allowed-list/index.js index 8640831855..40c3afefa9 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/index.js @@ -47,7 +47,9 @@ const rule = (primary) => { return; } - const name = entry.node.name.toString(); + const name = isMediaFeatureRange(entry.node) + ? entry.node.name.name.value[1].toString() + : entry.node.name.toString(); const nameKey = Object.keys(primary).find((nameIdentifier) => matchesStringOrRegExp(name, nameIdentifier), From 9d485960470e97e0cc8c260e4329122030062aff Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Mon, 2 Jan 2023 23:45:41 -0800 Subject: [PATCH 05/12] add changeset --- .changeset/few-monkeys-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-monkeys-fold.md diff --git a/.changeset/few-monkeys-fold.md b/.changeset/few-monkeys-fold.md new file mode 100644 index 0000000000..3a3799bb1a --- /dev/null +++ b/.changeset/few-monkeys-fold.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Add `media-feature-name-unit-allowed-list` From 712484e5ea692a48468e3e4d0113f306e41a4aef Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Mon, 2 Jan 2023 23:59:34 -0800 Subject: [PATCH 06/12] Uses `entry.node.name.getName()` Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> --- lib/rules/media-feature-name-unit-allowed-list/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/rules/media-feature-name-unit-allowed-list/index.js b/lib/rules/media-feature-name-unit-allowed-list/index.js index 40c3afefa9..52099682c4 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/index.js @@ -47,9 +47,7 @@ const rule = (primary) => { return; } - const name = isMediaFeatureRange(entry.node) - ? entry.node.name.name.value[1].toString() - : entry.node.name.toString(); + const name = entry.node.name.getName(); const nameKey = Object.keys(primary).find((nameIdentifier) => matchesStringOrRegExp(name, nameIdentifier), From d95146f7eaa2101aa48029d8b59342fdc338810b Mon Sep 17 00:00:00 2001 From: Matt Wang Date: Tue, 3 Jan 2023 18:02:14 -0800 Subject: [PATCH 07/12] Update changeset to match naming convention --- .changeset/few-monkeys-fold.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/few-monkeys-fold.md b/.changeset/few-monkeys-fold.md index 3a3799bb1a..ebb4098944 100644 --- a/.changeset/few-monkeys-fold.md +++ b/.changeset/few-monkeys-fold.md @@ -2,4 +2,4 @@ "stylelint": minor --- -Add `media-feature-name-unit-allowed-list` +Added `media-feature-name-unit-allowed-list` From 898a4c2128d4f8d3812d52cccddb8d2adc8c8fc7 Mon Sep 17 00:00:00 2001 From: Matt Wang Date: Tue, 3 Jan 2023 18:33:44 -0800 Subject: [PATCH 08/12] Update changeset to match naming convention (again) Co-authored-by: Marc G. --- .changeset/few-monkeys-fold.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/few-monkeys-fold.md b/.changeset/few-monkeys-fold.md index ebb4098944..f1dc1a67a0 100644 --- a/.changeset/few-monkeys-fold.md +++ b/.changeset/few-monkeys-fold.md @@ -2,4 +2,4 @@ "stylelint": minor --- -Added `media-feature-name-unit-allowed-list` +Added: `media-feature-name-unit-allowed-list` rule From 4a3e90ba2406cb7efa1dbbb11d67ea00170b05ff Mon Sep 17 00:00:00 2001 From: Matt Wang Date: Wed, 4 Jan 2023 20:06:31 -0800 Subject: [PATCH 09/12] Apply suggestions from code review Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com> --- .../media-feature-name-unit-allowed-list/README.md | 10 +++++----- .../__tests__/index.js | 2 +- .../media-feature-name-unit-allowed-list/index.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rules/media-feature-name-unit-allowed-list/README.md b/lib/rules/media-feature-name-unit-allowed-list/README.md index 338aee9330..d1ac10d549 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/README.md +++ b/lib/rules/media-feature-name-unit-allowed-list/README.md @@ -4,7 +4,7 @@ Specify a list of allowed name and unit pairs within media features. ```css -@media (width : 50em) {} +@media (width: 50em) {} /** ↑ ↑ * This media feature name and these units */ ``` @@ -13,7 +13,7 @@ Specify a list of allowed name and unit pairs within media features. `object`: `{ "name": ["array", "of", "units"]|"unit" }` -If a property name is surrounded with `"/"` (e.g. `"/height/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/height/` will match `height`, `min-height`, `max-height`, etc. +If a feature name is surrounded with `"/"` (e.g. `"/height/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/height/` will match `height`, `min-height`, `max-height`, etc. Given: @@ -33,17 +33,17 @@ The following patterns are considered problems: ```css -@media (height: 1000px) { } +@media (height: 1000px) {} ``` ```css -@media (min-height: 1000px) { } +@media (min-height: 1000px) {} ``` ```css -@media (max-height: 1000px) { } +@media (max-height: 1000px) {} ``` The following patterns are _not_ considered problems: diff --git a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js index 01f0cc0bde..f3829d17b5 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js @@ -8,7 +8,7 @@ testRule({ accept: [ { - code: '@media (width: 50em) { }', + code: '@media (width: 50em) {}', }, { code: '@media (width <= 50em) { }', diff --git a/lib/rules/media-feature-name-unit-allowed-list/index.js b/lib/rules/media-feature-name-unit-allowed-list/index.js index 52099682c4..432859d315 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/index.js @@ -26,7 +26,7 @@ const meta = { url: 'https://stylelint.io/user-guide/rules/media-feature-name-unit-allowed-list', }; -/** @type {import('stylelint').Rule} */ +/** @type {import('stylelint').Rule>} */ const rule = (primary) => { return (root, result) => { const validOptions = validateOptions(result, ruleName, { From 9aaf7cb3b0996c7dc5dfa4a2abf39b438fb9fd51 Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Wed, 4 Jan 2023 20:08:11 -0800 Subject: [PATCH 10/12] misc standardization --- .../__tests__/index.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js index f3829d17b5..813026e3de 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js @@ -11,41 +11,41 @@ testRule({ code: '@media (width: 50em) {}', }, { - code: '@media (width <= 50em) { }', + code: '@media (width <= 50em) {}', description: 'media queries level 4 - inequality', }, { - code: '@media (min-width: 50rem) { }', + code: '@media (min-width: 50rem) {}', }, { - code: '@media (max-width: 50rem) { }', + code: '@media (max-width: 50rem) {}', }, { - code: '@media (30em <= width <= 50em) { }', + code: '@media (30em <= width <= 50em) {}', description: 'media queries level 4 - range', }, { - code: '@media print and (width: 50em) { }', + code: '@media print and (width: 50em) {}', description: 'compound selector', }, { - code: '@media (height: 50rem) { }', + code: '@media (height: 50rem) {}', }, { - code: '@media (min-height: 50rem) { }', + code: '@media (min-height: 50rem) {}', }, { - code: '@media (max-height: 50rem) { }', + code: '@media (max-height: 50rem) {}', }, { - code: '@media (30rem <= height <= 50rem) { }', + code: '@media (30rem <= height <= 50rem) {}', description: 'media queries level 4 - range', }, ], reject: [ { - code: '@media (width: 50rem) { }', + code: '@media (width: 50rem) {}', message: messages.rejected('rem', 'width'), line: 1, column: 16, @@ -53,7 +53,7 @@ testRule({ endColumn: 21, }, { - code: '@media (width <= 50rem) { }', + code: '@media (width <= 50rem) {}', message: messages.rejected('rem', 'width'), description: 'media queries level 4 - inequality', line: 1, @@ -62,7 +62,7 @@ testRule({ endColumn: 23, }, { - code: '@media (30rem <= width <= 50rem) { }', + code: '@media (30rem <= width <= 50rem) {}', description: 'media queries level 4 - range', warnings: [ { @@ -82,7 +82,7 @@ testRule({ ], }, { - code: '@media (width: 1000px) { }', + code: '@media (width: 1000px) {}', message: messages.rejected('px', 'width'), line: 1, column: 16, @@ -90,7 +90,7 @@ testRule({ endColumn: 22, }, { - code: '@media (height: 1000px) { }', + code: '@media (height: 1000px) {}', message: messages.rejected('px', 'height'), line: 1, column: 17, @@ -98,7 +98,7 @@ testRule({ endColumn: 23, }, { - code: '@media (height <= 1000px) { }', + code: '@media (height <= 1000px) {}', message: messages.rejected('px', 'height'), description: 'media queries level 4 - inequality', line: 1, @@ -107,7 +107,7 @@ testRule({ endColumn: 25, }, { - code: '@media (100px <= height <= 1000px) { }', + code: '@media (100px <= height <= 1000px) {}', description: 'media queries level 4 - range', warnings: [ { @@ -127,7 +127,7 @@ testRule({ ], }, { - code: '@media (min-height: 1000px) { }', + code: '@media (min-height: 1000px) {}', message: messages.rejected('px', 'min-height'), line: 1, column: 21, @@ -135,7 +135,7 @@ testRule({ endColumn: 27, }, { - code: '@media (max-height: 1000px) { }', + code: '@media (max-height: 1000px) {}', message: messages.rejected('px', 'max-height'), line: 1, column: 21, @@ -151,19 +151,19 @@ testRule({ accept: [ { - code: '@media (prefers-reduced-motion) { }', + code: '@media (prefers-reduced-motion) {}', description: 'name-only value', }, { - code: '@media (color) { }', + code: '@media (color) {}', description: 'name-only value', }, { - code: '@media (grid: 0) { }', + code: '@media (grid: 0) {}', description: 'unitless value, number', }, { - code: '@media (video-dynamic-range: high) { }', + code: '@media (video-dynamic-range: high) {}', description: 'unitless value, keyword', }, ], From 7e5fab8e27dbc36a875ed18219d4284d3972c0d9 Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Wed, 4 Jan 2023 20:10:43 -0800 Subject: [PATCH 11/12] fix postcss location --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 2819b04a57..c12de211a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", + "postcss": "^8.4.20", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^6.0.0", @@ -85,7 +86,6 @@ "np": "^7.6.3", "npm-run-all": "^4.1.5", "patch-package": "^6.5.0", - "postcss": "^8.4.20", "postcss-html": "^1.5.0", "postcss-import": "^15.1.0", "postcss-less": "^6.0.0", From e5aabfe49e6026ed0db2c13265d377c2f4f0acee Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Wed, 4 Jan 2023 20:20:13 -0800 Subject: [PATCH 12/12] Improve readability/conciseness of library code No behaviour changes. Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com> --- .../README.md | 8 ++-- .../index.js | 41 +++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/rules/media-feature-name-unit-allowed-list/README.md b/lib/rules/media-feature-name-unit-allowed-list/README.md index d1ac10d549..da2bc1b3c5 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/README.md +++ b/lib/rules/media-feature-name-unit-allowed-list/README.md @@ -43,7 +43,7 @@ The following patterns are considered problems: ```css -@media (max-height: 1000px) {} +@media (height <= 1000px) {} ``` The following patterns are _not_ considered problems: @@ -55,15 +55,15 @@ The following patterns are _not_ considered problems: ```css -@media (height: 50em) {} +@media (width <= 50em) {} ``` ```css -@media (min-height: 50rem) {} +@media (height: 50em) {} ``` ```css -@media (max-width: 1000px) {} +@media (min-height: 50rem) {} ``` diff --git a/lib/rules/media-feature-name-unit-allowed-list/index.js b/lib/rules/media-feature-name-unit-allowed-list/index.js index 432859d315..a5ecddc650 100644 --- a/lib/rules/media-feature-name-unit-allowed-list/index.js +++ b/lib/rules/media-feature-name-unit-allowed-list/index.js @@ -13,7 +13,6 @@ const { const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps'); const { isString } = require('../../utils/validateTypes'); const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); -const flattenArray = require('../../utils/flattenArray'); const atRuleParamIndex = require('../../utils/atRuleParamIndex'); const ruleName = 'media-feature-name-unit-allowed-list'; @@ -38,6 +37,15 @@ const rule = (primary) => { return; } + const primaryPairs = Object.entries(primary); + const primaryUnitList = (/** @type {string} */ featureName) => { + for (const [name, unit] of primaryPairs) { + if (matchesStringOrRegExp(featureName, name)) return [unit].flat(); + } + + return undefined; + }; + root.walkAtRules(/^media$/i, (atRule) => { const mediaQueryList = parse(atRule.params); @@ -47,40 +55,37 @@ const rule = (primary) => { return; } - const name = entry.node.name.getName(); + const featureName = entry.node.name.getName(); + const unitList = primaryUnitList(featureName); - const nameKey = Object.keys(primary).find((nameIdentifier) => - matchesStringOrRegExp(name, nameIdentifier), - ); - - if (!nameKey) { + if (!unitList) { return; } - const nameList = flattenArray(primary[nameKey]); + entry.node.walk(({ node: childNode }) => { + if (!isTokenNode(childNode)) { + return; + } - if (!nameList) { - return; - } + const [tokenType, , startIndex, endIndex] = childNode.value; - entry.node.walk((childEntry) => { - if (!isTokenNode(childEntry.node) || childEntry.node.value[0] !== TokenType.Dimension) { + if (tokenType !== TokenType.Dimension) { return; } - const unit = childEntry.node.value[4].unit; + const unit = childNode.value[4].unit; - if (nameList.includes(unit.toLowerCase())) { + if (unitList.includes(unit.toLowerCase())) { return; } const atRuleIndex = atRuleParamIndex(atRule); report({ - message: messages.rejected(unit, name), + message: messages.rejected(unit, featureName), node: atRule, - index: atRuleIndex + childEntry.node.value[2], - endIndex: atRuleIndex + childEntry.node.value[3] + 1, + index: atRuleIndex + startIndex, + endIndex: atRuleIndex + endIndex + 1, result, ruleName, });