diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index d70cbc171a..ea2e615ce4 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -102,10 +102,14 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### Function - [`function-allowlist`](../../../lib/rules/function-allowlist/README.md): Specify a list of allowed functions. +- [`function-blacklist`](../../../lib/rules/function-blacklist/README.md): Specify a list of disallowed functions. **(deprecated)** - [`function-denylist`](../../../lib/rules/function-denylist/README.md): Specify a list of disallowed functions. - [`function-url-no-scheme-relative`](../../../lib/rules/function-url-no-scheme-relative/README.md): Disallow scheme-relative urls. -- [`function-url-scheme-denylist`](../../../lib/rules/function-url-scheme-denylist/README.md): Specify a list of disallowed URL schemes. - [`function-url-scheme-allowlist`](../../../lib/rules/function-url-scheme-allowlist/README.md): Specify a list of allowed URL schemes. +- [`function-url-scheme-blacklist`](../../../lib/rules/function-url-scheme-blacklist/README.md): Specify a list of disallowed URL schemes. **(deprecated)** +- [`function-url-scheme-denylist`](../../../lib/rules/function-url-scheme-denylist/README.md): Specify a list of disallowed URL schemes. +- [`function-url-scheme-whitelist`](../../../lib/rules/function-url-scheme-whitelist/README.md): Specify a list of allowed URL schemes. **(deprecated)** +- [`function-whitelist`](../../../lib/rules/function-whitelist/README.md): Specify a list of allowed functions. **(deprecated)** ### Keyframes @@ -122,7 +126,9 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### Unit - [`unit-allowlist`](../../../lib/rules/unit-allowlist/README.md): Specify a list of allowed units. +- [`unit-blacklist`](../../../lib/rules/unit-blacklist/README.md): Specify a list of disallowed units. **(deprecated)** - [`unit-denylist`](../../../lib/rules/unit-denylist/README.md): Specify a list of disallowed units. +- [`unit-whitelist`](../../../lib/rules/unit-whitelist/README.md): Specify a list of allowed units. **(deprecated)** ### Shorthand property @@ -139,17 +145,23 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### Property - [`property-allowlist`](../../../lib/rules/property-allowlist/README.md): Specify a list of allowed properties. +- [`property-blacklist`](../../../lib/rules/property-blacklist/README.md): Specify a list of disallowed properties. **(deprecated)** - [`property-denylist`](../../../lib/rules/property-denylist/README.md): Specify a list of disallowed properties. - [`property-no-vendor-prefix`](../../../lib/rules/property-no-vendor-prefix/README.md): Disallow vendor prefixes for properties. +- [`property-whitelist`](../../../lib/rules/property-whitelist/README.md): Specify a list of allowed properties. **(deprecated)** ### Declaration - [`declaration-block-no-redundant-longhand-properties`](../../../lib/rules/declaration-block-no-redundant-longhand-properties/README.md): Disallow longhand properties that can be combined into one shorthand property. - [`declaration-no-important`](../../../lib/rules/declaration-no-important/README.md): Disallow `!important` within declarations. - [`declaration-property-unit-allowlist`](../../../lib/rules/declaration-property-unit-allowlist/README.md): Specify a list of allowed property and unit pairs within declarations. +- [`declaration-property-unit-blacklist`](../../../lib/rules/declaration-property-unit-blacklist/README.md): Specify a list of disallowed property and unit pairs within declarations. **(deprecated)** - [`declaration-property-unit-denylist`](../../../lib/rules/declaration-property-unit-denylist/README.md): Specify a list of disallowed property and unit pairs within declarations. +- [`declaration-property-unit-whitelist`](../../../lib/rules/declaration-property-unit-whitelist/README.md): Specify a list of allowed property and unit pairs within declarations. **(deprecated)** - [`declaration-property-value-allowlist`](../../../lib/rules/declaration-property-value-allowlist/README.md): Specify a list of allowed property and value pairs within declarations. +- [`declaration-property-value-blacklist`](../../../lib/rules/declaration-property-value-blacklist/README.md): Specify a list of disallowed property and value pairs within declarations. **(deprecated)** - [`declaration-property-value-denylist`](../../../lib/rules/declaration-property-value-denylist/README.md): Specify a list of disallowed property and value pairs within declarations. +- [`declaration-property-value-whitelist`](../../../lib/rules/declaration-property-value-whitelist/README.md): Specify a list of allowed property and value pairs within declarations. **(deprecated)** ### Declaration block @@ -158,10 +170,14 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### Selector - [`selector-attribute-operator-allowlist`](../../../lib/rules/selector-attribute-operator-allowlist/README.md): Specify a list of allowed attribute operators. +- [`selector-attribute-operator-blacklist`](../../../lib/rules/selector-attribute-operator-blacklist/README.md): Specify a list of disallowed attribute operators. **(deprecated)** - [`selector-attribute-operator-denylist`](../../../lib/rules/selector-attribute-operator-denylist/README.md): Specify a list of disallowed attribute operators. +- [`selector-attribute-operator-whitelist`](../../../lib/rules/selector-attribute-operator-whitelist/README.md): Specify a list of allowed attribute operators. **(deprecated)** - [`selector-class-pattern`](../../../lib/rules/selector-class-pattern/README.md): Specify a pattern for class selectors. - [`selector-combinator-allowlist`](../../../lib/rules/selector-combinator-allowlist/README.md): Specify a list of allowed combinators. +- [`selector-combinator-blacklist`](../../../lib/rules/selector-combinator-blacklist/README.md): Specify a list of disallowed combinators. **(deprecated)** - [`selector-combinator-denylist`](../../../lib/rules/selector-combinator-denylist/README.md): Specify a list of disallowed combinators. +- [`selector-combinator-whitelist`](../../../lib/rules/selector-combinator-whitelist/README.md): Specify a list of allowed combinators. **(deprecated)** - [`selector-id-pattern`](../../../lib/rules/selector-id-pattern/README.md): Specify a pattern for ID selectors. - [`selector-max-attribute`](../../../lib/rules/selector-max-attribute/README.md): Limit the number of attribute selectors in a selector. - [`selector-max-class`](../../../lib/rules/selector-max-class/README.md): Limit the number of classes in a selector. @@ -177,17 +193,24 @@ Grouped first by the following categories and then by the [_thing_](http://apps. - [`selector-no-qualifying-type`](../../../lib/rules/selector-no-qualifying-type/README.md): Disallow qualifying a selector by type. - [`selector-no-vendor-prefix`](../../../lib/rules/selector-no-vendor-prefix/README.md): Disallow vendor prefixes for selectors. - [`selector-pseudo-class-allowlist`](../../../lib/rules/selector-pseudo-class-allowlist/README.md): Specify a list of allowed pseudo-class selectors. +- [`selector-pseudo-class-blacklist`](../../../lib/rules/selector-pseudo-class-blacklist/README.md): Specify a list of disallowed pseudo-class selectors. **(deprecated)** - [`selector-pseudo-class-denylist`](../../../lib/rules/selector-pseudo-class-denylist/README.md): Specify a list of disallowed pseudo-class selectors. +- [`selector-pseudo-class-whitelist`](../../../lib/rules/selector-pseudo-class-whitelist/README.md): Specify a list of allowed pseudo-class selectors. **(deprecated)** - [`selector-pseudo-element-allowlist`](../../../lib/rules/selector-pseudo-element-allowlist/README.md): Specify a list of allowed pseudo-element selectors. +- [`selector-pseudo-element-blacklist`](../../../lib/rules/selector-pseudo-element-blacklist/README.md): Specify a list of disallowed pseudo-element selectors. **(deprecated)** - [`selector-pseudo-element-colon-notation`](../../../lib/rules/selector-pseudo-element-colon-notation/README.md): Specify single or double colon notation for applicable pseudo-elements (Autofixable). - [`selector-pseudo-element-denylist`](../../../lib/rules/selector-pseudo-element-denylist/README.md): Specify a list of disallowed pseudo-element selectors. +- [`selector-pseudo-element-whitelist`](../../../lib/rules/selector-pseudo-element-whitelist/README.md): Specify a list of allowed pseudo-element selectors. **(deprecated)** ### Media feature - [`media-feature-name-allowlist`](../../../lib/rules/media-feature-name-allowlist/README.md): Specify a list of allowed media feature names. +- [`media-feature-name-blacklist`](../../../lib/rules/media-feature-name-blacklist/README.md): Specify a list of disallowed media feature names. **(deprecated)** - [`media-feature-name-denylist`](../../../lib/rules/media-feature-name-denylist/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. - [`media-feature-name-value-allowlist`](../../../lib/rules/media-feature-name-value-allowlist/README.md): Specify a list of allowed media feature name and value pairs. +- [`media-feature-name-value-whitelist`](../../../lib/rules/media-feature-name-value-whitelist/README.md): Specify a list of allowed media feature name and value pairs. **(deprecated)** +- [`media-feature-name-whitelist`](../../../lib/rules/media-feature-name-whitelist/README.md): Specify a list of allowed media feature names. **(deprecated)** ### Custom media @@ -196,12 +219,15 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### At-rule - [`at-rule-allowlist`](../../../lib/rules/at-rule-allowlist/README.md): Specify a list of allowed at-rules. +- [`at-rule-blacklist`](../../../lib/rules/at-rule-blacklist/README.md): Specify a list of disallowed at-rules. **(deprecated)** - [`at-rule-denylist`](../../../lib/rules/at-rule-denylist/README.md): Specify a list of disallowed at-rules. - [`at-rule-no-vendor-prefix`](../../../lib/rules/at-rule-no-vendor-prefix/README.md): Disallow vendor prefixes for at-rules. - [`at-rule-property-requirelist`](../../../lib/rules/at-rule-property-requirelist/README.md): Specify a requirelist of properties for an at-rule. +- [`at-rule-whitelist`](../../../lib/rules/at-rule-whitelist/README.md): Specify a list of allowed at-rules. **(deprecated)** ### Comment +- [`comment-word-blacklist`](../../../lib/rules/comment-word-blacklist/README.md): Specify a list of disallowed words within comments. **(deprecated)** - [`comment-word-denylist`](../../../lib/rules/comment-word-denylist/README.md): Specify a list of disallowed words within comments. ### General / Sheet diff --git a/lib/rules/at-rule-blacklist/README.md b/lib/rules/at-rule-blacklist/README.md new file mode 100644 index 0000000000..3be72fbf6b --- /dev/null +++ b/lib/rules/at-rule-blacklist/README.md @@ -0,0 +1,52 @@ +# at-rule-blacklist + +**_Deprecated: Instead use the [`at-rule-denylist`](../at-rule-denylist/README.md) rule._** + +Specify a list of disallowed at-rules. + + +```css + @keyframes name {} +/** ↑ + * At-rules like this */ +``` + +## Options + +`array|string`: `["array", "of", "unprefixed", "at-rules"]|"at-rule"` + +Given: + +``` +["extend", "keyframes"] +``` + +The following patterns are considered violations: + + +```css +a { @extend placeholder; } +``` + + +```css +@keyframes name { + from { top: 10px; } + to { top: 20px; } +} +``` + + +```css +@-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } +} +``` + +The following patterns are _not_ considered violations: + + +```css +@import "path/to/file.css"; +``` diff --git a/lib/rules/at-rule-blacklist/__tests__/index.js b/lib/rules/at-rule-blacklist/__tests__/index.js new file mode 100644 index 0000000000..5a4e4a0a2e --- /dev/null +++ b/lib/rules/at-rule-blacklist/__tests__/index.js @@ -0,0 +1,197 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['extend'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'at-rule-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['extend', 'supports', 'keyframes'], + + accept: [ + { + code: 'a { color: pink; }', + description: 'Some random code.', + }, + { + code: '@mixin name ($p) {}', + description: '@rule not from a blacklist.', + }, + ], + + reject: [ + { + code: 'a { @extend %placeholder; }', + message: messages.rejected('extend'), + line: 1, + column: 5, + description: '@rule from a blacklist, is a Sass directive.', + }, + { + code: ` + a { + @extend + %placeholder; + } + `, + message: messages.rejected('extend'), + line: 3, + column: 9, + description: '@rule from a blacklist; newline after its name.', + }, + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('keyframes'), + line: 2, + description: '@rule from a blacklist; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('Keyframes'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule; messed case.', + }, + { + code: ` + @-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('-moz-keyframes'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule; has vendor prefix.', + }, + { + code: ` + @-WEBKET-KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('-WEBKET-KEYFRAMES'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule; has vendor prefix.', + }, + ], +}); + +testRule({ + ruleName, + + config: ['keyframes'], + + accept: [ + { + code: 'a { color: pink; }', + description: 'Some random code.', + }, + { + code: '@mixin name ($p) {}', + description: '@rule not from a blacklist.', + }, + ], + + reject: [ + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('keyframes'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('Keyframes'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule; messed case.', + }, + { + code: ` + @-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('-moz-keyframes'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule; has vendor prefix.', + }, + { + code: ` + @-WEBKET-KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('-WEBKET-KEYFRAMES'), + line: 2, + column: 7, + description: '@rule from a blacklist; independent rule; has vendor prefix.', + }, + ], +}); + +testRule({ + ruleName, + syntax: 'less', + config: ['keyframes'], + + accept: [ + { + code: ` + .keyframes() { margin: 0; } + + span { .keyframes(); } + `, + description: 'ignore Less mixin which are treated as at-rule', + }, + ], +}); diff --git a/lib/rules/at-rule-blacklist/index.js b/lib/rules/at-rule-blacklist/index.js new file mode 100644 index 0000000000..d3e9c706e5 --- /dev/null +++ b/lib/rules/at-rule-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../at-rule-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'at-rule-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected at-rule "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/at-rule-whitelist/README.md b/lib/rules/at-rule-whitelist/README.md new file mode 100644 index 0000000000..85d5dff5a4 --- /dev/null +++ b/lib/rules/at-rule-whitelist/README.md @@ -0,0 +1,67 @@ +# at-rule-whitelist + +**_Deprecated: Instead use the [`at-rule-allowlist`](../at-rule-allowlist/README.md) rule._** + +Specify a list of allowed at-rules. + + +```css + @keyframes name {} +/** ↑ + * At-rules like this */ +``` + +## Options + +`array|string`: `["array", "of", "unprefixed", "at-rules"]|"at-rule"` + +Given: + +``` +["extend", "keyframes"] +``` + +The following patterns are considered violations: + + +```css +@import "path/to/file.css"; +``` + + +```css +@media screen and (max-width: 1024px) { + a { display: none; } +} +``` + +The following patterns are _not_ considered violations: + + +```css +a { @extend placeholder; } +``` + + +```css +@keyframes name { + from { top: 10px; } + to { top: 20px; } +} +``` + + +```css +@KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } +} +``` + + +```css +@-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } +} +``` diff --git a/lib/rules/at-rule-whitelist/__tests__/index.js b/lib/rules/at-rule-whitelist/__tests__/index.js new file mode 100644 index 0000000000..31127080c5 --- /dev/null +++ b/lib/rules/at-rule-whitelist/__tests__/index.js @@ -0,0 +1,190 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['extend'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'at-rule-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['extend', 'import', 'keyframes'], + + accept: [ + { + code: 'a { color: pink; }', + description: 'Some random code.', + }, + { + code: 'a { @extend %placeholder; }', + description: '@rule from a whitelist, is a Sass directive.', + }, + { + code: ` + a { + @extend + %placeholder; + } + `, + description: '@rule from a whitelist; newline after its name.', + }, + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule; messed case.', + }, + { + code: ` + @-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule; has vendor prefix.', + }, + { + code: ` + @-WEBKET-KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule; has vendor prefix.', + }, + ], + + reject: [ + { + code: ` + @mixin name () {} + `, + line: 2, + columt: 7, + message: messages.rejected('mixin'), + description: '@rule not from a whitelist; independent rule.', + }, + ], +}); + +testRule({ + ruleName, + skipBasicChecks: true, + + config: ['keyframes'], + + accept: [ + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule; messed case.', + }, + { + code: ` + @-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule; has vendor prefix.', + }, + { + code: ` + @-WEBKET-KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from a whitelist; independent rule; has vendor prefix.', + }, + ], + + reject: [ + { + code: ` + @mixin name ($p) {} + `, + message: messages.rejected('mixin'), + line: 2, + column: 7, + description: '@rule not from a whitelist.', + }, + { + code: "@import 'path/to/file.css';", + message: messages.rejected('import'), + line: 1, + column: 1, + description: '@rule not from a whitelist.', + }, + { + code: '@media screen and (max-witdh: 1000px) {}', + message: messages.rejected('media'), + line: 1, + column: 1, + description: '@rule not from a whitelist.', + }, + ], +}); + +testRule({ + ruleName, + syntax: 'less', + config: ['keyframes'], + skipBasicChecks: true, + + accept: [ + { + code: ` + .mixin() { margin: 0; } + + span { .mixin(); } + `, + description: 'ignore Less mixin which are treated as at-rule', + }, + ], +}); diff --git a/lib/rules/at-rule-whitelist/index.js b/lib/rules/at-rule-whitelist/index.js new file mode 100644 index 0000000000..8b546e77f0 --- /dev/null +++ b/lib/rules/at-rule-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../at-rule-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'at-rule-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected at-rule "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/comment-word-blacklist/README.md b/lib/rules/comment-word-blacklist/README.md new file mode 100644 index 0000000000..e88200449e --- /dev/null +++ b/lib/rules/comment-word-blacklist/README.md @@ -0,0 +1,50 @@ +# comment-word-blacklist + +**_Deprecated: Instead use the [`comment-word-denylist`](../comment-word-denylist/README.md) rule._** + +Specify a list of disallowed words within comments. + + +```css + /* words within comments */ +/** ↑ ↑ ↑ + * These three words */ +``` + +**Caveat:** Comments within _selector and value lists_ are currently ignored. + +## Options + +`array|string|regexp`: `["array", "of", "words", /or/, "/regex/"]|"word"|"/regex/"` + +If a string is surrounded with `"/"` (e.g. `"/^TODO:/"`), it is interpreted as a regular expression. + +Given: + +``` +["/^TODO:/", "badword"] +``` + +The following patterns are considered violations: + + +```css +/* TODO: */ +``` + + +```css +/* TODO: add fallback */ +``` + + +```css +/* some badword */ +``` + +The following patterns are _not_ considered violations: + + +```css +/* comment */ +``` diff --git a/lib/rules/comment-word-blacklist/__tests__/index.js b/lib/rules/comment-word-blacklist/__tests__/index.js new file mode 100644 index 0000000000..98feea68fb --- /dev/null +++ b/lib/rules/comment-word-blacklist/__tests__/index.js @@ -0,0 +1,347 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['bad-word'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'comment-word-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['bad-word'], + + accept: [ + { + code: '/* comment */', + }, + { + code: '/*# bad-word */', + }, + ], + + reject: [ + { + code: '/* Comment with bad-word */', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/* bad-word */', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/*** bad-word ***/', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/*! bad-word */', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/** bad-word **/', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + ], +}); + +testRule({ + ruleName, + config: ['/^TODO:/', 'bad-word'], + + accept: [ + { + code: '/* comment */', + }, + { + code: '/* comment comment */', + }, + { + code: '/* comment\ncomment */', + }, + { + code: '/* comment\n\ncomment */', + }, + { + code: '/** comment */', + }, + { + code: '/**** comment ***/', + }, + { + code: '/*\ncomment\n*/', + }, + { + code: '/*\tcomment */', + }, + { + code: '/*! copyright */', + }, + { + code: '/*# sourcemap */', + }, + { + code: '/*# sourcemap bad-word */', + }, + { + code: 'a { color: pink; /* comment */\ntop: 0; }', + }, + { + code: 'a {} /* comment */', + }, + { + code: '/* todo */', + }, + { + code: '/* todo: */', + }, + { + code: '/* todo: comment */', + }, + { + code: '/* tOdO: comment */', + }, + { + code: '/* Todo: comment */', + }, + { + code: '/*! Todo: comment */', + }, + { + code: '/*# Todo: comment */', + }, + { + code: '/** TODO: comment **/', + }, + { + code: '/*** TODO: comment ***/', + }, + ], + + reject: [ + { + code: '/* TODO: */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/* TODO: comment */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/* TODO: comment\n next line */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/* TODO: comment\n next line */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/* TODO: comment\r\n next line */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/* TODO: comment\n\n next line */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/*\n TODO: comment */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/*\r\n TODO: comment */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/*\n\n TODO: comment */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/*\r\n\r\n TODO: comment */', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '/* bad-word */', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/* Comment with bad-word */', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/*! copyright bad-word */', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/** bad-word **/', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + { + code: '/*** bad-word ***/', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + ], +}); + +testRule({ + ruleName, + syntax: 'scss', + config: [['/^TODO:/', 'bad-word']], + + accept: [ + { + code: '// comment', + }, + { + code: '// todo', + }, + { + code: '// todo:', + }, + { + code: '// Todo:', + }, + { + code: '// tOdO:', + }, + ], + + reject: [ + { + code: '// TODO:', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '// TODO: comment', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '// bad-word', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + ], +}); + +testRule({ + ruleName, + syntax: 'less', + config: ['/^TODO:/', 'bad-word'], + + accept: [ + { + code: '// comment', + }, + { + code: '// todo:', + }, + { + code: '// Todo:', + }, + { + code: '// tOdO:', + }, + ], + + reject: [ + { + code: '// TODO:', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '// TODO: comment', + message: messages.rejected('/^TODO:/'), + line: 1, + column: 1, + }, + { + code: '// bad-word', + message: messages.rejected('bad-word'), + line: 1, + column: 1, + }, + ], +}); + +testRule({ + ruleName, + config: [/^TODO:/, 'bad-word'], + + accept: [ + { + code: '/* comment */', + }, + ], + + reject: [ + { + code: '/* TODO: */', + message: messages.rejected(/^TODO:/), + line: 1, + column: 1, + }, + ], +}); diff --git a/lib/rules/comment-word-blacklist/index.js b/lib/rules/comment-word-blacklist/index.js new file mode 100644 index 0000000000..ac902681c4 --- /dev/null +++ b/lib/rules/comment-word-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../comment-word-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'comment-word-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (pattern) => `Unexpected word matching pattern "${pattern}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-unit-blacklist/README.md b/lib/rules/declaration-property-unit-blacklist/README.md new file mode 100644 index 0000000000..20269a40e3 --- /dev/null +++ b/lib/rules/declaration-property-unit-blacklist/README.md @@ -0,0 +1,76 @@ +# declaration-property-unit-blacklist + +**_Deprecated: Instead use the [`declaration-property-unit-denylist`](../declaration-property-unit-denylist/README.md) rule._** + +Specify a list of disallowed property and unit pairs within declarations. + + +```css +a { width: 100px; } +/** ↑ ↑ + * These properties and these units */ +``` + +## Options + +`object`: `{ "unprefixed-property-name": ["array", "of", "units"] }` + +If a property name is surrounded with `"/"` (e.g. `"/^animation/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^animation/` will match `animation`, `animation-duration`, `animation-timing-function`, etc. + +Given: + +``` +{ + "font-size": ["em", "px"], + "/^animation/": ["s"] +} +``` + +The following patterns are considered violations: + + +```css +a { font-size: 1em; } +``` + + +```css +a { animation: animation-name 5s ease; } +``` + + +```css +a { -webkit-animation: animation-name 5s ease; } +``` + + +```css +a { animation-duration: 5s; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { font-size: 1.2rem; } +``` + + +```css +a { height: 100px; } +``` + + +```css +a { animation: animation-name 500ms ease; } +``` + + +```css +a { -webkit-animation: animation-name 500ms ease; } +``` + + +```css +a { animation-duration: 500ms; } +``` diff --git a/lib/rules/declaration-property-unit-blacklist/__tests__/index.js b/lib/rules/declaration-property-unit-blacklist/__tests__/index.js new file mode 100644 index 0000000000..4c3318b469 --- /dev/null +++ b/lib/rules/declaration-property-unit-blacklist/__tests__/index.js @@ -0,0 +1,195 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [{ margin: ['em'] }], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-unit-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: [ + { + 'font-size': ['px', 'em'], + margin: ['em'], + 'background-position': ['%'], + animation: ['s'], + }, + ], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { top: 0; }', + }, + { + code: 'a { color: #000; }', + }, + { + code: 'a { margin: 0 0 0 0 }', + }, + { + code: 'a { margin: 0 10px 5rem 2in; }', + }, + { + code: 'a { margin: 0 10pX 5rem 2in; }', + }, + { + code: 'a { margin: 0 10PX 5rem 2in; }', + }, + { + code: 'a { background-position: top right, 1em 5vh; }', + }, + { + code: 'a { margin: calc(30vh - 10vh); }', + }, + { + code: 'a { animation: animation-name 300ms ease; }', + }, + { + code: 'a { -webkit-animation: animation-name 300ms ease; }', + }, + { + code: 'a { animation-duration: 3s; }', + }, + { + code: 'a { -webkit-animation-duration: 3s; }', + }, + { + code: 'a { font-size: /* 100px */ 1.2rem; }', + description: 'ignore unit within comments', + }, + { + code: 'a::before { font-size: "10px"}', + description: 'ignore unit within quotes', + }, + { + code: 'a { font-size: $fs10px; }', + description: 'ignore preprocessor variable includes unit', + }, + { + code: 'a { font-size: --some-fs-10px; }', + description: 'ignore css variable includes unit', + }, + ], + + reject: [ + { + code: 'a { font-size: 12px; }', + message: messages.rejected('font-size', 'px'), + line: 1, + column: 16, + }, + { + code: 'a { font-size: 12pX; }', + message: messages.rejected('font-size', 'pX'), + line: 1, + column: 16, + }, + { + code: 'a { font-size: 12PX; }', + message: messages.rejected('font-size', 'PX'), + line: 1, + column: 16, + }, + { + code: 'a { margin: 10px 0 5em; }', + message: messages.rejected('margin', 'em'), + line: 1, + column: 20, + }, + { + code: 'a { background-position: 0 10%; }', + message: messages.rejected('background-position', '%'), + line: 1, + column: 28, + }, + { + code: 'a { background-position: top right, 0 10%; }', + message: messages.rejected('background-position', '%'), + line: 1, + column: 39, + }, + { + code: 'a { margin: calc(10vh - 10em); }', + message: messages.rejected('margin', 'em'), + column: 25, + }, + { + code: 'a { animation: foo 3s; }', + message: messages.rejected('animation', 's'), + }, + { + code: 'a { -webkit-animation: foo 3s; }', + message: messages.rejected('-webkit-animation', 's'), + }, + ], +}); + +testRule({ + ruleName, + + config: [ + { + '/^animation/': ['s'], + }, + ], + + skipBasicChecks: true, + + accept: [ + { + code: 'a { animation: animation-name 300ms ease; }', + }, + { + code: 'a { -webkit-animation: animation-name 300ms ease; }', + }, + { + code: 'a { animation-duration: 300ms; }', + }, + { + code: 'a { -webkit-animation-duration: 300ms; }', + }, + ], + + reject: [ + { + code: 'a { animation: animation-name 3s ease; }', + message: messages.rejected('animation', 's'), + }, + { + code: 'a { -webkit-animation: animation-name 3s ease; }', + message: messages.rejected('-webkit-animation', 's'), + }, + { + code: 'a { animation-duration: 3s; }', + message: messages.rejected('animation-duration', 's'), + }, + { + code: 'a { -webkit-animation-duration: 3s; }', + message: messages.rejected('-webkit-animation-duration', 's'), + }, + ], +}); diff --git a/lib/rules/declaration-property-unit-blacklist/index.js b/lib/rules/declaration-property-unit-blacklist/index.js new file mode 100644 index 0000000000..48ecd67204 --- /dev/null +++ b/lib/rules/declaration-property-unit-blacklist/index.js @@ -0,0 +1,18 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../declaration-property-unit-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'declaration-property-unit-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, unit) => `Unexpected unit "${unit}" for property "${property}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-unit-whitelist/README.md b/lib/rules/declaration-property-unit-whitelist/README.md new file mode 100644 index 0000000000..8a6b2efae1 --- /dev/null +++ b/lib/rules/declaration-property-unit-whitelist/README.md @@ -0,0 +1,87 @@ +# declaration-property-unit-whitelist + +**_Deprecated: Instead use the [`declaration-property-unit-allowlist`](../declaration-property-unit-allowlist/README.md) rule._** + +Specify a list of allowed property and unit pairs within declarations. + + +```css +a { width: 100px; } +/** ↑ ↑ + * These properties and these units */ +``` + +## Options + +`object`: `{ "unprefixed-property-name": ["array", "of", "units"] }` + +If a property name is surrounded with `"/"` (e.g. `"/^animation/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^animation/` will match `animation`, `animation-duration`, `animation-timing-function`, etc. + +Given: + +``` +{ + "font-size": ["em", "px"], + "/^animation/": ["s"], + "line-height": [] +} +``` + +The following patterns are considered violations: + + +```css +a { font-size: 1.2rem; } +``` + + +```css +a { animation: animation-name 500ms ease; } +``` + + +```css +a { -webkit-animation: animation-name 500ms ease; } +``` + + +```css +a { animation-duration: 500ms; } +``` + + +```css +a { line-height: 13px; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { font-size: 1em; } +``` + + +```css +a { height: 100px; } +``` + + +```css +a { animation: animation-name 5s ease; } +``` + + +```css +a { -webkit-animation: animation-name 5s ease; } +``` + + +```css +a { animation-duration: 5s; } +``` + + +```css +a { line-height: 1; } +``` diff --git a/lib/rules/declaration-property-unit-whitelist/__tests__/index.js b/lib/rules/declaration-property-unit-whitelist/__tests__/index.js new file mode 100644 index 0000000000..20f0243517 --- /dev/null +++ b/lib/rules/declaration-property-unit-whitelist/__tests__/index.js @@ -0,0 +1,200 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [{ margin: ['em'] }], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-unit-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: [ + { + 'font-size': ['px', 'em'], + margin: ['em'], + 'background-position': ['%'], + animation: ['s'], + 'line-height': [], + }, + ], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { top: 0; }', + }, + { + code: 'a { color: #000; }', + }, + { + code: 'a { margin: 0 0 0 0; }', + }, + { + code: 'a { margin: 0 10em; }', + }, + { + code: 'a { margin: 0 10eM; }', + }, + { + code: 'a { margin: 0 10EM; }', + }, + { + code: 'a { background-position: top right, 0 50%; }', + }, + { + code: 'a { margin: calc(30em - 10em); }', + }, + { + code: 'a { animation: animation-name 1s ease; }', + }, + { + code: 'a { -webkit-animation: animation-name 1s ease; }', + }, + { + code: 'a { line-height: 1; }', + }, + { + code: 'a { font-size: /* 1.2rem */ 12px; }', + description: 'ignore unit within comments', + }, + { + code: 'a::before { font-size: "1.2rem"}', + description: 'ignore unit within quotes', + }, + { + code: 'a { font-size: $fs1rem; }', + description: 'ignore preprocessor variable includes unit', + }, + { + code: 'a { font-size: --some-fs-1rem; }', + description: 'ignore css variable includes unit', + }, + ], + + reject: [ + { + code: 'a { font-size: 1.2rem; }', + message: messages.rejected('font-size', 'rem'), + line: 1, + column: 16, + }, + { + code: 'a { font-size: 1.2rEm; }', + message: messages.rejected('font-size', 'rEm'), + line: 1, + column: 16, + }, + { + code: 'a { font-size: 1.2REM; }', + message: messages.rejected('font-size', 'REM'), + line: 1, + column: 16, + }, + { + code: 'a { margin: 10em 0 1rem; }', + message: messages.rejected('margin', 'rem'), + line: 1, + column: 20, + }, + { + code: 'a { background-position: 0 10px; }', + message: messages.rejected('background-position', 'px'), + line: 1, + column: 28, + }, + { + code: 'a { background-position: top right, 0 10px; }', + message: messages.rejected('background-position', 'px'), + line: 1, + column: 39, + }, + { + code: 'a { margin: calc(10em - 10px); }', + message: messages.rejected('margin', 'px'), + column: 25, + }, + { + code: 'a { animation: animation-name 300ms ease; }', + message: messages.rejected('animation', 'ms'), + column: 31, + }, + { + code: 'a { -webkit-animation: animation-name 300ms ease; }', + message: messages.rejected('-webkit-animation', 'ms'), + column: 39, + }, + { + code: 'a { line-height: 1.2em; }', + message: messages.rejected('line-height', 'em'), + column: 18, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + { + '/^animation/': ['ms'], + }, + ], + + skipBasicChecks: true, + + accept: [ + { + code: 'a { animation: animation-name 300ms ease; }', + }, + { + code: 'a { -webkit-animation: animation-name 300ms ease; }', + }, + { + code: 'a { animation-duration: 300ms; }', + }, + { + code: 'a { -webkit-animation-duration: 300ms; }', + }, + ], + + reject: [ + { + code: 'a { animation: animation-name 3s ease; }', + message: messages.rejected('animation', 's'), + }, + { + code: 'a { -webkit-animation: animation-name 3s ease; }', + message: messages.rejected('-webkit-animation', 's'), + }, + { + code: 'a { animation-duration: 3s; }', + message: messages.rejected('animation-duration', 's'), + }, + { + code: 'a { -webkit-animation-duration: 3s; }', + message: messages.rejected('-webkit-animation-duration', 's'), + }, + ], +}); diff --git a/lib/rules/declaration-property-unit-whitelist/index.js b/lib/rules/declaration-property-unit-whitelist/index.js new file mode 100644 index 0000000000..2db2c2012e --- /dev/null +++ b/lib/rules/declaration-property-unit-whitelist/index.js @@ -0,0 +1,18 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../declaration-property-unit-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'declaration-property-unit-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, unit) => `Unexpected unit "${unit}" for property "${property}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-value-blacklist/README.md b/lib/rules/declaration-property-value-blacklist/README.md new file mode 100644 index 0000000000..d017e8b588 --- /dev/null +++ b/lib/rules/declaration-property-value-blacklist/README.md @@ -0,0 +1,107 @@ +# declaration-property-value-blacklist + +**_Deprecated: Instead use the [`declaration-property-value-denylist`](../declaration-property-value-denylist/README.md) rule._** + +Specify a list of disallowed property and value pairs within declarations. + + +```css +a { text-transform: uppercase; } +/** ↑ ↑ + * These properties and these values */ +``` + +## Options + +`object`: `{ "unprefixed-property-name": ["array", "of", "values"], "unprefixed-property-name": ["/regex/", "non-regex", /regex/] }` + +If a property name is surrounded with `"/"` (e.g. `"/^animation/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^animation/` will match `animation`, `animation-duration`, `animation-timing-function`, etc. + +The same goes for values. Keep in mind that a regular expression value is matched against the entire value of the declaration, not specific parts of it. For example, a value like `"10px solid rgba( 255 , 0 , 0 , 0.5 )"` will _not_ match `"/^solid/"` (notice beginning of the line boundary) but _will_ match `"/\\s+solid\\s+/"` or `"/\\bsolid\\b/"`. + +Be careful with regex matching not to accidentally consider quoted string values and `url()` arguments. For example, `"/red/"` will match value such as `"1px dotted red"` as well as `"\"foo\""` and `"white url(/mysite.com/red.png)"`. + +Given: + +``` +{ + "transform": ["/scale3d/", "/rotate3d/", "/translate3d/"], + "position": ["fixed"], + "color": ["/^green/"], + "/^animation/": ["/ease/"] +} +``` + +The following patterns are considered violations: + + +```css +a { position: fixed; } +``` + + +```css +a { transform: scale3d(1, 2, 3); } +``` + + +```css +a { -webkit-transform: scale3d(1, 2, 3); } +``` + + +```css +a { color: green; } +``` + + +```css +a { animation: foo 2s ease-in-out; } +``` + + +```css +a { animation-timing-function: ease-in-out; } +``` + + +```css +a { -webkit-animation-timing-function: ease-in-out; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { position: relative; } +``` + + +```css +a { transform: scale(2); } +``` + + +```css +a { -webkit-transform: scale(2); } +``` + + +```css +a { color: lightgreen; } +``` + + +```css +a { animation: foo 2s linear; } +``` + + +```css +a { animation-timing-function: linear; } +``` + + +```css +a { -webkit-animation-timing-function: linear; } +``` diff --git a/lib/rules/declaration-property-value-blacklist/__tests__/index.js b/lib/rules/declaration-property-value-blacklist/__tests__/index.js new file mode 100644 index 0000000000..3746f637db --- /dev/null +++ b/lib/rules/declaration-property-value-blacklist/__tests__/index.js @@ -0,0 +1,210 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [{ color: ['red'] }], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-value-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: [ + { + // regular string + 'text-transform': ['uppercase'], + // regexes + transform: ['/scale3d/', '/rotate3d/', '/translate3d/'], + // mixed string and regex + color: ['red', 'green', 'blue', '/^sea/'], + }, + ], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { color: lightgreen; }', + }, + { + code: 'a { text-transform: lowercase; }', + }, + { + code: 'a { transform: matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0) translate(12px, 50%); }', + }, + { + code: 'a { -webkit-transform: matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0) translate(12px, 50%); }', + }, + { + code: 'a { color: /* red */ pink; }', + description: 'ignore value within comments', + }, + { + code: 'a::before { color: "red"}', + description: 'ignore value within quotes', + }, + { + code: 'a { color: $red; }', + description: 'ignore preprocessor variable includes value', + }, + { + code: 'a { color: --some-red; }', + description: 'ignore css variable includes value', + }, + { + code: 'a { color: darkseagreen }', + }, + ], + + reject: [ + { + code: 'a { color: red; }', + message: messages.rejected('color', 'red'), + line: 1, + column: 5, + }, + { + code: 'a { color: green }', + message: messages.rejected('color', 'green'), + line: 1, + column: 5, + }, + { + code: 'a { text-transform: uppercase; }', + message: messages.rejected('text-transform', 'uppercase'), + line: 1, + column: 5, + }, + { + code: 'a { transform: scale3d(1, 2, 3) }', + message: messages.rejected('transform', 'scale3d(1, 2, 3)'), + line: 1, + column: 5, + }, + { + code: 'a { -webkit-transform: scale3d(1, 2, 3) }', + message: messages.rejected('-webkit-transform', 'scale3d(1, 2, 3)'), + column: 5, + }, + { + code: 'a { color: seagreen }', + message: messages.rejected('color', 'seagreen'), + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + { + '/^animation/': ['/ease/'], + }, + ], + + skipBasicChecks: true, + + accept: [ + { + code: 'a { animation: foo 1s linear; }', + }, + { + code: 'a { -webkit-animation: foo 1s linear; }', + }, + { + code: 'a { animation-timing-function: linear; }', + }, + { + code: 'a { -webkit-animation-timing-function: linear; }', + }, + ], + + reject: [ + { + code: 'a { animation: foo 1s ease-in-out; }', + message: messages.rejected('animation', 'foo 1s ease-in-out'), + }, + { + code: 'a { -webkit-animation: foo 1s ease-in-out; }', + message: messages.rejected('-webkit-animation', 'foo 1s ease-in-out'), + }, + { + code: 'a { animation-timing-function: ease-in-out; }', + message: messages.rejected('animation-timing-function', 'ease-in-out'), + }, + { + code: 'a { -webkit-animation-timing-function: ease-in-out; }', + message: messages.rejected('-webkit-animation-timing-function', 'ease-in-out'), + }, + ], +}); + +testRule({ + ruleName, + + config: [ + { + '/^animation/': [/ease/], + }, + ], + + skipBasicChecks: true, + + accept: [ + { + code: 'a { animation: foo 1s linear; }', + }, + ], + + reject: [ + { + code: 'a { animation: foo 1s ease-in-out; }', + message: messages.rejected('animation', 'foo 1s ease-in-out'), + }, + { + code: 'a { -webkit-animation: foo 1s ease-in-out; }', + message: messages.rejected('-webkit-animation', 'foo 1s ease-in-out'), + }, + { + code: 'a { animation-timing-function: ease-in-out; }', + message: messages.rejected('animation-timing-function', 'ease-in-out'), + }, + { + code: 'a { -webkit-animation-timing-function: ease-in-out; }', + message: messages.rejected('-webkit-animation-timing-function', 'ease-in-out'), + }, + ], +}); + +testRule({ + ruleName, + config: { position: ['fixed'] }, + skipBasicChecks: true, + accept: [ + { + code: 'a { font-size: 1em; }', + description: 'irrelevant CSS', + }, + ], +}); diff --git a/lib/rules/declaration-property-value-blacklist/index.js b/lib/rules/declaration-property-value-blacklist/index.js new file mode 100644 index 0000000000..ccaff7d859 --- /dev/null +++ b/lib/rules/declaration-property-value-blacklist/index.js @@ -0,0 +1,18 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../declaration-property-value-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'declaration-property-value-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-value-whitelist/README.md b/lib/rules/declaration-property-value-whitelist/README.md new file mode 100644 index 0000000000..f7333319d5 --- /dev/null +++ b/lib/rules/declaration-property-value-whitelist/README.md @@ -0,0 +1,98 @@ +# declaration-property-value-whitelist + +**_Deprecated: Instead use the [`declaration-property-value-allowlist`](../declaration-property-value-allowlist/README.md) rule._** + +Specify a list of allowed property and value pairs within declarations. + + +```css +a { text-transform: uppercase; } +/** ↑ ↑ + * These properties and these values */ +``` + +## Options + +`object`: `{ "unprefixed-property-name": ["array", "of", "values"], "unprefixed-property-name": ["/regex/", "non-regex"] }` + +If a property name is found in the object, only its whitelisted property values are allowed. This rule complains about all non-matching values. (If the property name is not included in the object, anything goes.) + +If a property name is surrounded with `"/"` (e.g. `"/^animation/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^animation/` will match `animation`, `animation-duration`, `animation-timing-function`, etc. + +The same goes for values. Keep in mind that a regular expression value is matched against the entire value of the declaration, not specific parts of it. For example, a value like `"10px solid rgba( 255 , 0 , 0 , 0.5 )"` will _not_ match `"/^solid/"` (notice beginning of the line boundary) but _will_ match `"/\\s+solid\\s+/"` or `"/\\bsolid\\b/"`. + +Be careful with regex matching not to accidentally consider quoted string values and `url()` arguments. For example, `"/red/"` will match value such as `"1px dotted red"` as well as `"\"red\""` and `"white url(/mysite.com/red.png)"`. + +Given: + +``` +{ + "transform": ["/scale/"], + "whitespace": ["nowrap"], + "/color/": ["/^green/"] +} +``` + +The following patterns are considered violations: + + +```css +a { whitespace: pre; } +``` + + +```css +a { transform: translate(1, 1); } +``` + + +```css +a { -webkit-transform: translate(1, 1); } +``` + + +```css +a { color: pink; } +``` + + +```css +a { background-color: pink; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { color: pink; } +``` + + +```css +a { whitespace: nowrap; } +``` + + +```css +a { transform: scale(1, 1); } +``` + + +```css +a { -webkit-transform: scale(1, 1); } +``` + + +```css +a { color: green; } +``` + + +```css +a { background-color: green; } +``` + + +```css +a { background: pink; } +``` diff --git a/lib/rules/declaration-property-value-whitelist/__tests__/index.js b/lib/rules/declaration-property-value-whitelist/__tests__/index.js new file mode 100644 index 0000000000..531bd82573 --- /dev/null +++ b/lib/rules/declaration-property-value-whitelist/__tests__/index.js @@ -0,0 +1,101 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [{ transform: ['/scale/'] }], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-value-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: [ + { + transform: ['/scale/'], + whitespace: ['nowrap'], + '/color/': ['/^green/'], + }, + ], + + accept: [ + { + code: 'div { whitespace: nowrap; }', + }, + { + code: 'a { transform: scale(1, 1); }', + }, + { + code: 'a { -webkit-transform: scale(1, 1); }', + }, + { + code: 'a { color: green; }', + }, + { + code: 'a { background-color: green; }', + }, + ], + + reject: [ + { + code: 'div { whitespace: pre; }', + message: messages.rejected('whitespace', 'pre'), + line: 1, + column: 7, + }, + { + code: 'a { transform: translate(1, 1); }', + message: messages.rejected('transform', 'translate(1, 1)'), + line: 1, + column: 5, + }, + { + code: 'a { -webkit-transform: translate(1, 1); }', + message: messages.rejected('-webkit-transform', 'translate(1, 1)'), + line: 1, + column: 5, + }, + { + code: 'a { color: pink; }', + message: messages.rejected('color', 'pink'), + line: 1, + column: 5, + }, + { + code: 'a { background-color: pink; }', + message: messages.rejected('background-color', 'pink'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + config: { position: ['static'] }, + skipBasicChecks: true, + accept: [ + { + code: 'a { font-size: 1em; }', + description: 'irrelevant CSS', + }, + ], +}); diff --git a/lib/rules/declaration-property-value-whitelist/index.js b/lib/rules/declaration-property-value-whitelist/index.js new file mode 100644 index 0000000000..d521b25632 --- /dev/null +++ b/lib/rules/declaration-property-value-whitelist/index.js @@ -0,0 +1,18 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../declaration-property-value-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'declaration-property-value-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-blacklist/README.md b/lib/rules/function-blacklist/README.md new file mode 100644 index 0000000000..2137865984 --- /dev/null +++ b/lib/rules/function-blacklist/README.md @@ -0,0 +1,54 @@ +# function-blacklist + +**_Deprecated: Instead use the [`function-denylist`](../function-denylist/README.md) rule._** + +Specify a list of disallowed functions. + + +```css +a { transform: scale(1); } +/** ↑ + * This function */ +``` + +## Options + +`array|string`: `["array", "of", "unprefixed", /functions/ or "regex"]|"function"|"/regex/"` + +If a string is surrounded with `"/"` (e.g. `"/^rgb/"`), it is interpreted as a regular expression. + +Given: + +``` +["scale", "rgba", "linear-gradient"] +``` + +The following patterns are considered violations: + + +```css +a { transform: scale(1); } +``` + + +```css +a { + color: rgba(0, 0, 0, 0.5); +} +``` + + +```css +a { + background: + red, + -moz-linear-gradient(45deg, blue, red); +} +``` + +The following patterns are _not_ considered violations: + + +```css +a { background: red; } +``` diff --git a/lib/rules/function-blacklist/__tests__/index.js b/lib/rules/function-blacklist/__tests__/index.js new file mode 100644 index 0000000000..c456d06ee5 --- /dev/null +++ b/lib/rules/function-blacklist/__tests__/index.js @@ -0,0 +1,243 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['rgba'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'function-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['rgba', 'scale', 'linear-gradient'], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { transform: SCALE(1); }', + }, + { + code: 'a { transform: sCaLe(1); }', + }, + { + code: 'a { transform: rotate(7deg) }', + }, + { + code: 'a { transform: rOtAtE(7deg) }', + }, + { + code: 'a { transform: ROTATE(7deg) }', + }, + { + code: 'a { background: -webkit-radial-gradient(red, green, blue); }', + }, + { + code: 'a { color: color(rgb(0, 0, 0) lightness(50%)); }', + }, + { + code: '@media (max-width: 10px) { a { color: color(rgb(0, 0, 0) lightness(50%)); } }', + }, + { + code: '$scale: (value, value2)', + description: 'Sass list ignored', + }, + ], + + reject: [ + { + code: 'a { transform: scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 16, + }, + { + code: 'a { transform : scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 17, + }, + { + code: 'a\n{ transform: scale(1); }', + message: messages.rejected('scale'), + line: 2, + column: 14, + }, + { + code: 'a { transform: scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 19, + }, + { + code: ' a { transform: scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 18, + }, + { + code: 'a { color: rgba(0, 0, 0, 0) }', + message: messages.rejected('rgba'), + line: 1, + column: 12, + }, + { + code: 'a { color: color(rgba(0, 0, 0, 0) lightness(50%)); }', + message: messages.rejected('rgba'), + line: 1, + column: 18, + }, + { + code: 'a { background: red, -moz-linear-gradient(45deg, blue, red); }', + message: messages.rejected('-moz-linear-gradient'), + line: 1, + column: 22, + }, + { + code: '@media (max-width: 10px) { a { color: color(rgba(0, 0, 0) lightness(50%)); } }', + message: messages.rejected('rgba'), + line: 1, + column: 45, + }, + ], +}); + +testRule({ + ruleName, + + config: ['/rgb/'], + + accept: [ + { + code: 'a { color: hsl(208, 100%, 97%); }', + }, + ], + + reject: [ + { + code: 'a { color: rgb(0, 0, 0); }', + message: messages.rejected('rgb'), + line: 1, + column: 12, + }, + { + code: 'a { color: rgba(0, 0, 0); }', + message: messages.rejected('rgba'), + line: 1, + column: 12, + }, + ], +}); + +testRule({ + ruleName, + + config: [/rgb/], + + accept: [ + { + code: 'a { color: hsl(208, 100%, 97%); }', + }, + ], + + reject: [ + { + code: 'a { color: rgb(0, 0, 0); }', + message: messages.rejected('rgb'), + line: 1, + column: 12, + }, + { + code: 'a { color: rgba(0, 0, 0); }', + message: messages.rejected('rgba'), + line: 1, + column: 12, + }, + ], +}); + +testRule({ + ruleName, + + config: ['skewx', 'translateX', 'SCALEX', '/rotate/i', '/MATRIX/'], + + accept: [ + { + code: 'a { transform: stewX(10deg); }', + }, + { + code: 'a { transform: translateY(5px); }', + }, + { + code: 'a { transform: scaleX(1); }', + }, + { + code: 'a { transform: matrix3d(a1); }', + }, + ], + + reject: [ + { + code: 'a { transform: skewx(10deg); }', + message: messages.rejected('skewx'), + line: 1, + column: 16, + }, + { + code: 'a { transform: translateX(5px); }', + message: messages.rejected('translateX'), + line: 1, + column: 16, + }, + { + code: 'a { transform: SCALEX(1); }', + message: messages.rejected('SCALEX'), + line: 1, + column: 16, + }, + { + code: 'a { transform: rotatex(60deg); }', + message: messages.rejected('rotatex'), + line: 1, + column: 16, + }, + { + code: 'a { transform: rotateX(60deg); }', + message: messages.rejected('rotateX'), + line: 1, + column: 16, + }, + { + code: 'a { transform: ROTATEX(60deg); }', + message: messages.rejected('ROTATEX'), + line: 1, + column: 16, + }, + { + code: 'a { transform: MATRIX3d(a1); }', + message: messages.rejected('MATRIX3d'), + line: 1, + column: 16, + }, + ], +}); diff --git a/lib/rules/function-blacklist/index.js b/lib/rules/function-blacklist/index.js new file mode 100644 index 0000000000..1aad9ddf46 --- /dev/null +++ b/lib/rules/function-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../function-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'function-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected function "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-url-scheme-blacklist/README.md b/lib/rules/function-url-scheme-blacklist/README.md new file mode 100644 index 0000000000..5a85b1cb7f --- /dev/null +++ b/lib/rules/function-url-scheme-blacklist/README.md @@ -0,0 +1,73 @@ +# function-url-scheme-blacklist + +**_Deprecated: Instead use the [`function-url-scheme-denylist`](../function-url-scheme-denylist/README.md) rule._** + +Specify a list of disallowed URL schemes. + + +```css +a { background-image: url('http://www.example.com/file.jpg'); } +/** ↑ + * This URL scheme */ +``` + +A [URL scheme](https://url.spec.whatwg.org/#syntax-url-scheme) consists of alphanumeric, `+`, `-`, and `.` characters. It can appear at the start of a URL and is followed by `:`. + +This rule ignores: + +- URL arguments without an existing URL scheme +- URL arguments with variables or variable interpolation (`$sass`, `@less`, `--custom-property`, `#{$var}`, `@{var}`, `$(var)`) + +## Options + +`array|string|regex`: `["array", "of", /schemes/ or "/regex/"]|"scheme"|/regex/` + +Given: + +``` +["ftp", "/^http/"] +``` + +The following patterns are considered violations: + + +```css +a { background-image: url('ftp://www.example.com/file.jpg'); } +``` + + +```css +a { background-image: url('http://www.example.com/file.jpg'); } +``` + + +```css +a { background-image: url('https://www.example.com/file.jpg'); } +``` + +The following patterns are _not_ considered violations: + + +```css +a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); } +``` + + +```css +a { background-image: url('example.com/file.jpg'); } +``` + + +```css +a { background-image: url('/example.com/file.jpg'); } +``` + + +```css +a { background-image: url('//example.com/file.jpg'); } +``` + + +```css +a { background-image: url('./path/to/file.jpg'); } +``` diff --git a/lib/rules/function-url-scheme-blacklist/__tests__/index.js b/lib/rules/function-url-scheme-blacklist/__tests__/index.js new file mode 100644 index 0000000000..fe93118309 --- /dev/null +++ b/lib/rules/function-url-scheme-blacklist/__tests__/index.js @@ -0,0 +1,280 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['https'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'function-url-scheme-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: [[]], + + accept: [ + { + code: "a { background: url('http://www.example.com/file.jpg'); }", + }, + ], +}); + +testRule({ + ruleName, + config: [''], + + accept: [ + { + code: "a { background: url('http://www.example.com/file.jpg'); }", + }, + ], +}); + +testRule({ + ruleName, + config: ['https', 'data'], + + accept: [ + { + code: 'a { background: url(); }', + }, + { + code: "a { background: url(''); }", + }, + { + code: 'a { background: url(""); }', + }, + { + code: 'a { background: url(:); }', + }, + { + code: 'a { background: url(://); }', + }, + { + code: 'a { background: url(//); }', + }, + { + code: 'a { background: url(/); }', + }, + { + code: 'a { background: url(./); }', + }, + { + code: 'a { background: url(./file.jpg); }', + }, + { + code: 'a { background: url(../file.jpg); }', + }, + { + code: 'a { background: URL(../file.jpg); }', + }, + { + code: "a { background: url('../file.jpg'); }", + }, + { + code: 'a { background: url("../file.jpg"); }', + }, + { + code: "a { background: url('/path/to/file.jpg'); }", + }, + { + code: 'a { background: url(//www.example.com/file.jpg); }', + }, + { + code: 'a { background: url("//www.example.com/file.jpg"); }', + }, + { + code: "a { background: url('http://www.example.com/file.jpg'); }", + }, + { + code: "a { background-image: url('http://example.com:3000'); }", + }, + { + code: "a { background-image: url('//example.com:3000'); }", + }, + { + code: "@font-face { font-family: 'foo'; src: url('/path/to/foo.ttf'); }", + }, + { + code: 'a { background: url(HTTP://example.com/file.jpg); }', + description: 'ignore case', + }, + { + code: 'a { background: some-url(); }', + description: 'ignore contain url function', + }, + { + code: 'a { background: url($image); }', + description: 'ignore variable', + }, + { + code: 'a { background: url(@image); }', + description: 'ignore variable', + }, + { + code: 'a { background: url(http://#{$host}/path); }', + description: 'ignore interpolation', + }, + { + code: "a { background: url('http://@{host}/path'); }", + description: 'ignore interpolation', + }, + { + code: 'a { background: url(http://$(host)/path); }', + description: 'ignore interpolation', + }, + { + code: 'a { background: url(var(--image)); }', + description: 'ignore variable', + }, + { + code: 'a { background: url(example.com); }', + description: 'schemeless url', + }, + { + code: 'a { background: url(example.com:3000); }', + description: 'schemeless url and port', + }, + { + code: 'a { background: url(http://example.com:3000); }', + description: 'url with scheme and port', + }, + ], + + reject: [ + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + message: messages.rejected('data'), + line: 1, + column: 27, + }, + { + code: 'a { background: url(HTTPS://www.example.com/file.jpg); }', + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: "a { background: url('https://www.example.com/file.jpg'); }", + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: 'a { background: url("https://www.example.com/file.jpg"); }', + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: "a { background: url('https://example.com:3000'); }", + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: "@font-face { font-family: 'foo'; src: url('https://www.example.com/file.jpg'); }", + message: messages.rejected('https'), + line: 1, + column: 43, + }, + { + code: "a { background: no-repeat center/80% url('https://www.example.com/file.jpg'); }", + message: messages.rejected('https'), + line: 1, + column: 42, + }, + ], +}); + +testRule({ + ruleName, + + config: [['/^http/']], + + accept: [ + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + }, + { + code: 'a { background: url(./file.jpg); }', + }, + ], + + reject: [ + { + code: 'a { background: url(https://example.com/file.jpg); }', + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: 'a { background: url(HTTPS://example.com/file.jpg); }', + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: 'a { background: url(http://example.com/file.jpg); }', + message: messages.rejected('http'), + line: 1, + column: 21, + }, + ], +}); + +testRule({ + ruleName, + + config: [[/^http/]], + + accept: [ + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + }, + { + code: 'a { background: url(./file.jpg); }', + }, + ], + + reject: [ + { + code: 'a { background: url(https://example.com/file.jpg); }', + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: 'a { background: url(HTTPS://example.com/file.jpg); }', + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: 'a { background: url(http://example.com/file.jpg); }', + message: messages.rejected('http'), + line: 1, + column: 21, + }, + ], +}); diff --git a/lib/rules/function-url-scheme-blacklist/index.js b/lib/rules/function-url-scheme-blacklist/index.js new file mode 100644 index 0000000000..ed6ceb1abc --- /dev/null +++ b/lib/rules/function-url-scheme-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../function-url-scheme-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'function-url-scheme-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (scheme) => `Unexpected URL scheme "${scheme}:"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-url-scheme-whitelist/README.md b/lib/rules/function-url-scheme-whitelist/README.md new file mode 100644 index 0000000000..5ea10b353a --- /dev/null +++ b/lib/rules/function-url-scheme-whitelist/README.md @@ -0,0 +1,78 @@ +# function-url-scheme-whitelist + +**_Deprecated: Instead use the [`function-url-scheme-allowlist`](../function-url-scheme-allowlist/README.md) rule._** + +Specify a list of allowed URL schemes. + + +```css +a { background-image: url('http://www.example.com/file.jpg'); } +/** ↑ + * This URL scheme */ +``` + +A [URL scheme](https://url.spec.whatwg.org/#syntax-url-scheme) consists of alphanumeric, `+`, `-`, and `.` characters. It can appear at the start of a URL and is followed by `:`. + +This rule ignores: + +- URL arguments without an existing URL scheme +- URL arguments with variables or variable interpolation (`$sass`, `@less`, `--custom-property`, `#{$var}`, `@{var}`, `$(var)`) + +## Options + +`array|string|regex`: `["array", "of", /schemes/ or "/regex/"]|"scheme"|/regex/` + +Given: + +``` +["data", "/^http/"] +``` + +The following patterns are considered violations: + + +```css +a { background-image: url('file://file.jpg'); } +``` + +The following patterns are _not_ considered violations: + + +```css +a { background-image: url('example.com/file.jpg'); } +``` + + +```css +a { background-image: url('/example.com/file.jpg'); } +``` + + +```css +a { background-image: url('//example.com/file.jpg'); } +``` + + +```css +a { background-image: url('./path/to/file.jpg'); } +``` + + +```css +a { background-image: url('http://www.example.com/file.jpg'); } +``` + + +```css +a { background-image: url('https://www.example.com/file.jpg'); } +``` + + +```css +a { background-image: url('HTTPS://www.example.com/file.jpg'); } +``` + + +```css +a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); } +``` diff --git a/lib/rules/function-url-scheme-whitelist/__tests__/index.js b/lib/rules/function-url-scheme-whitelist/__tests__/index.js new file mode 100644 index 0000000000..9677a76ceb --- /dev/null +++ b/lib/rules/function-url-scheme-whitelist/__tests__/index.js @@ -0,0 +1,342 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['https'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'function-url-scheme-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['https', 'data'], + + accept: [ + { + code: 'a { background: url(); }', + }, + { + code: "a { background: url(''); }", + }, + { + code: 'a { background: url(""); }', + }, + { + code: 'a { background: url(:); }', + }, + { + code: 'a { background: url(://); }', + }, + { + code: 'a { background: url(//); }', + }, + { + code: 'a { background: url(/); }', + }, + { + code: 'a { background: url(./); }', + }, + { + code: 'a { background: url(./file.jpg); }', + }, + { + code: 'a { background: url(../file.jpg); }', + }, + { + code: 'a { background: URL(../file.jpg); }', + }, + { + code: "a { background: url('../file.jpg'); }", + }, + { + code: 'a { background: url("../file.jpg"); }', + }, + { + code: "a { background: url('/path/to/file.jpg'); }", + }, + { + code: 'a { background: url(//www.example.com/file.jpg); }', + }, + { + code: 'a { background: url("//www.example.com/file.jpg"); }', + }, + { + code: "a { background: url('https://www.example.com/file.jpg'); }", + }, + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + }, + { + code: "a { background-image: url('https://example.com:3000'); }", + }, + { + code: "a { background-image: url('//example.com:3000'); }", + }, + { + code: "@font-face { font-family: 'foo'; src: url('/path/to/foo.ttf'); }", + }, + { + code: 'a { background: url(HTTPS://example.com/file.jpg); }', + description: 'ignore case', + }, + { + code: 'a { background: some-url(); }', + description: 'ignore contain url function', + }, + { + code: 'a { background: url($image); }', + description: 'ignore variable', + }, + { + code: 'a { background: url(@image); }', + description: 'ignore variable', + }, + { + code: 'a { background: url(http://#{$host}/path); }', + description: 'ignore interpolation', + }, + { + code: "a { background: url('http://@{host}/path'); }", + description: 'ignore interpolation', + }, + { + code: 'a { background: url(http://$(host)/path); }', + description: 'ignore interpolation', + }, + { + code: 'a { background: url(var(--image)); }', + description: 'ignore variable', + }, + { + code: 'a { background: url(example.com); }', + description: 'schemeless url', + }, + { + code: 'a { background: url(example.com:3000); }', + description: 'schemeless url and port', + }, + { + code: 'a { background: url(https://example.com:3000); }', + description: 'url with scheme and port', + }, + ], + + reject: [ + { + code: 'a { background: url(http://www.example.com/file.jpg); }', + message: messages.rejected('http'), + line: 1, + column: 21, + }, + { + code: "a { background: url('http://www.example.com/file.jpg'); }", + message: messages.rejected('http'), + line: 1, + column: 21, + }, + { + code: 'a { background: url("http://www.example.com/file.jpg"); }', + message: messages.rejected('http'), + line: 1, + column: 21, + }, + { + code: "a { background: url('http://example.com:3000'); }", + message: messages.rejected('http'), + line: 1, + column: 21, + }, + { + code: "@font-face { font-family: 'foo'; src: url('http://www.example.com/file.jpg'); }", + message: messages.rejected('http'), + line: 1, + column: 43, + }, + { + code: "a { background: no-repeat center/80% url('http://www.example.com/file.jpg'); }", + message: messages.rejected('http'), + line: 1, + column: 42, + }, + ], +}); + +testRule({ + ruleName, + config: [[]], + + accept: [ + { + code: "a { background: url('/path/to/file.jpg'); }", + }, + { + code: 'a { background: url(//www.example.com/file.jpg); }', + }, + { + code: 'a { background: url("//www.example.com/file.jpg"); }', + }, + { + code: 'a { background: url(example.com:3000); }', + }, + ], + + reject: [ + { + code: "a { background: url('https://www.example.com/file.jpg'); }", + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + message: messages.rejected('data'), + line: 1, + column: 27, + }, + ], +}); + +testRule({ + ruleName, + config: [''], + + accept: [ + { + code: "a { background: url('/path/to/file.jpg'); }", + }, + { + code: 'a { background: url(//www.example.com/file.jpg); }', + }, + { + code: 'a { background: url("//www.example.com/file.jpg"); }', + }, + { + code: 'a { background: url(example.com:3000); }', + }, + ], + + reject: [ + { + code: "a { background: url('https://www.example.com/file.jpg'); }", + message: messages.rejected('https'), + line: 1, + column: 21, + }, + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + message: messages.rejected('data'), + line: 1, + column: 27, + }, + ], +}); + +testRule({ + ruleName, + // primaryOptionArray + config: ['uri', 'file', 'https'], + + accept: [ + { + code: 'a { background: url(https://example.com/file.jpg); }', + }, + ], + + reject: [ + { + code: 'a { background: url(http://example.com/file.jpg); }', + message: messages.rejected('http'), + line: 1, + column: 21, + }, + ], +}); + +testRule({ + ruleName, + + config: [['/^http/']], + + accept: [ + { + code: 'a { background: url(http://example.com/file.jpg); }', + }, + { + code: 'a { background: url(HTTP://example.com/file.jpg); }', + }, + { + code: 'a { background: url(https://example.com/file.jpg); }', + }, + ], + + reject: [ + { + code: 'a { background: url(ftp://example.com/file.jpg); }', + message: messages.rejected('ftp'), + line: 1, + column: 21, + }, + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + message: messages.rejected('data'), + line: 1, + column: 27, + }, + ], +}); + +testRule({ + ruleName, + + config: [[/^http/]], + + accept: [ + { + code: 'a { background: url(http://example.com/file.jpg); }', + }, + { + code: 'a { background: url(HTTP://example.com/file.jpg); }', + }, + { + code: 'a { background: url(https://example.com/file.jpg); }', + }, + ], + + reject: [ + { + code: 'a { background: url(ftp://example.com/file.jpg); }', + message: messages.rejected('ftp'), + line: 1, + column: 21, + }, + { + code: + "a { background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='); }", + message: messages.rejected('data'), + line: 1, + column: 27, + }, + ], +}); diff --git a/lib/rules/function-url-scheme-whitelist/index.js b/lib/rules/function-url-scheme-whitelist/index.js new file mode 100644 index 0000000000..9257618825 --- /dev/null +++ b/lib/rules/function-url-scheme-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../function-url-scheme-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'function-url-scheme-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (scheme) => `Unexpected URL scheme "${scheme}:"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-whitelist/README.md b/lib/rules/function-whitelist/README.md new file mode 100644 index 0000000000..c88983607f --- /dev/null +++ b/lib/rules/function-whitelist/README.md @@ -0,0 +1,75 @@ +# function-whitelist + +**_Deprecated: Instead use the [`function-allowlist`](../function-allowlist/README.md) rule._** + +Specify a list of allowed functions. + + +```css +a { transform: scale(1); } +/** ↑ + * This function */ +``` + +## Options + +`array|string`: `["array", "of", "unprefixed", /functions/ or "regex"]|"function"|"/regex/"` + +If a string is surrounded with `"/"` (e.g. `"/^rgb/"`), it is interpreted as a regular expression. + +Given: + +``` +["scale", "rgba", "linear-gradient"] +``` + +The following patterns are considered violations: + + +```css +a { transform: rotate(1); } +``` + + +```css +a { + color: hsla(170, 50%, 45%, 1) +} +``` + + +```css +a { + background: + red, + -webkit-radial-gradient(red, green, blue); +} +``` + +The following patterns are _not_ considered violations: + + +```css +a { background: red; } +``` + + +```css +a { transform: scale(1); } +``` + + +```css +a { + color: rgba(0, 0, 0, 0.5); +} +``` + + +```css +a { + background: + red, + -moz-linear-gradient(45deg, blue, red); +} +``` diff --git a/lib/rules/function-whitelist/__tests__/index.js b/lib/rules/function-whitelist/__tests__/index.js new file mode 100644 index 0000000000..a9f50d3f83 --- /dev/null +++ b/lib/rules/function-whitelist/__tests__/index.js @@ -0,0 +1,204 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['rgba'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'function-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['rotate', 'rgb', 'radial-gradient', 'lightness', 'color'], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { transform: rotate(7deg) }', + }, + { + code: 'a { background: -webkit-radial-gradient(red, green, blue); }', + }, + { + code: 'a { color: color(rgb(0, 0, 0) lightness(50%)); }', + }, + { + code: '@media (max-width: 10px) { a { color: color(rgb(0, 0, 0) lightness(50%)); } }', + }, + { + code: '$list: (value, value2)', + description: 'Sass list ignored', + }, + ], + + reject: [ + { + code: 'a { transform: rOtAtE(7deg) }', + message: messages.rejected('rOtAtE'), + line: 1, + column: 16, + }, + { + code: 'a { transform: ROTATE(7deg) }', + message: messages.rejected('ROTATE'), + line: 1, + column: 16, + }, + { + code: 'a { transform: scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 16, + }, + { + code: 'a { transform: sCaLe(1); }', + message: messages.rejected('sCaLe'), + line: 1, + column: 16, + }, + { + code: 'a { transform: SCALE(1); }', + message: messages.rejected('SCALE'), + line: 1, + column: 16, + }, + { + code: 'a { transform : scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 17, + }, + { + code: 'a\n{ transform: scale(1); }', + message: messages.rejected('scale'), + line: 2, + column: 14, + }, + { + code: 'a { transform: scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 19, + }, + { + code: ' a { transform: scale(1); }', + message: messages.rejected('scale'), + line: 1, + column: 18, + }, + { + code: 'a { color: rgba(0, 0, 0, 0) }', + message: messages.rejected('rgba'), + line: 1, + column: 12, + }, + { + code: 'a { color: color(rgba(0, 0, 0, 0) lightness(50%)); }', + message: messages.rejected('rgba'), + line: 1, + column: 18, + }, + { + code: 'a { background: red, -moz-linear-gradient(45deg, blue, red); }', + message: messages.rejected('-moz-linear-gradient'), + line: 1, + column: 22, + }, + { + code: '@media (max-width: 10px) { a { color: color(rgba(0, 0, 0) lightness(50%)); } }', + message: messages.rejected('rgba'), + line: 1, + column: 45, + }, + ], +}); + +testRule({ + ruleName, + config: ['translate'], + skipBasicChecks: true, + + accept: [ + { + code: 'a { transform: translate(1px); }', + }, + ], + + reject: [ + { + code: 'a { transform: scale(4); }', + message: messages.rejected('scale'), + line: 1, + column: 16, + }, + ], +}); + +testRule({ + ruleName, + + config: ['/rgb/'], + + accept: [ + { + code: 'a { color: rgb(0, 0, 0); }', + }, + { + code: 'a { color: rgba(0, 0, 0, 0); }', + }, + ], + + reject: [ + { + code: 'a { color: hsl(208, 100%, 97%); }', + message: messages.rejected('hsl'), + line: 1, + column: 12, + }, + ], +}); + +testRule({ + ruleName, + + config: [/rgb/], + + accept: [ + { + code: 'a { color: rgb(0, 0, 0); }', + }, + { + code: 'a { color: rgba(0, 0, 0, 0); }', + }, + ], + + reject: [ + { + code: 'a { color: hsl(208, 100%, 97%); }', + message: messages.rejected('hsl'), + line: 1, + column: 12, + }, + ], +}); diff --git a/lib/rules/function-whitelist/index.js b/lib/rules/function-whitelist/index.js new file mode 100644 index 0000000000..818ddc76da --- /dev/null +++ b/lib/rules/function-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../function-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'function-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected function "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/index.js b/lib/rules/index.js index ffbd3d873e..dd7afc6677 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -8,6 +8,7 @@ const importLazy = require('import-lazy'); const rules = { 'alpha-value-notation': importLazy(() => require('./alpha-value-notation'))(), 'at-rule-allowlist': importLazy(() => require('./at-rule-allowlist'))(), + 'at-rule-blacklist': importLazy(() => require('./at-rule-blacklist'))(), 'at-rule-denylist': importLazy(() => require('./at-rule-denylist'))(), 'at-rule-empty-line-before': importLazy(() => require('./at-rule-empty-line-before'))(), 'at-rule-name-case': importLazy(() => require('./at-rule-name-case'))(), @@ -20,6 +21,7 @@ const rules = { 'at-rule-semicolon-newline-after': importLazy(() => require('./at-rule-semicolon-newline-after'), )(), + 'at-rule-whitelist': importLazy(() => require('./at-rule-whitelist'))(), 'block-closing-brace-empty-line-before': importLazy(() => require('./block-closing-brace-empty-line-before'), )(), @@ -57,6 +59,7 @@ const rules = { 'comment-empty-line-before': importLazy(() => require('./comment-empty-line-before'))(), 'comment-no-empty': importLazy(() => require('./comment-no-empty'))(), 'comment-whitespace-inside': importLazy(() => require('./comment-whitespace-inside'))(), + 'comment-word-blacklist': importLazy(() => require('./comment-word-blacklist'))(), 'comment-word-denylist': importLazy(() => require('./comment-word-denylist'))(), 'custom-media-pattern': importLazy(() => require('./custom-media-pattern'))(), 'custom-property-empty-line-before': importLazy(() => @@ -102,15 +105,27 @@ const rules = { 'declaration-property-unit-allowlist': importLazy(() => require('./declaration-property-unit-allowlist'), )(), + 'declaration-property-unit-blacklist': importLazy(() => + require('./declaration-property-unit-blacklist'), + )(), 'declaration-property-unit-denylist': importLazy(() => require('./declaration-property-unit-denylist'), )(), + 'declaration-property-unit-whitelist': importLazy(() => + require('./declaration-property-unit-whitelist'), + )(), 'declaration-property-value-allowlist': importLazy(() => require('./declaration-property-value-allowlist'), )(), + 'declaration-property-value-blacklist': importLazy(() => + require('./declaration-property-value-blacklist'), + )(), 'declaration-property-value-denylist': importLazy(() => require('./declaration-property-value-denylist'), )(), + 'declaration-property-value-whitelist': importLazy(() => + require('./declaration-property-value-whitelist'), + )(), 'font-family-no-missing-generic-family-keyword': importLazy(() => require('./font-family-no-missing-generic-family-keyword'), )(), @@ -118,6 +133,7 @@ const rules = { 'font-family-no-duplicate-names': importLazy(() => require('./font-family-no-duplicate-names'))(), 'font-weight-notation': importLazy(() => require('./font-weight-notation'))(), 'function-allowlist': importLazy(() => require('./function-allowlist'))(), + 'function-blacklist': importLazy(() => require('./function-blacklist'))(), 'function-calc-no-invalid': importLazy(() => require('./function-calc-no-invalid'))(), 'function-calc-no-unspaced-operator': importLazy(() => require('./function-calc-no-unspaced-operator'), @@ -143,7 +159,10 @@ const rules = { )(), 'function-url-quotes': importLazy(() => require('./function-url-quotes'))(), 'function-url-scheme-allowlist': importLazy(() => require('./function-url-scheme-allowlist'))(), + 'function-url-scheme-blacklist': importLazy(() => require('./function-url-scheme-blacklist'))(), 'function-url-scheme-denylist': importLazy(() => require('./function-url-scheme-denylist'))(), + 'function-url-scheme-whitelist': importLazy(() => require('./function-url-scheme-whitelist'))(), + 'function-whitelist': importLazy(() => require('./function-whitelist'))(), 'function-whitespace-after': importLazy(() => require('./function-whitespace-after'))(), 'hue-degree-notation': importLazy(() => require('./hue-degree-notation'))(), 'keyframe-declaration-no-important': importLazy(() => @@ -162,6 +181,7 @@ const rules = { require('./media-feature-colon-space-before'), )(), 'media-feature-name-allowlist': importLazy(() => require('./media-feature-name-allowlist'))(), + 'media-feature-name-blacklist': importLazy(() => require('./media-feature-name-blacklist'))(), 'media-feature-name-case': importLazy(() => require('./media-feature-name-case'))(), 'media-feature-name-denylist': importLazy(() => require('./media-feature-name-denylist'))(), 'media-feature-name-no-unknown': importLazy(() => require('./media-feature-name-no-unknown'))(), @@ -171,6 +191,10 @@ const rules = { 'media-feature-name-value-allowlist': importLazy(() => require('./media-feature-name-value-allowlist'), )(), + 'media-feature-name-value-whitelist': importLazy(() => + require('./media-feature-name-value-whitelist'), + )(), + 'media-feature-name-whitelist': importLazy(() => require('./media-feature-name-whitelist'))(), 'media-feature-parentheses-space-inside': importLazy(() => require('./media-feature-parentheses-space-inside'), )(), @@ -210,10 +234,12 @@ const rules = { 'number-max-precision': importLazy(() => require('./number-max-precision'))(), 'number-no-trailing-zeros': importLazy(() => require('./number-no-trailing-zeros'))(), 'property-allowlist': importLazy(() => require('./property-allowlist'))(), + 'property-blacklist': importLazy(() => require('./property-blacklist'))(), 'property-case': importLazy(() => require('./property-case'))(), 'property-denylist': importLazy(() => require('./property-denylist'))(), 'property-no-unknown': importLazy(() => require('./property-no-unknown'))(), 'property-no-vendor-prefix': importLazy(() => require('./property-no-vendor-prefix'))(), + 'property-whitelist': importLazy(() => require('./property-whitelist'))(), 'rule-empty-line-before': importLazy(() => require('./rule-empty-line-before'))(), 'selector-attribute-brackets-space-inside': importLazy(() => require('./selector-attribute-brackets-space-inside'), @@ -221,6 +247,9 @@ const rules = { 'selector-attribute-operator-allowlist': importLazy(() => require('./selector-attribute-operator-allowlist'), )(), + 'selector-attribute-operator-blacklist': importLazy(() => + require('./selector-attribute-operator-blacklist'), + )(), 'selector-attribute-operator-denylist': importLazy(() => require('./selector-attribute-operator-denylist'), )(), @@ -230,9 +259,13 @@ const rules = { 'selector-attribute-operator-space-before': importLazy(() => require('./selector-attribute-operator-space-before'), )(), + 'selector-attribute-operator-whitelist': importLazy(() => + require('./selector-attribute-operator-whitelist'), + )(), 'selector-attribute-quotes': importLazy(() => require('./selector-attribute-quotes'))(), 'selector-class-pattern': importLazy(() => require('./selector-class-pattern'))(), 'selector-combinator-allowlist': importLazy(() => require('./selector-combinator-allowlist'))(), + 'selector-combinator-blacklist': importLazy(() => require('./selector-combinator-blacklist'))(), 'selector-combinator-denylist': importLazy(() => require('./selector-combinator-denylist'))(), 'selector-combinator-space-after': importLazy(() => require('./selector-combinator-space-after'), @@ -240,6 +273,7 @@ const rules = { 'selector-combinator-space-before': importLazy(() => require('./selector-combinator-space-before'), )(), + 'selector-combinator-whitelist': importLazy(() => require('./selector-combinator-whitelist'))(), 'selector-descendant-combinator-no-non-space': importLazy(() => require('./selector-descendant-combinator-no-non-space'), )(), @@ -274,6 +308,9 @@ const rules = { 'selector-pseudo-class-allowlist': importLazy(() => require('./selector-pseudo-class-allowlist'), )(), + 'selector-pseudo-class-blacklist': importLazy(() => + require('./selector-pseudo-class-blacklist'), + )(), 'selector-pseudo-class-case': importLazy(() => require('./selector-pseudo-class-case'))(), 'selector-pseudo-class-denylist': importLazy(() => require('./selector-pseudo-class-denylist'))(), 'selector-pseudo-class-no-unknown': importLazy(() => @@ -282,9 +319,15 @@ const rules = { 'selector-pseudo-class-parentheses-space-inside': importLazy(() => require('./selector-pseudo-class-parentheses-space-inside'), )(), + 'selector-pseudo-class-whitelist': importLazy(() => + require('./selector-pseudo-class-whitelist'), + )(), 'selector-pseudo-element-allowlist': importLazy(() => require('./selector-pseudo-element-allowlist'), )(), + 'selector-pseudo-element-blacklist': importLazy(() => + require('./selector-pseudo-element-blacklist'), + )(), 'selector-pseudo-element-case': importLazy(() => require('./selector-pseudo-element-case'))(), 'selector-pseudo-element-colon-notation': importLazy(() => require('./selector-pseudo-element-colon-notation'), @@ -295,6 +338,9 @@ const rules = { 'selector-pseudo-element-no-unknown': importLazy(() => require('./selector-pseudo-element-no-unknown'), )(), + 'selector-pseudo-element-whitelist': importLazy(() => + require('./selector-pseudo-element-whitelist'), + )(), 'selector-type-case': importLazy(() => require('./selector-type-case'))(), 'selector-type-no-unknown': importLazy(() => require('./selector-type-no-unknown'))(), 'shorthand-property-no-redundant-values': importLazy(() => @@ -305,9 +351,11 @@ const rules = { 'time-min-milliseconds': importLazy(() => require('./time-min-milliseconds'))(), 'unicode-bom': importLazy(() => require('./unicode-bom'))(), 'unit-allowlist': importLazy(() => require('./unit-allowlist'))(), + 'unit-blacklist': importLazy(() => require('./unit-blacklist'))(), 'unit-case': importLazy(() => require('./unit-case'))(), 'unit-denylist': importLazy(() => require('./unit-denylist'))(), 'unit-no-unknown': importLazy(() => require('./unit-no-unknown'))(), + 'unit-whitelist': importLazy(() => require('./unit-whitelist'))(), 'value-keyword-case': importLazy(() => require('./value-keyword-case'))(), 'value-list-comma-newline-after': importLazy(() => require('./value-list-comma-newline-after'))(), 'value-list-comma-newline-before': importLazy(() => diff --git a/lib/rules/media-feature-name-blacklist/README.md b/lib/rules/media-feature-name-blacklist/README.md new file mode 100644 index 0000000000..1b9ce1e395 --- /dev/null +++ b/lib/rules/media-feature-name-blacklist/README.md @@ -0,0 +1,66 @@ +# media-feature-name-blacklist + +**_Deprecated: Instead use the [`media-feature-name-denylist`](../media-feature-name-denylist/README.md) rule._** + +Specify a list of disallowed media feature names. + + +```css +@media (min-width: 700px) {} +/** ↑ + * This media feature name */ +``` + +## Options + +`array|string|regex`: `["array", "of", "unprefixed", /media-features/ or "regex"]|"media-feature"|/regex/` + +Given: + +``` +["max-width", "/^my-/"] +``` + +The following patterns are considered violations: + + +```css +@media (max-width: 50em) {} +``` + + +```css +@media (my-width: 50em) {} +``` + + +```css +@media (max-width < 50em) {} +``` + + +```css +@media (10em < my-height < 50em) {} +``` + +The following patterns are _not_ considered violations: + + +```css +@media (min-width: 50em) {} +``` + + +```css +@media print and (min-resolution: 300dpi) {} +``` + + +```css +@media (min-width >= 50em) {} +``` + + +```css +@media (10em < width < 50em) {} +``` diff --git a/lib/rules/media-feature-name-blacklist/__tests__/index.js b/lib/rules/media-feature-name-blacklist/__tests__/index.js new file mode 100644 index 0000000000..eb3db52bc0 --- /dev/null +++ b/lib/rules/media-feature-name-blacklist/__tests__/index.js @@ -0,0 +1,228 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['max-width'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'media-feature-name-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['max-width', '--wide-viewport', 'width', '/^my-/', 'color'], + + accept: [ + { + code: '@media (min-width: 50em) { }', + }, + { + code: '@media (MaX-wIdTh: 50em) { }', + }, + { + code: '@media (MiN-wIdTh: 50em) { }', + }, + { + code: '@media (height <= 50em) { }', + }, + { + code: '@media (400px < height < 1000px) { }', + }, + { + code: '@media (--wide-viewport) { }', + description: 'ignore custom media query', + }, + { + code: '@media (/* max-width: 50em */ min-width: 50em) { }', + description: 'ignore comments', + }, + { + code: '@media (monochrome) { }', + description: 'boolean feature name', + }, + ], + + reject: [ + { + code: '@media (max-width: 50em) { }', + message: messages.rejected('max-width'), + line: 1, + column: 9, + }, + { + code: '@media print and (max-width: 50em) { }', + message: messages.rejected('max-width'), + line: 1, + column: 19, + }, + { + code: '@media handheld and (min-width: 20em), screen and (max-width: 20em) { }', + message: messages.rejected('max-width'), + line: 1, + column: 52, + }, + { + code: '@media (my-width: 50em) { }', + message: messages.rejected('my-width'), + line: 1, + column: 9, + }, + { + code: '@media (color) { }', + message: messages.rejected('color'), + line: 1, + column: 9, + }, + { + code: '@media (width: 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 9, + }, + { + code: '@media (20em < width <= 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 16, + }, + { + code: '@media (10em < max-width <= 50em) and (width > 50em) { }', + warnings: [ + { + message: messages.rejected('max-width'), + line: 1, + column: 16, + }, + { + message: messages.rejected('width'), + line: 1, + column: 40, + }, + ], + }, + ], +}); + +testRule({ + ruleName, + config: [/^my-/], + + accept: [ + { + code: '@media (min-width: 50em) { }', + }, + ], + + reject: [ + { + code: '@media (my-width: 50em) { }', + message: messages.rejected('my-width'), + line: 1, + column: 9, + }, + { + code: '@media (my-width >= 50em) { }', + message: messages.rejected('my-width'), + line: 1, + column: 9, + }, + { + code: '@media (10em < my-width <= 50em) { }', + message: messages.rejected('my-width'), + line: 1, + column: 16, + }, + { + code: '@media (50em < my-width) { }', + message: messages.rejected('my-width'), + line: 1, + column: 16, + }, + ], +}); + +testRule({ + ruleName, + config: ['feature-name'], + syntax: 'less', + + accept: [ + { + code: '@media @feature-name and (orientation: landscape) { }', + }, + { + code: '@media @feature-name { }', + }, + ], +}); + +testRule({ + ruleName, + config: ['feature-name', 'width'], + syntax: 'scss', + + accept: [ + { + code: '@media not all and ($feature-name) { }', + }, + { + code: '@media not all and ($FEATURE-NAME) { }', + }, + { + code: '@media not all and (#{feature-name}) { }', + }, + { + code: '@media not all and (#{FEATURE-NAME}) { }', + }, + { + code: '@media ($feature-name: $value) { }', + }, + { + code: '@media ($FEATURE-NAME: $value) { }', + }, + { + code: '@media (#{$feature-name}: $value) { }', + }, + { + code: '@media (#{$FEATURE-NAME}: $value) { }', + }, + { + code: "@media ('min-' + $width: $value) { }", + }, + { + code: "@media ('MIN-' + $WIDTH: $value) { }", + }, + { + code: "@media ($value + 'width': $value) { }", + }, + { + code: "@media ($VALUE + 'WIDTH': $value) { }", + }, + { + code: '@media (#{$width}: $value) { }', + }, + { + code: '@media (#{$WIDTH}: $value) { }', + }, + { + code: '@media #{$feature-name} { }', + }, + ], +}); diff --git a/lib/rules/media-feature-name-blacklist/index.js b/lib/rules/media-feature-name-blacklist/index.js new file mode 100644 index 0000000000..daefd29e35 --- /dev/null +++ b/lib/rules/media-feature-name-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../media-feature-name-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'media-feature-name-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected media feature name "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/media-feature-name-value-whitelist/README.md b/lib/rules/media-feature-name-value-whitelist/README.md new file mode 100644 index 0000000000..4c906dcb5a --- /dev/null +++ b/lib/rules/media-feature-name-value-whitelist/README.md @@ -0,0 +1,86 @@ +# media-feature-name-value-whitelist + +**_Deprecated: Instead use the [`media-feature-name-value-allowlist`](../media-feature-name-value-allowlist/README.md) rule._** + +Specify a list of allowed media feature name and value pairs. + + +```css +@media screen and (min-width: 768px) {} +/** ↑ ↑ + * These features and values */ +``` + +## Options + +```js +{ + "unprefixed-media-feature-name": ["array", "of", "values"], + "/unprefixed-media-feature-name/": ["/regex/", "non-regex", /real-regex/] +} +``` + +If a media feature name is found in the object, only its whitelisted values are +allowed. If the media feature name is not included in the object, anything goes. + +If a name or value is surrounded with `/` (e.g. `"/width$/"`), it is interpreted +as a regular expression. For example, `/width$/` will match `max-width` and +`min-width`. + +Given: + +``` +{ + "min-width": ["768px", "1024px"], + "/resolution/": ["/dpcm$/"] +} +``` + +The following patterns are considered violations: + + +```css +@media screen and (min-width: 1000px) {} +``` + + +```css +@media screen and (min-resolution: 2dpi) {} +``` + + +```css +@media screen and (min-width > 1000px) {} +``` + +The following patterns are _not_ considered violations: + + +```css +@media screen and (min-width: 768px) {} +``` + + +```css +@media screen and (min-width: 1024px) {} +``` + + +```css +@media screen and (orientation: portrait) {} +``` + + +```css +@media screen and (min-resolution: 2dpcm) {} +``` + + +```css +@media screen and (resolution: 10dpcm) {} +``` + + +```css +@media screen and (768px < min-width) {} +``` diff --git a/lib/rules/media-feature-name-value-whitelist/__tests__/index.js b/lib/rules/media-feature-name-value-whitelist/__tests__/index.js new file mode 100644 index 0000000000..400423cded --- /dev/null +++ b/lib/rules/media-feature-name-value-whitelist/__tests__/index.js @@ -0,0 +1,195 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [{ color: [] }], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'media-feature-name-value-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: [ + { + 'min-width': ['768px', '$sm'], + '/resolution/': ['/dpcm$/'], // Only dpcm unit + color: [], // Test boolean context + width: [], // Test range context + }, + ], + + accept: [ + { + code: '@media screen and (min-width: 768px) {}', + description: 'Specified media feature', + }, + { + code: '@media screen and ( min-width : 768px ) {}', + description: 'Whitespace', + }, + { + code: '@media screen and (max-width: 1000px) {}', + description: 'Unspecified media feature', + }, + { + code: '@media screen and ( min-resolution : 2dpcm ) {}', + description: 'Regex feature name and Regex value', + }, + { + code: '@media screen and (resolution: 10.1dpcm) {}', + description: 'Floating point value', + }, + { + code: '@media screen and (min-width: $sm) {}', + description: 'Non-standard syntax in whitelist', + }, + { + code: '@media (color) {}', + description: 'Boolean context, media feature in whitelist', + }, + { + code: '@media (update) {}', + description: 'Boolean context, media feature NOT in whitelist', + }, + { + code: '@media (update /* pw:ned */) {}', + description: 'Boolean context with colon in comments', + }, + { + code: '@media screen and (min-width <= 768px) {}', + description: 'Range context, media feature in whitelist', + }, + ], + + reject: [ + { + code: '@media screen and (min-width: 1000px) {}', + message: messages.rejected('min-width', '1000px'), + line: 1, + column: 31, + }, + { + code: '@media screen (min-width: 768px) and (min-width: 1000px) {}', + description: 'Media feature multiple', + message: messages.rejected('min-width', '1000px'), + line: 1, + column: 50, + }, + { + code: '@media screen (min-width: 768px)\nand (min-width: 1000px) {}', + description: 'Media feature multiline', + message: messages.rejected('min-width', '1000px'), + line: 2, + column: 17, + }, + { + code: '@media screen and (min-width: 768PX) {}', + description: 'Case sensitive', + message: messages.rejected('min-width', '768PX'), + line: 1, + column: 31, + }, + { + code: '@media screen and (min-width: $md) {}', + description: 'Non-standard syntax NOT in whitelist', + message: messages.rejected('min-width', '$md'), + line: 1, + column: 31, + }, + { + code: '@media screen and (min-resolution: 2dpi) {}', + message: messages.rejected('min-resolution', '2dpi'), + line: 1, + column: 37, + }, + { + code: '@media screen and (min-width > 500px) {}', + message: messages.rejected('min-width', '500px'), + line: 1, + column: 32, + }, + { + code: '@media screen and (400px < min-width) {}', + message: messages.rejected('min-width', '400px'), + line: 1, + column: 20, + }, + { + code: '@media (400px < min-width < 500px) and (min-width < 1200px)', + warnings: [ + { + message: messages.rejected('min-width', '400px'), + line: 1, + column: 9, + }, + { + message: messages.rejected('min-width', '500px'), + line: 1, + column: 29, + }, + { + message: messages.rejected('min-width', '1200px'), + line: 1, + column: 53, + }, + ], + }, + ], +}); + +testRule({ + ruleName, + config: [ + { + '/resolution/': [/dpcm$/], // Only dpcm unit + }, + ], + + accept: [ + { + code: '@media screen and (min-width: 768px) {}', + description: 'Specified media feature', + }, + { + code: '@media screen and ( min-resolution : 2dpcm ) {}', + description: 'Regex feature name and Regex value', + }, + { + code: '@media screen and (resolution: 10.1dpcm) {}', + description: 'Floating point value', + }, + ], + + reject: [ + { + code: '@media screen and (min-resolution: 2dpi) {}', + message: messages.rejected('min-resolution', '2dpi'), + line: 1, + column: 37, + }, + { + code: '@media screen and (min-resolution > 2dpi) {}', + message: messages.rejected('min-resolution', '2dpi'), + line: 1, + column: 38, + }, + ], +}); diff --git a/lib/rules/media-feature-name-value-whitelist/index.js b/lib/rules/media-feature-name-value-whitelist/index.js new file mode 100644 index 0000000000..4830814568 --- /dev/null +++ b/lib/rules/media-feature-name-value-whitelist/index.js @@ -0,0 +1,18 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../media-feature-name-value-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'media-feature-name-value-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name, value) => `Unexpected value "${value}" for name "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/media-feature-name-whitelist/README.md b/lib/rules/media-feature-name-whitelist/README.md new file mode 100644 index 0000000000..8abd51ce6c --- /dev/null +++ b/lib/rules/media-feature-name-whitelist/README.md @@ -0,0 +1,66 @@ +# media-feature-name-whitelist + +**_Deprecated: Instead use the [`media-feature-name-allowlist`](../media-feature-name-allowlist/README.md) rule._** + +Specify a list of allowed media feature names. + + +```css +@media (min-width: 700px) {} +/** ↑ + * This media feature name */ +``` + +## Options + +`array|string|regex`: `["array", "of", "unprefixed", /media-features/ or "regex"]|"media-feature"|/regex/` + +Given: + +``` +["max-width", "/^my-/"] +``` + +The following patterns are considered violations: + + +```css +@media (min-width: 50em) {} +``` + + +```css +@media print and (min-resolution: 300dpi) {} +``` + + +```css +@media (min-width < 50em) {} +``` + + +```css +@media (10em < min-width < 50em) {} +``` + +The following patterns are _not_ considered violations: + + +```css +@media (max-width: 50em) {} +``` + + +```css +@media (my-width: 50em) {} +``` + + +```css +@media (max-width > 50em) {} +``` + + +```css +@media (10em < my-width < 50em) {} +``` diff --git a/lib/rules/media-feature-name-whitelist/__tests__/index.js b/lib/rules/media-feature-name-whitelist/__tests__/index.js new file mode 100644 index 0000000000..d79d5b4616 --- /dev/null +++ b/lib/rules/media-feature-name-whitelist/__tests__/index.js @@ -0,0 +1,218 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['max-width'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'media-feature-name-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['max-width', '/^my-/', 'color'], + + accept: [ + { + code: '@media (max-width: 50em) { }', + }, + { + code: '@media (--wide-viewport) { }', + description: 'ignore custom media query', + }, + { + code: '@media (/* min-width: 50em */ max-width: 50em) { }', + description: 'ignore comments', + }, + { + code: '@media (max-width <= 50em) { }', + }, + { + code: '@media (400px < my-width < 1000px) { }', + }, + { + code: '@media (my-width: 50em) { }', + }, + { + code: '@media (my-max-width: 50em) { }', + }, + { + code: '@media print and (max-width: 50em) { }', + }, + { + code: '@media (color) { }', + }, + ], + + reject: [ + { + code: '@media (MaX-wIdTh: 50em) { }', + message: messages.rejected('MaX-wIdTh'), + line: 1, + column: 9, + }, + { + code: '@media (min-width: 50em) { }', + message: messages.rejected('min-width'), + line: 1, + column: 9, + }, + { + code: '@media (-webkit-min-device-pixel-ratio: 2) { }', + message: messages.rejected('-webkit-min-device-pixel-ratio'), + line: 1, + column: 9, + }, + { + code: '@media handheld and (max-width: 20em), screen and (min-width: 20em) { }', + message: messages.rejected('min-width'), + line: 1, + column: 52, + }, + { + code: '@media (monochrome) { }', + message: messages.rejected('monochrome'), + line: 1, + column: 9, + }, + { + code: '@media (width: 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 9, + }, + { + code: '@media (50em < width) { }', + message: messages.rejected('width'), + line: 1, + column: 16, + }, + { + code: '@media (10em < width <= 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 16, + }, + { + code: '@media (max-width <= 50em) and (10em < min-width < 50em) { }', + message: messages.rejected('min-width'), + line: 1, + column: 40, + }, + ], +}); + +testRule({ + ruleName, + config: [/^my-/], + + accept: [ + { + code: '@media (my-width: 50em) { }', + }, + { + code: '@media (my-max-width: 50em) { }', + }, + { + code: '@media (my-width >= 50em) { }', + }, + { + code: '@media (10em < my-max-width <= 50em) { }', + }, + ], + + reject: [ + { + code: '@media (MaX-wIdTh: 50em) { }', + message: messages.rejected('MaX-wIdTh'), + line: 1, + column: 9, + }, + ], +}); + +testRule({ + ruleName, + config: ['max-width', 'orientation'], + syntax: 'less', + + accept: [ + { + code: '@media @feature-name and (orientation: landscape) { }', + }, + { + code: '@media @feature-name { }', + }, + ], +}); + +testRule({ + ruleName, + config: ['max-width'], + syntax: 'scss', + + accept: [ + { + code: '@media not all and ($feature-name) { }', + }, + { + code: '@media not all and ($FEATURE-NAME) { }', + }, + { + code: '@media not all and (#{feature-name}) { }', + }, + { + code: '@media not all and (#{FEATURE-NAME}) { }', + }, + { + code: '@media ($feature-name: $value) { }', + }, + { + code: '@media ($FEATURE-NAME: $value) { }', + }, + { + code: '@media (#{$feature-name}: $value) { }', + }, + { + code: '@media (#{$FEATURE-NAME}: $value) { }', + }, + { + code: "@media ('min-' + $width: $value) { }", + }, + { + code: "@media ('MIN-' + $WIDTH: $value) { }", + }, + { + code: "@media ($value + 'width': $value) { }", + }, + { + code: "@media ($VALUE + 'WIDTH': $value) { }", + }, + { + code: '@media (#{$width}: $value) { }', + }, + { + code: '@media (#{$WIDTH}: $value) { }', + }, + { + code: '@media #{$feature-name} { }', + }, + ], +}); diff --git a/lib/rules/media-feature-name-whitelist/index.js b/lib/rules/media-feature-name-whitelist/index.js new file mode 100644 index 0000000000..6cf4aeefb3 --- /dev/null +++ b/lib/rules/media-feature-name-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../media-feature-name-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'media-feature-name-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected media feature name "${name}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/property-blacklist/README.md b/lib/rules/property-blacklist/README.md new file mode 100644 index 0000000000..2824d37b83 --- /dev/null +++ b/lib/rules/property-blacklist/README.md @@ -0,0 +1,66 @@ +# property-blacklist + +**_Deprecated: Instead use the [`property-denylist`](../property-denylist/README.md) rule._** + +Specify a list of disallowed properties. + + +```css +a { text-rendering: optimizeLegibility; } +/** ↑ + * This property */ +``` + +## Options + +`array|string`: `["array", "of", "unprefixed", /properties/ or "regex"]|"property"|"/regex/"`|/regex/ + +If a string is surrounded with `"/"` (e.g. `"/^background/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^background/` will match `background`, `background-size`, `background-color`, etc. + +Given: + +``` +["text-rendering", "animation", "/^background/"] +``` + +The following patterns are considered violations: + + +```css +a { text-rendering: optimizeLegibility; } +``` + + +```css +a { + animation: my-animation 2s; + color: pink; +} +``` + + +```css +a { -webkit-animation: my-animation 2s; } +``` + + +```css +a { background: pink; } +``` + + +```css +a { background-size: cover; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { color: pink; } +``` + + +```css +a { no-background: sure; } +``` diff --git a/lib/rules/property-blacklist/__tests__/index.js b/lib/rules/property-blacklist/__tests__/index.js new file mode 100644 index 0000000000..58394cc8ca --- /dev/null +++ b/lib/rules/property-blacklist/__tests__/index.js @@ -0,0 +1,182 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'property-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: [''], + + accept: [ + { + code: 'a { color: pink; }', + }, + ], +}); + +testRule({ + ruleName, + + config: [[]], + + accept: [ + { + code: 'a { color: pink; }', + }, + ], +}); + +testRule({ + ruleName, + + config: ['transform', 'background-size'], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { background: red; }', + }, + { + code: 'a { top: 0; color: pink; }', + }, + { + code: 'a { $scss: 0; }', + }, + { + code: 'a { @less: 0; }', + }, + { + code: 'a { --custom-property: 0; }', + }, + ], + + reject: [ + { + code: 'a { transform: scale(1); }', + message: messages.rejected('transform'), + line: 1, + column: 5, + }, + { + code: 'a { color: pink; background-size: cover; }', + message: messages.rejected('background-size'), + line: 1, + column: 18, + }, + { + code: 'a { color: pink; -webkit-transform: scale(1); }', + message: messages.rejected('-webkit-transform'), + line: 1, + column: 18, + }, + ], +}); + +testRule({ + ruleName, + + config: [['/^background/']], + + accept: [ + { + code: 'a { color: pink; }', + }, + { + code: 'a { no-background: sure; }', + }, + { + code: 'a { $scss: 0; }', + }, + { + code: 'a { @less: 0; }', + }, + { + code: 'a { --custom-property: 0; }', + }, + ], + + reject: [ + { + code: 'a { background: pink; }', + message: messages.rejected('background'), + line: 1, + column: 5, + }, + { + code: 'a { background-size: cover; }', + message: messages.rejected('background-size'), + line: 1, + column: 5, + }, + { + code: 'a { background-image: none; }', + message: messages.rejected('background-image'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: [[/^background/]], + + accept: [ + { + code: 'a { color: pink; }', + }, + ], + + reject: [ + { + code: 'a { background-size: cover; }', + message: messages.rejected('background-size'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: ['/margin/'], + + accept: [ + { + code: 'a { $margin: 0; }', + }, + { + code: 'a { @margin: 0; }', + }, + { + code: 'a { --margin: 0; }', + }, + ], +}); diff --git a/lib/rules/property-blacklist/index.js b/lib/rules/property-blacklist/index.js new file mode 100644 index 0000000000..09c211608b --- /dev/null +++ b/lib/rules/property-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../property-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'property-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (property) => `Unexpected property "${property}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/property-whitelist/README.md b/lib/rules/property-whitelist/README.md new file mode 100644 index 0000000000..6e42186604 --- /dev/null +++ b/lib/rules/property-whitelist/README.md @@ -0,0 +1,77 @@ +# property-whitelist + +**_Deprecated: Instead use the [`property-allowlist`](../property-allowlist/README.md) rule._** + +Specify a list of allowed properties. + + +```css +a { display: block; } +/** ↑ + * This property */ +``` + +This rule ignores variables (`$sass`, `@less`, `--custom-property`). + +## Options + +`array|string`: `["array", "of", "unprefixed", /properties/ or "regex"]|"property"|"/regex/"`|/regex/ + +If a string is surrounded with `"/"` (e.g. `"/^background/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^background/` will match `background`, `background-size`, `background-color`, etc. + +Given: + +``` +["display", "animation", "/^background/"] +``` + +The following patterns are considered violations: + + +```css +a { color: pink; } +``` + + +```css +a { + animation: my-animation 2s; + color: pink; +} +``` + + +```css +a { borkgrund: orange; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { display: block; } +``` + + +```css +a { -webkit-animation: my-animation 2s; } +``` + + +```css +a { + animation: my-animation 2s; + -webkit-animation: my-animation 2s; + display: block; +} +``` + + +```css +a { background: pink; } +``` + + +```css +a { background-color: pink; } +``` diff --git a/lib/rules/property-whitelist/__tests__/index.js b/lib/rules/property-whitelist/__tests__/index.js new file mode 100644 index 0000000000..f8fc0d8bd8 --- /dev/null +++ b/lib/rules/property-whitelist/__tests__/index.js @@ -0,0 +1,191 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: [], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'property-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: [''], + + reject: [ + { + code: 'a { color: pink; }', + message: messages.rejected('color'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: [[]], + + reject: [ + { + code: 'a { color: pink; }', + message: messages.rejected('color'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: ['transform', 'background-size'], + + accept: [ + { + code: 'a { background-size: cover; }', + }, + { + code: 'a { transform: scale(1); }', + }, + { + code: 'a { -webkit-transform: scale(1); }', + }, + { + code: 'a { transform: scale(1); background-size: cover; }', + }, + { + code: 'a { transform: scale(1); -webkit-transform: scale(1); background-size: cover; }', + }, + { + code: 'a { $scss: 0; }', + }, + { + code: 'a { @less: 0; }', + }, + { + code: 'a { --custom-property: 0; }', + }, + ], + + reject: [ + { + code: 'a { background: pink; }', + message: messages.rejected('background'), + line: 1, + column: 5, + }, + { + code: 'a { color: pink; }', + message: messages.rejected('color'), + line: 1, + column: 5, + }, + { + code: 'a { overflow: hidden; background-size: cover; }', + message: messages.rejected('overflow'), + line: 1, + column: 5, + }, + { + code: 'a { color: orange; -webkit-transform: scale(1); }', + message: messages.rejected('color'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: [['/^background/']], + + accept: [ + { + code: 'a { background: pink; }', + }, + { + code: 'a { background-color: pink; }', + }, + { + code: 'a { background-image: none; }', + }, + { + code: 'a { $scss: 0; }', + }, + { + code: 'a { @less: 0; }', + }, + { + code: 'a { --custom-property: 0; }', + }, + ], + + reject: [ + { + code: 'a { color: pink; }', + message: messages.rejected('color'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: [[/^background/]], + + accept: [ + { + code: 'a { background-image: none; }', + }, + ], + + reject: [ + { + code: 'a { color: pink; }', + message: messages.rejected('color'), + line: 1, + column: 5, + }, + ], +}); + +testRule({ + ruleName, + + config: ['/margin/'], + + accept: [ + { + code: 'a { $padding: 0; }', + }, + { + code: 'a { @padding: 0; }', + }, + { + code: 'a { --padding: 0; }', + }, + ], +}); diff --git a/lib/rules/property-whitelist/index.js b/lib/rules/property-whitelist/index.js new file mode 100644 index 0000000000..f071b5e1ac --- /dev/null +++ b/lib/rules/property-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../property-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'property-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (property) => `Unexpected property "${property}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-attribute-operator-blacklist/README.md b/lib/rules/selector-attribute-operator-blacklist/README.md new file mode 100644 index 0000000000..1bfad4330e --- /dev/null +++ b/lib/rules/selector-attribute-operator-blacklist/README.md @@ -0,0 +1,46 @@ +# selector-attribute-operator-blacklist + +**_Deprecated: Instead use the [`selector-attribute-operator-denylist`](../selector-attribute-operator-denylist/README.md) rule._** + +Specify a list of disallowed attribute operators. + + +```css +[target="_blank"] {} +/** ↑ + * This operator */ +``` + +## Options + +`array|string`: `["array", "of", "operators"]|"operator"` + +Given: + +``` +["*="] +``` + +The following patterns are considered violations: + + +```css +[class*="test"] {} +``` + +The following patterns are _not_ considered violations: + + +```css +[target] {} +``` + + +```css +[target="_blank"] {} +``` + + +```css +[class|="top"] {} +``` diff --git a/lib/rules/selector-attribute-operator-blacklist/__tests__/index.js b/lib/rules/selector-attribute-operator-blacklist/__tests__/index.js new file mode 100644 index 0000000000..20f8339530 --- /dev/null +++ b/lib/rules/selector-attribute-operator-blacklist/__tests__/index.js @@ -0,0 +1,114 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['~='], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-attribute-operator-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['*=', '~='], + + accept: [ + { + code: 'a[target] { }', + }, + { + code: 'a[target="_blank"] { }', + }, + { + code: '[class|="top"] { }', + }, + { + code: '[class^=top] { }', + }, + { + code: '[class$="test"] { }', + }, + { + code: ':root { --foo: 1px; }', + description: 'custom property in root', + }, + { + code: 'html { --foo: 1px; }', + description: 'custom property in selector', + }, + { + code: ':root { --custom-property-set: {} }', + description: 'custom property set in root', + }, + { + code: 'html { --custom-property-set: {} }', + description: 'custom property set in selector', + }, + ], + + reject: [ + { + code: '[title~="flower"] { }', + message: messages.rejected('~='), + line: 1, + column: 7, + }, + { + code: '[ title~="flower" ] { }', + message: messages.rejected('~='), + line: 1, + column: 8, + }, + { + code: '[title ~= "flower"] { }', + message: messages.rejected('~='), + line: 1, + column: 8, + }, + { + code: '[class*=te] { }', + message: messages.rejected('*='), + line: 1, + column: 7, + }, + ], +}); + +testRule({ + ruleName, + + config: ['*='], + + accept: [ + { + code: 'a[target="_blank"] { }', + }, + ], + + reject: [ + { + code: '[title*="foo"] { }', + message: messages.rejected('*='), + line: 1, + column: 7, + }, + ], +}); diff --git a/lib/rules/selector-attribute-operator-blacklist/index.js b/lib/rules/selector-attribute-operator-blacklist/index.js new file mode 100644 index 0000000000..964b732014 --- /dev/null +++ b/lib/rules/selector-attribute-operator-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-attribute-operator-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-attribute-operator-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (operator) => `Unexpected operator "${operator}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-attribute-operator-whitelist/README.md b/lib/rules/selector-attribute-operator-whitelist/README.md new file mode 100644 index 0000000000..c9b37ec1f0 --- /dev/null +++ b/lib/rules/selector-attribute-operator-whitelist/README.md @@ -0,0 +1,56 @@ +# selector-attribute-operator-whitelist + +**_Deprecated: Instead use the [`selector-attribute-operator-allowlist`](../selector-attribute-operator-allowlist/README.md) rule._** + +Specify a list of allowed attribute operators. + + +```css +[target="_blank"] {} +/** ↑ + * This operator */ +``` + +## Options + +`array|string`: `["array", "of", "operators"]|"operator"` + +Given: + +``` +["=", "|="] +``` + +The following patterns are considered violations: + + +```css +[class*="test"] {} +``` + + +```css +[title~="flower"] {} +``` + + +```css +[class^="top"] {} +``` + +The following patterns are _not_ considered violations: + + +```css +[target] {} +``` + + +```css +[target="_blank"] {} +``` + + +```css +[class|="top"] {} +``` diff --git a/lib/rules/selector-attribute-operator-whitelist/__tests__/index.js b/lib/rules/selector-attribute-operator-whitelist/__tests__/index.js new file mode 100644 index 0000000000..0772c86567 --- /dev/null +++ b/lib/rules/selector-attribute-operator-whitelist/__tests__/index.js @@ -0,0 +1,120 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['='], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-attribute-operator-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['=', '|='], + + accept: [ + { + code: 'a[target] { }', + }, + { + code: 'a[target="_blank"] { }', + }, + { + code: '[class|="top"] { }', + }, + { + code: ':root { --foo: 1px; }', + description: 'custom property in root', + }, + { + code: 'html { --foo: 1px; }', + description: 'custom property in selector', + }, + { + code: ':root { --custom-property-set: {} }', + description: 'custom property set in root', + }, + { + code: 'html { --custom-property-set: {} }', + description: 'custom property set in selector', + }, + ], + + reject: [ + { + code: '[title~="flower"] { }', + message: messages.rejected('~='), + line: 1, + column: 7, + }, + { + code: '[ title~="flower" ] { }', + message: messages.rejected('~='), + line: 1, + column: 8, + }, + { + code: '[title ~= "flower"] { }', + message: messages.rejected('~='), + line: 1, + column: 8, + }, + { + code: '[class^=top] { }', + message: messages.rejected('^='), + line: 1, + column: 7, + }, + { + code: '[class$="test"] { }', + message: messages.rejected('$='), + line: 1, + column: 7, + }, + { + code: '[class*=te] { }', + message: messages.rejected('*='), + line: 1, + column: 7, + }, + ], +}); + +testRule({ + ruleName, + + config: ['='], + + accept: [ + { + code: 'a[target="_blank"] { }', + }, + ], + + reject: [ + { + code: '[title~="flower"] { }', + message: messages.rejected('~='), + line: 1, + column: 7, + }, + ], +}); diff --git a/lib/rules/selector-attribute-operator-whitelist/index.js b/lib/rules/selector-attribute-operator-whitelist/index.js new file mode 100644 index 0000000000..94ac282ddc --- /dev/null +++ b/lib/rules/selector-attribute-operator-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-attribute-operator-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-attribute-operator-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (operator) => `Unexpected operator "${operator}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-combinator-blacklist/README.md b/lib/rules/selector-combinator-blacklist/README.md new file mode 100644 index 0000000000..be9fd62fdb --- /dev/null +++ b/lib/rules/selector-combinator-blacklist/README.md @@ -0,0 +1,56 @@ +# selector-combinator-blacklist + +**_Deprecated: Instead use the [`selector-combinator-denylist`](../selector-combinator-denylist/README.md) rule._** + +Specify a list of disallowed combinators. + + +```css + a + b {} +/** ↑ + * This combinator */ +``` + +This rule normalizes the whitespace descendant combinator to be a single space. + +This rule ignores [reference combinators](https://www.w3.org/TR/selectors4/#idref-combinators) e.g. `/for/`. + +## Options + +`array|string`: `["array", "of", "combinators"]|"combinator"` + +Given: + +``` +[">", " "] +``` + +The following patterns are considered violations: + + +```css +a > b {} +``` + + +```css +a b {} +``` + + +```css +a +b {} +``` + +The following patterns are _not_ considered violations: + + +```css +a + b {} +``` + + +```css +a ~ b {} +``` diff --git a/lib/rules/selector-combinator-blacklist/__tests__/index.js b/lib/rules/selector-combinator-blacklist/__tests__/index.js new file mode 100644 index 0000000000..c3dbcaee59 --- /dev/null +++ b/lib/rules/selector-combinator-blacklist/__tests__/index.js @@ -0,0 +1,89 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['>'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-combinator-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['>', ' '], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a, b {}', + }, + { + code: 'a /for/ b {}', + }, + { + code: 'a + b {}', + }, + { + code: 'a:not(b ~ c) {}', + }, + ], + + reject: [ + { + code: 'a b {}', + message: messages.rejected(' '), + line: 1, + column: 2, + }, + { + code: 'a\tb {}', + message: messages.rejected(' '), + line: 1, + column: 2, + }, + { + code: 'a\n\tb {}', + message: messages.rejected(' '), + line: 1, + column: 2, + }, + { + code: 'a,\nb c {}', + message: messages.rejected(' '), + line: 2, + column: 2, + }, + { + code: 'a:not(b > c) {}', + message: messages.rejected('>'), + line: 1, + column: 9, + }, + { + code: 'a > b {}', + message: messages.rejected('>'), + line: 1, + column: 3, + }, + ], +}); diff --git a/lib/rules/selector-combinator-blacklist/index.js b/lib/rules/selector-combinator-blacklist/index.js new file mode 100644 index 0000000000..ea190ee6dd --- /dev/null +++ b/lib/rules/selector-combinator-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-combinator-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-combinator-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (combinator) => `Unexpected combinator "${combinator}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-combinator-whitelist/README.md b/lib/rules/selector-combinator-whitelist/README.md new file mode 100644 index 0000000000..5674ec8213 --- /dev/null +++ b/lib/rules/selector-combinator-whitelist/README.md @@ -0,0 +1,56 @@ +# selector-combinator-whitelist + +**_Deprecated: Instead use the [`selector-combinator-allowlist`](../selector-combinator-allowlist/README.md) rule._** + +Specify a list of allowed combinators. + + +```css + a + b {} +/** ↑ + * This combinator */ +``` + +This rule normalizes the whitespace descendant combinator to be a single space. + +This rule ignores [reference combinators](https://www.w3.org/TR/selectors4/#idref-combinators) e.g. `/for/`. + +## Options + +`array|string`: `["array", "of", "combinators"]|"combinator"` + +Given: + +``` +[">", " "] +``` + +The following patterns are considered violations: + + +```css +a + b {} +``` + + +```css +a ~ b {} +``` + +The following patterns are _not_ considered violations: + + +```css +a > b {} +``` + + +```css +a b {} +``` + + +```css +a +b {} +``` diff --git a/lib/rules/selector-combinator-whitelist/__tests__/index.js b/lib/rules/selector-combinator-whitelist/__tests__/index.js new file mode 100644 index 0000000000..5e5002e4dc --- /dev/null +++ b/lib/rules/selector-combinator-whitelist/__tests__/index.js @@ -0,0 +1,113 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['>'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-combinator-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['>', ' '], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a, b {}', + }, + { + code: 'a /for/ b {}', + }, + { + code: 'a > b {}', + }, + { + code: 'a:not(b > c) {}', + }, + { + code: 'a b {}', + }, + { + code: 'a\tb {}', + }, + { + code: 'a\nb {}', + }, + ], + + reject: [ + { + code: 'a ~ b {}', + message: messages.rejected('~'), + line: 1, + column: 3, + }, + { + code: 'a:not(b ~ c) {}', + message: messages.rejected('~'), + line: 1, + column: 9, + }, + { + code: 'a,\nb + c {}', + message: messages.rejected('+'), + line: 2, + column: 3, + }, + ], +}); + +testRule({ + ruleName, + config: ['~'], + skipBasicChecks: true, + + accept: [ + { + code: 'a ~ b {}', + }, + ], + + reject: [ + { + code: 'a b {}', + message: messages.rejected(' '), + line: 1, + column: 2, + }, + { + code: 'a\tb {}', + message: messages.rejected(' '), + line: 1, + column: 2, + }, + { + code: 'a\n\tb {}', + message: messages.rejected(' '), + line: 1, + column: 2, + }, + ], +}); diff --git a/lib/rules/selector-combinator-whitelist/index.js b/lib/rules/selector-combinator-whitelist/index.js new file mode 100644 index 0000000000..b0ad0b838e --- /dev/null +++ b/lib/rules/selector-combinator-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-combinator-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-combinator-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (combinator) => `Unexpected combinator "${combinator}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-class-blacklist/README.md b/lib/rules/selector-pseudo-class-blacklist/README.md new file mode 100644 index 0000000000..ae39c50b0a --- /dev/null +++ b/lib/rules/selector-pseudo-class-blacklist/README.md @@ -0,0 +1,55 @@ +# selector-pseudo-class-blacklist + +**_Deprecated: Instead use the [`selector-pseudo-class-denylist`](../selector-pseudo-class-denylist/README.md) rule._** + +Specify a list of disallowed pseudo-class selectors. + + +```css + a:hover {} +/** ↑ + * This pseudo-class selector */ +``` + +This rule ignores selectors that use variable interpolation e.g. `:#{$variable} {}`. + +## Options + +`array|string|regex`: `["array", "of", "unprefixed", /pseudo-classes/ or "/regex/"]|"pseudo-class"|/regex/` + +If a string is surrounded with `"/"` (e.g. `"/^nth-/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^nth-/` will match `nth-child`, `nth-last-child`, `nth-of-type`, etc. + +Given: + +``` +["hover", "/^nth-/"] +``` + +The following patterns are considered violations: + + +```css +a:hover {} +``` + + +```css +a:nth-of-type(5) {} +``` + + +```css +a:nth-child(2) {} +``` + +The following patterns are _not_ considered violations: + + +```css +a:focus {} +``` + + +```css +a:first-of-type {} +``` diff --git a/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js b/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js new file mode 100644 index 0000000000..7c5ff11b4f --- /dev/null +++ b/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js @@ -0,0 +1,247 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['focus'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-class-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['focus', 'global', 'input-placeholder', 'not', 'nth-last-child', 'has'], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a:hover {}', + }, + { + code: 'a:nth-child(5) {}', + }, + { + code: 'div:nth-LAST-child {}', + }, + { + code: 'input:-Ms-INPUT-placeholder {}', + }, + { + code: ':root {}', + }, + { + code: 'a:HOVER {}', + }, + { + code: 'a:hover, a:nth-child(5) {}', + }, + { + code: 'a::before {}', + }, + { + code: 'a:nth-child(5)::before {}', + }, + { + code: 'a:-moz-placeholder {}', + }, + { + code: 'a:-MOZ-PLACEholder {}', + }, + { + code: ':root { --foo: 1px; }', + description: 'custom property in root', + }, + { + code: 'html { --foo: 1px; }', + description: 'custom property in selector', + }, + { + code: ':root { --custom-property-set: {} }', + description: 'custom property set in root', + }, + { + code: 'html { --custom-property-set: {} }', + description: 'custom property set in selector', + }, + ], + + reject: [ + { + code: 'a:focus {}', + message: messages.rejected('focus'), + line: 1, + column: 2, + }, + { + code: 'a,\n:global {}', + message: messages.rejected('global'), + line: 2, + column: 1, + }, + { + code: 'input:-ms-input-placeholder {}', + message: messages.rejected('-ms-input-placeholder'), + line: 1, + column: 6, + }, + { + code: 'a:not(::selection) {}', + message: messages.rejected('not'), + line: 1, + column: 2, + }, + { + code: 'a:has(> img) {}', + message: messages.rejected('has'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: [['/^last/']], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a:first-child() {}', + }, + { + code: 'a:nth-LAST-child(5) {}', + }, + ], + + reject: [ + { + code: 'a:last-child {}', + message: messages.rejected('last-child'), + line: 1, + column: 2, + }, + { + code: 'a:last-of-child {}', + message: messages.rejected('last-of-child'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: [[/^last/]], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + ], + + reject: [ + { + code: 'a:last-child {}', + message: messages.rejected('last-child'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: [[/(not|matches|has)/]], + skipBasicChecks: true, + + accept: [ + { + code: 'a:focus {}', + }, + ], + + reject: [ + { + code: 'a:not() {}', + message: messages.rejected('not'), + line: 1, + column: 2, + }, + { + code: 'body:not(div):has(span) {}', + warnings: [ + { + message: messages.rejected('not'), + line: 1, + column: 5, + }, + { + message: messages.rejected('has'), + line: 1, + column: 14, + }, + ], + }, + { + code: 'body:nt(div):not(span) {}', + message: messages.rejected('not'), + line: 1, + column: 13, + }, + { + code: 'a:has() {}', + message: messages.rejected('has'), + line: 1, + column: 2, + }, + { + code: 'a:matches() {}', + message: messages.rejected('matches'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: ['variable'], + skipBasicChecks: true, + syntax: 'scss', + + accept: [ + { + code: ':#{$variable} {}', + }, + { + code: ':#{$VARIABLE} {}', + }, + { + code: 'a:#{$variable} {}', + }, + ], +}); diff --git a/lib/rules/selector-pseudo-class-blacklist/index.js b/lib/rules/selector-pseudo-class-blacklist/index.js new file mode 100644 index 0000000000..99df54333a --- /dev/null +++ b/lib/rules/selector-pseudo-class-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-pseudo-class-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-pseudo-class-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-class "${selector}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-class-whitelist/README.md b/lib/rules/selector-pseudo-class-whitelist/README.md new file mode 100644 index 0000000000..e4b2d93991 --- /dev/null +++ b/lib/rules/selector-pseudo-class-whitelist/README.md @@ -0,0 +1,55 @@ +# selector-pseudo-class-whitelist + +**_Deprecated: Instead use the [`selector-pseudo-class-allowlist`](../selector-pseudo-class-allowlist/README.md) rule._** + +Specify a list of allowed pseudo-class selectors. + + +```css + a:hover {} +/** ↑ + * This pseudo-class selector */ +``` + +This rule ignores selectors that use variable interpolation e.g. `:#{$variable} {}`. + +## Options + +`array|string|regex`: `["array", "of", "unprefixed", /pseudo-classes/ or "/regex/"]|"pseudo-class"|/regex/` + +If a string is surrounded with `"/"` (e.g. `"/^nth-/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^nth-/` will match `nth-child`, `nth-last-child`, `nth-of-type`, etc. + +Given: + +``` +["hover", "/^nth-/"] +``` + +The following patterns are considered violations: + + +```css +a:focus {} +``` + + +```css +a:first-of-type {} +``` + +The following patterns are _not_ considered violations: + + +```css +a:hover {} +``` + + +```css +a:nth-of-type(5) {} +``` + + +```css +a:nth-child(2) {} +``` diff --git a/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js b/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js new file mode 100644 index 0000000000..0c4cc779d5 --- /dev/null +++ b/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js @@ -0,0 +1,196 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['hover'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-class-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['hover', 'nth-child', 'root', 'placeholder', 'has'], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a:hover {}', + }, + { + code: 'a:nth-child(5) {}', + }, + { + code: ':root {}', + }, + { + code: 'a:has(#id) {}', + }, + { + code: 'a:hover, a:nth-child(5) {}', + }, + { + code: 'a::before {}', + }, + { + code: 'a:nth-child(5)::before {}', + }, + { + code: 'a:-moz-placeholder {}', + }, + { + code: ':root { --foo: 1px; }', + description: 'custom property in root', + }, + { + code: 'html { --foo: 1px; }', + description: 'custom property in selector', + }, + { + code: ':root { --custom-property-set: {} }', + description: 'custom property set in root', + }, + { + code: 'html { --custom-property-set: {} }', + description: 'custom property set in selector', + }, + ], + + reject: [ + { + code: 'a:HOVER {}', + message: messages.rejected('HOVER'), + line: 1, + column: 2, + }, + { + code: 'a:-MOZ-PLACEholder {}', + message: messages.rejected('-MOZ-PLACEholder'), + line: 1, + column: 2, + }, + { + code: 'a:focus {}', + message: messages.rejected('focus'), + line: 1, + column: 2, + }, + { + code: 'div:nth-LAST-child {}', + message: messages.rejected('nth-LAST-child'), + line: 1, + column: 4, + }, + { + code: 'a,\n:global {}', + message: messages.rejected('global'), + line: 2, + column: 1, + }, + { + code: 'input:-ms-input-placeholder {}', + message: messages.rejected('-ms-input-placeholder'), + line: 1, + column: 6, + }, + { + code: 'input:-Ms-INPUT-placeholder {}', + message: messages.rejected('-Ms-INPUT-placeholder'), + line: 1, + column: 6, + }, + { + code: 'a:not(::selection) {}', + message: messages.rejected('not'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: [['/^nth/']], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a:nth-child(5) {}', + }, + { + code: 'a:nth-LAST-child {}', + }, + ], + + reject: [ + { + code: 'a:hover {}', + message: messages.rejected('hover'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: [[/^nth/]], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + ], + + reject: [ + { + code: 'a:hover {}', + message: messages.rejected('hover'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: ['hover'], + skipBasicChecks: true, + syntax: 'scss', + + accept: [ + { + code: ':#{$variable} {}', + }, + { + code: ':#{$VARIABLE} {}', + }, + { + code: 'a:#{$variable} {}', + }, + ], +}); diff --git a/lib/rules/selector-pseudo-class-whitelist/index.js b/lib/rules/selector-pseudo-class-whitelist/index.js new file mode 100644 index 0000000000..5a05678478 --- /dev/null +++ b/lib/rules/selector-pseudo-class-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-pseudo-class-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-pseudo-class-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-class "${selector}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-element-blacklist/README.md b/lib/rules/selector-pseudo-element-blacklist/README.md new file mode 100644 index 0000000000..56a7dc8683 --- /dev/null +++ b/lib/rules/selector-pseudo-element-blacklist/README.md @@ -0,0 +1,56 @@ +# selector-pseudo-element-blacklist + +**_Deprecated: Instead use the [`selector-pseudo-element-denylist`](../selector-pseudo-element-denylist/README.md) rule._** + +Specify a list of disallowed pseudo-element selectors. + + +```css + a::before {} +/** ↑ + * This pseudo-element selector */ +``` + +This rule ignores: + +- CSS2 pseudo-elements i.e. those prefixed with a single colon +- selectors that use variable interpolation e.g. `::#{$variable} {}` + +## Options + +`array|string|regex`: `["array", "of", "unprefixed", "pseudo-elements" or "regex"]|"pseudo-element"|/regex/` + +Given: + +``` +["before", "/^my-/i"] +``` + +The following patterns are considered violations: + + +```css +a::before {} +``` + + +```css +a::my-pseudo-element {} +``` + + +```css +a::MY-OTHER-pseudo-element {} +``` + +The following patterns are _not_ considered violations: + + +```css +a::after {} +``` + + +```css +a::not-my-pseudo-element {} +``` diff --git a/lib/rules/selector-pseudo-element-blacklist/__tests__/index.js b/lib/rules/selector-pseudo-element-blacklist/__tests__/index.js new file mode 100644 index 0000000000..cc856e2b3e --- /dev/null +++ b/lib/rules/selector-pseudo-element-blacklist/__tests__/index.js @@ -0,0 +1,126 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['before'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-element-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['before', 'selection', /^my/i], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a:hover {}', + }, + { + code: 'a::BEFORE {}', + }, + { + code: 'a::after {}', + }, + { + code: '::first-line {}', + }, + { + code: '::-webkit-first-line {}', + }, + { + code: 'a:not(::first-line) {}', + }, + { + code: 'a::their-pseudo-element {}', + }, + { + code: 'a::THEIR-other-pseudo-element {}', + }, + ], + + reject: [ + { + code: 'a::before {}', + message: messages.rejected('before'), + line: 1, + column: 2, + }, + { + code: 'a,\nb::before {}', + message: messages.rejected('before'), + line: 2, + column: 2, + }, + { + code: '::selection {}', + message: messages.rejected('selection'), + line: 1, + column: 1, + }, + { + code: '::-webkit-selection {}', + message: messages.rejected('-webkit-selection'), + line: 1, + column: 1, + }, + { + code: 'a:not(::selection) {}', + message: messages.rejected('selection'), + line: 1, + column: 7, + }, + { + code: 'a::my-pseudo-element {}', + message: messages.rejected('my-pseudo-element'), + line: 1, + column: 2, + }, + { + code: 'a::MY-OTHER-pseudo-element {}', + message: messages.rejected('MY-OTHER-pseudo-element'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: ['before'], + skipBasicChecks: true, + syntax: 'scss', + + accept: [ + { + code: '::#{$variable} {}', + }, + { + code: '::#{$VARIABLE} {}', + }, + { + code: 'a::#{$variable} {}', + }, + ], +}); diff --git a/lib/rules/selector-pseudo-element-blacklist/index.js b/lib/rules/selector-pseudo-element-blacklist/index.js new file mode 100644 index 0000000000..d79daa8061 --- /dev/null +++ b/lib/rules/selector-pseudo-element-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-pseudo-element-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-pseudo-element-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-element "${selector}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-element-whitelist/README.md b/lib/rules/selector-pseudo-element-whitelist/README.md new file mode 100644 index 0000000000..09342e35fb --- /dev/null +++ b/lib/rules/selector-pseudo-element-whitelist/README.md @@ -0,0 +1,56 @@ +# selector-pseudo-element-whitelist + +**_Deprecated: Instead use the [`selector-pseudo-element-allowlist`](../selector-pseudo-element-allowlist/README.md) rule._** + +Specify a list of allowed pseudo-element selectors. + + +```css + a::before {} +/** ↑ + * This pseudo-element selector */ +``` + +This rule ignores: + +- CSS2 pseudo-elements i.e. those prefixed with a single colon +- selectors that use variable interpolation e.g. `::#{$variable} {}` + +## Options + +`array|string|regex`: `["array", "of", "unprefixed", "pseudo-elements" or "regex"]|"pseudo-element"|/regex/` + +Given: + +``` +["before", "/^my-/i"] +``` + +The following patterns are considered violations: + + +```css +a::after {} +``` + + +```css +a::not-my-pseudo-element {} +``` + +The following patterns are _not_ considered violations: + + +```css +a::before {} +``` + + +```css +a::my-pseudo-element {} +``` + + +```css +a::MY-OTHER-pseudo-element {} +``` diff --git a/lib/rules/selector-pseudo-element-whitelist/__tests__/index.js b/lib/rules/selector-pseudo-element-whitelist/__tests__/index.js new file mode 100644 index 0000000000..6bc6135634 --- /dev/null +++ b/lib/rules/selector-pseudo-element-whitelist/__tests__/index.js @@ -0,0 +1,148 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['before'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-element-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + config: ['before', 'selection', /^my/i], + skipBasicChecks: true, + + accept: [ + { + code: 'a {}', + }, + { + code: 'a:hover {}', + }, + { + code: 'a::before {}', + }, + { + code: '::selection {}', + }, + { + code: '::-webkit-selection {}', + }, + { + code: 'a:not(::selection) {}', + }, + { + code: 'a::my-pseudo-element {}', + }, + { + code: 'a::MY-other-pseudo-element {}', + }, + ], + + reject: [ + { + code: 'a::BEFORE {}', + message: messages.rejected('BEFORE'), + line: 1, + column: 2, + }, + { + code: 'a::after {}', + message: messages.rejected('after'), + line: 1, + column: 2, + }, + { + code: 'a::AFTER {}', + message: messages.rejected('AFTER'), + line: 1, + column: 2, + }, + { + code: 'a,\nb::after {}', + message: messages.rejected('after'), + line: 2, + column: 2, + }, + { + code: 'a::not-my-pseudo-element {}', + message: messages.rejected('not-my-pseudo-element'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: /^before/, + skipBasicChecks: true, + + accept: [ + { + code: '::before {}', + }, + { + code: '::before-custom {}', + }, + ], + reject: [ + { + code: 'a::after {}', + message: messages.rejected('after'), + line: 1, + column: 2, + }, + { + code: 'a::not-before {}', + message: messages.rejected('not-before'), + line: 1, + column: 2, + }, + ], +}); + +testRule({ + ruleName, + config: ['before'], + skipBasicChecks: true, + syntax: 'scss', + + accept: [ + { + code: '::#{$variable} {}', + }, + { + code: '::#{$VARIABLE} {}', + }, + { + code: 'a::#{$variable} {}', + }, + ], + reject: [ + { + code: 'a::after {}', + message: messages.rejected('after'), + line: 1, + column: 2, + }, + ], +}); diff --git a/lib/rules/selector-pseudo-element-whitelist/index.js b/lib/rules/selector-pseudo-element-whitelist/index.js new file mode 100644 index 0000000000..ca0dd2dcd2 --- /dev/null +++ b/lib/rules/selector-pseudo-element-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../selector-pseudo-element-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'selector-pseudo-element-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-element "${selector}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/unit-blacklist/README.md b/lib/rules/unit-blacklist/README.md new file mode 100644 index 0000000000..bb2813b25c --- /dev/null +++ b/lib/rules/unit-blacklist/README.md @@ -0,0 +1,161 @@ +# unit-blacklist + +**_Deprecated: Instead use the [`unit-denylist`](../unit-denylist/README.md) rule._** + +Specify a list of disallowed units. + + +```css +a { width: 100px; } +/** ↑ + * These units */ +``` + +## Options + +`array|string`: `["array", "of", "units"]|"unit"` + +Given: + +``` +["px", "em", "deg"] +``` + +The following patterns are considered violations: + + +```css +a { width: 100px; } +``` + + +```css +a { font-size: 10em; } +``` + + +```css +a { transform: rotate(30deg); } +``` + +The following patterns are _not_ considered violations: + + +```css +a { font-size: 1.2rem; } +``` + + +```css +a { line-height: 1.2; } +``` + + +```css +a { height: 100vmin; } +``` + + +```css +a { animation: animation-name 5s ease; } +``` + +## Optional secondary options + +### `ignoreProperties: { unit: ["property", "/regex/", /regex/] }` + +Ignore units in the values of declarations with the specified properties. + +For example, with `["px", "vmin"]`. + +Given: + +``` +{ + "px": [ "font-size", "/^border/" ], + "vmin": [ "width" ] +} +``` + +The following patterns are _not_ considered violations: + + +```css +a { font-size: 13px; } +``` + + +```css +a { border-bottom-width: 6px; } +``` + + +```css +a { width: 100vmin; } +``` + +The following patterns are considered violations: + + +```css +a { line-height: 12px; } +``` + + +```css +a { -moz-border-radius-topright: 40px; } +``` + + +```css +a { height: 100vmin; } +``` + +### `ignoreMediaFeatureNames: { unit: ["property", "/regex/", /regex/] }` + +Ignore units for specific feature names. + +For example, with `["px", "dpi"]`. + +Given: + +``` +{ + "px": [ "min-width", "/height$/" ], + "dpi": [ "resolution" ] +} +``` + +The following patterns are _not_ considered violations: + + +```css +@media (min-width: 960px) {} +``` + + +```css +@media (max-height: 280px) {} +``` + + +```css +@media not (resolution: 300dpi) {} +``` + +The following patterns are considered violations: + + +```css +@media screen and (max-device-width: 500px) {} +``` + + +```css +@media all and (min-width: 500px) and (max-width: 200px) {} +``` + + +```css +@media print and (max-resolution: 100dpi) {} +``` diff --git a/lib/rules/unit-blacklist/__tests__/index.js b/lib/rules/unit-blacklist/__tests__/index.js new file mode 100644 index 0000000000..9bf89fc399 --- /dev/null +++ b/lib/rules/unit-blacklist/__tests__/index.js @@ -0,0 +1,563 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['px'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'unit-denylist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['px', 'vmin'], + + accept: [ + { + code: 'a { line-height: 1; }', + }, + { + code: 'a { color: #000; }', + }, + { + code: 'a { top: 0; left: 0; }', + }, + { + code: 'a { font-size: 100%; }', + }, + { + code: 'a { line-height: 1.2rem; }', + }, + { + code: 'a { line-height: 1.2rEm; }', + }, + { + code: 'a { line-height: 1.2REM; }', + }, + { + code: 'a { font-size: .5rem; }', + }, + { + code: 'a { font-size: 0.5rem; }', + }, + { + code: 'a { margin: 0 10em 5rem 2in; }', + }, + { + code: 'a { background-position: top right, 1em 5vh; }', + }, + { + code: 'a { top: calc(10em - 3em); }', + }, + { + code: 'a { top: calc(10em*2rem); }', + }, + { + code: 'a { background-image: linear-gradient(to right, white calc(100% - 50em), silver); }', + }, + { + code: 'a { width: /* 100px */ 1em; }', + description: 'ignore unit within comments', + }, + { + code: 'a::before { content: "10px"}', + description: 'ignore unit within quotes', + }, + { + code: 'a { font-size: $fs10px; }', + description: 'ignore preprocessor variable includes unit', + }, + { + code: 'a { font-size: --some-fs-10px; }', + description: 'ignore css variable includes unit', + }, + { + code: 'a { background-url: url(10vmin); }', + description: 'ignore url function', + }, + { + code: 'a { background-url: uRl(10vmin); }', + description: 'ignore url function', + }, + { + code: 'a { background-url: URL(10vmin); }', + description: 'ignore url function', + }, + { + code: 'a { margin10px: 10em; }', + description: 'ignore property include wrong unit', + }, + { + code: 'a10px { margin: 10em; }', + description: 'ignore type selector include wrong unit', + }, + { + code: '#a10px { margin: 10em; }', + description: 'ignore class selector include wrong unit', + }, + { + code: '.a10px { margin: 10em; }', + description: 'ignore class selector include wrong unit', + }, + { + code: 'input[type=10px] { margin: 10em; }', + description: 'ignore class selector include wrong unit', + }, + { + code: 'a:hover10px { margin: 10em; }', + description: 'ignore pseudo-class include wrong unit', + }, + { + code: 'a::before10px { margin: 10em; }', + description: 'ignore pseudo-class include wrong unit', + }, + { + code: 'a { margin: calc(100% - #{margin * 2}); }', + description: 'work with interpolation', + }, + { + code: '@media (min-width: 10em) {}', + description: '@media', + }, + { + code: '@media (min-width: 10em)\n and (max-width: 20em) {}', + description: 'complex @media', + }, + ], + + reject: [ + { + code: 'a { font-size: 13px; }', + message: messages.rejected('px'), + line: 1, + column: 16, + }, + { + code: 'a { font-size: 13pX; }', + message: messages.rejected('pX'), + line: 1, + column: 16, + }, + { + code: 'a { font-size: 13PX; }', + message: messages.rejected('PX'), + line: 1, + column: 16, + }, + { + code: 'a { width: 100vmin; }', + message: messages.rejected('vmin'), + line: 1, + column: 12, + }, + { + code: 'a { line-height: .1px; }', + message: messages.rejected('px'), + line: 1, + column: 18, + }, + { + code: 'a { line-height: 0.1px; }', + message: messages.rejected('px'), + line: 1, + column: 18, + }, + { + code: 'a { border-left: 1px solid #ccc; }', + message: messages.rejected('px'), + line: 1, + column: 18, + }, + { + code: 'a { margin: 0 20px; }', + message: messages.rejected('px'), + line: 1, + column: 15, + }, + { + code: 'a { margin: 0 0 0 20px; }', + message: messages.rejected('px'), + line: 1, + column: 19, + }, + { + code: 'a { background-position: top right, 1em 5px; }', + message: messages.rejected('px'), + line: 1, + column: 41, + }, + { + code: 'a { top: calc(100px - 30vh); }', + message: messages.rejected('px'), + line: 1, + column: 15, + }, + { + code: 'a { top: calc(100px*2); }', + message: messages.rejected('px'), + line: 1, + column: 15, + }, + { + code: 'a { background-image: linear-gradient(to right, white calc(100vh - 5vmin), silver); }', + message: messages.rejected('vmin'), + line: 1, + column: 68, + }, + { + code: 'a { margin: calc(100% - #{$margin * 2px}); }', + message: messages.rejected('px'), + line: 1, + column: 37, + }, + { + code: '@media (min-width: 13px) {}', + message: messages.rejected('px'), + description: '@media', + line: 1, + column: 20, + }, + { + code: '@media (min-width: 10em)\n and (max-width: 20px) {}', + message: messages.rejected('px'), + description: 'complex @media', + line: 2, + column: 19, + }, + { + code: '@media (width < 10.01px) {}', + message: messages.rejected('px'), + description: 'media feature range', + line: 1, + column: 17, + }, + ], +}); + +testRule({ + ruleName, + + config: ['px'], + + accept: [ + { + code: 'a { line-height: 1em; }', + }, + ], + + reject: [ + { + code: 'a { line-height: 1px; }', + message: messages.rejected('px'), + line: 1, + column: 18, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + ['px', 'vmin'], + { + ignoreProperties: { + px: ['font-size', 'margin', '/^border/'], + vmin: ['width', 'height'], + }, + }, + ], + + accept: [ + { + code: 'a { font-size: 13px; }', + }, + { + code: 'a { font-size: 13pX; }', + }, + { + code: 'a { margin: 0 20px; }', + }, + { + code: 'a { margin: 0 0 0 20Px; }', + }, + { + code: 'a { width: 100vmin; }', + }, + { + code: 'a { height: 99vmIn; }', + }, + { + code: 'a { border: 1px solid purple; }', + }, + { + code: 'a { border-bottom-width: 6px; }', + }, + ], + + reject: [ + { + code: 'a { line-height: .1px; }', + message: messages.rejected('px'), + line: 1, + column: 18, + }, + { + code: 'a { background-image: linear-gradient(to right, white calc(100vh - 5vmin), silver); }', + message: messages.rejected('vmin'), + line: 1, + column: 68, + }, + { + code: 'a { -moz-border-radius-topright: 40px; }', + message: messages.rejected('px'), + line: 1, + column: 34, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + ['px', 'vmin'], + { + ignoreProperties: { + px: ['font-size', 'margin', /^border/], + vmin: ['width', 'height'], + }, + }, + ], + + accept: [ + { + code: 'a { border: 1px solid purple; }', + }, + { + code: 'a { border-bottom-width: 6px; }', + }, + ], + + reject: [ + { + code: 'a { line-height: .1px; }', + message: messages.rejected('px'), + line: 1, + column: 18, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + ['px', 'dpi', '%'], + { + ignoreMediaFeatureNames: { + px: ['min-width', 'height'], + dpi: ['min-resolution', 'resolution'], + '%': ['width', '/^min/'], + }, + }, + ], + + accept: [ + { + code: '@media (min-width: 960px) { body { font-size: 13em } }', + }, + { + code: '@media (width: 960%) { /* body { font-size: 13em } */ }', + }, + { + code: '@media (min-width: 960%) { /* body { font-size: 13em } */ }', + }, + { + code: 'a { @media (min-width: 960px) { body { font-size: 13em } } }', + }, + { + code: '@media print and (min-resolution: 300dpi) { body { font-size: 13em } }', + }, + { + code: '@media print { body { font-size: 40pt } }', + }, + { + code: '@media screen, print { body { line-height: 1.2 } }', + }, + { + code: '@MEDIA (min-width: 960px) { body { font-size: 13em } }', + }, + { + code: '@media (MIN-WIDTH: 960px) { body { font-size: 13em } }', + }, + { + code: '@media (height > -100px) { body { background: green; } }', + }, + { + code: '@media not (resolution: -300dpi) { body { background: green; } }', + }, + { + code: '@media only screen and (min-width: 500px) { }', + }, + { + code: '@media only speech and (width > 20%) { }', + }, + { + code: '@media speech and (device-aspect-ratio: 16/9) { }', + }, + { + code: + '@media only screen and (min-width: 320px) and (height: 480px) and (-webkit-min-device-pixel-ratio: 2) { body { line-height: 1.4 } }', + }, + { + code: '@media screen, print { }', + }, + { + code: '@media speech and (aspect-ratio: 11/5) { }', + }, + { + code: '@media (min-width: 700px), handheld and (orientation: landscape) { }', + }, + ], + + reject: [ + { + code: '@media screen and (max-width: 500px) { }', + message: messages.rejected('px'), + line: 1, + column: 31, + }, + { + code: '@media (width: 960px) { /* body { font-size: 13em } */ }', + message: messages.rejected('px'), + line: 1, + column: 16, + }, + { + code: '@media (min-height: 960px) { /* body { font-size: 13em } */ }', + message: messages.rejected('px'), + line: 1, + column: 21, + }, + { + code: 'a { @media screen and (max-width: 500px) { } }', + message: messages.rejected('px'), + line: 1, + column: 35, + }, + { + code: '@media all and (min-width: 500px) and (max-width: 200px) { }', + message: messages.rejected('px'), + line: 1, + column: 51, + }, + { + code: '@MEDIA print { body { font-size: 60dpi } }', + message: messages.rejected('dpi'), + line: 1, + column: 34, + }, + { + code: '@media (MAX-WIDTH: 10px) { }', + message: messages.rejected('px'), + line: 1, + column: 20, + }, + { + code: '@media (min-width: 10em)\n and (max-width: 20px) { }', + message: messages.rejected('px'), + line: 2, + column: 19, + }, + { + code: '@media (width < 10.01px) {}', + message: messages.rejected('px'), + line: 1, + column: 17, + }, + { + code: '@media only speech and (max-device-width > 20%) { }', + message: messages.rejected('%'), + line: 1, + column: 44, + }, + { + code: '@media not (max-resolution: -300dpi) { body { background: green; } }', + message: messages.rejected('dpi'), + line: 1, + column: 29, + }, + { + code: + '@media only screen and (min-width: 320px) and (height: 480px) and (-webkit-min-device-pixel-ratio: 2) { body { line-height: 1.4px } }', + message: messages.rejected('px'), + line: 1, + column: 125, + }, + { + code: + '@media only screen and (min-width: 320px) and (height: 480px) and (-webkit-min-device-pixel-ratio: 2px) { body { line-height: 1.4 } }', + message: messages.rejected('px'), + line: 1, + column: 100, + }, + { + code: '@media screen and (min-width: 699px) and (min-width: 520px), (max-width: 1151px)', + message: messages.rejected('px'), + line: 1, + column: 74, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + ['px', 'dpi', '%'], + { + ignoreMediaFeatureNames: { + px: ['min-width', 'height'], + dpi: ['min-resolution', 'resolution'], + '%': ['width', /^min/], + }, + }, + ], + + accept: [ + { + code: '@media (width: 960%) { /* body { font-size: 13em } */ }', + }, + { + code: '@media (min-width: 960%) { /* body { font-size: 13em } */ }', + }, + ], + + reject: [ + { + code: '@media screen and (max-width: 500px) { }', + message: messages.rejected('px'), + line: 1, + column: 31, + }, + ], +}); diff --git a/lib/rules/unit-blacklist/index.js b/lib/rules/unit-blacklist/index.js new file mode 100644 index 0000000000..4b035f20e4 --- /dev/null +++ b/lib/rules/unit-blacklist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../unit-denylist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'unit-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (unit) => `Unexpected unit "${unit}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/unit-whitelist/README.md b/lib/rules/unit-whitelist/README.md new file mode 100644 index 0000000000..d74e2f1364 --- /dev/null +++ b/lib/rules/unit-whitelist/README.md @@ -0,0 +1,117 @@ +# unit-whitelist + +**_Deprecated: Instead use the [`unit-allowlist`](../unit-allowlist/README.md) rule._** + +Specify a list of allowed units. + + +```css +a { width: 100px; } +/** ↑ + * These units */ +``` + +## Options + +`array|string`: `["array", "of", "units"]|"unit"` + +Given: + +``` +["px", "em", "deg"] +``` + +The following patterns are considered violations: + + +```css +a { width: 100%; } +``` + + +```css +a { font-size: 10rem; } +``` + + +```css +a { animation: animation-name 5s ease; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { font-size: 1.2em; } +``` + + +```css +a { line-height: 1.2; } +``` + + +```css +a { height: 100px; } +``` + + +```css +a { height: 100PX; } +``` + + +```css +a { transform: rotate(30deg); } +``` + +## Optional secondary options + +### `ignoreProperties: { unit: ["property", "/regex/", /regex/] }` + +Ignore units in the values of declarations with the specified properties. + +For example, with `["px", "em"]`. + +Given: + +``` +{ + "rem": [ "line-height", "/^border/" ], + "%": [ "width" ] +} +``` + +The following patterns are _not_ considered violations: + + +```css +a { line-height: 0.1rem; } +``` + + +```css +a { border-bottom-width: 6rem; } +``` + + +```css +a { width: 100%; } +``` + +The following patterns are considered violations: + + +```css +a { margin: 0 20rem; } +``` + + +```css +a { -moz-border-radius-topright: 20rem; } +``` + + +```css +a { height: 100%; } +``` diff --git a/lib/rules/unit-whitelist/__tests__/index.js b/lib/rules/unit-whitelist/__tests__/index.js new file mode 100644 index 0000000000..c06d65a2e5 --- /dev/null +++ b/lib/rules/unit-whitelist/__tests__/index.js @@ -0,0 +1,365 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('includes a deprecation warning', () => { + const config = { + rules: { + [ruleName]: ['px'], + }, + }; + + const code = ''; + + return standalone({ code, config }).then((output) => { + const result = output.results[0]; + + expect(result.deprecations).toHaveLength(1); + expect(result.deprecations[0].text).toEqual( + `'${ruleName}' has been deprecated. Instead use 'unit-allowlist'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://stylelint.io/user-guide/rules/${ruleName}/`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['px', 'em'], + + accept: [ + { + code: 'a { line-height: 1; }', + }, + { + code: 'a { color: #000; }', + }, + { + code: 'a { font-size: 14px; }', + }, + { + code: 'a { font-size: 14pX; }', + }, + { + code: 'a { font-size: 14PX; }', + }, + { + code: 'a { font-size: 1.2em; }', + }, + { + code: 'a { font-size: .5em; }', + }, + { + code: 'a { font-size: 0.5em; }', + }, + { + code: 'a { margin: 0 10em 5em 2px; }', + }, + { + code: 'a { background-position: top right, 10px 20px; }', + }, + { + code: 'a { top: calc(10em - 3em); }', + }, + { + code: 'a { top: calc(10em*3); }', + }, + { + code: 'a { background-image: linear-gradient(to right, white calc(100px - 50em), silver); }', + }, + { + code: 'a { width: /* 100pc */ 1em; }', + description: 'ignore unit within comments', + }, + { + code: 'a::before { content: "10%"}', + description: 'ignore unit within quotes', + }, + { + code: 'a { font-size: $fs10%; }', + description: 'ignore preprocessor variable includes unit', + }, + { + code: 'a { font-size: --some-fs-10rem; }', + description: 'ignore css variable includes unit', + }, + { + code: 'a { background-url: url(10vmin); }', + description: 'ignore url function', + }, + { + code: 'a { background-url: uRl(10vmin); }', + description: 'ignore url function', + }, + { + code: 'a { background-url: URL(10vmin); }', + description: 'ignore url function', + }, + { + code: 'a { margin10rem: 10em; }', + description: 'ignore property include wrong unit', + }, + { + code: 'a10rem { margin: 10em; }', + description: 'ignore type selector include wrong unit', + }, + { + code: '#a10rem { margin: 10em; }', + description: 'ignore class selector include wrong unit', + }, + { + code: '.a10rem { margin: 10em; }', + description: 'ignore class selector include wrong unit', + }, + { + code: 'input[type=10rem] { margin: 10em; }', + description: 'ignore class selector include wrong unit', + }, + { + code: 'a:hover10rem { margin: 10em; }', + description: 'ignore pseudo-class include wrong unit', + }, + { + code: 'a::before10rem { margin: 10em; }', + description: 'ignore pseudo-class include wrong unit', + }, + { + code: 'a { margin: calc(100px - #{margin * 2}); }', + description: 'work with interpolation', + }, + { + code: '@media (min-width: 10em) {}', + description: '@media', + }, + { + code: '@media (min-width: 10px)\n and (max-width: 20em) {}', + description: 'complex @media', + }, + ], + + reject: [ + { + code: 'a { font-size: 80%; }', + message: messages.rejected('%'), + line: 1, + column: 16, + }, + { + code: 'a { width: 100vmin; }', + message: messages.rejected('vmin'), + line: 1, + column: 12, + }, + { + code: 'a { width: 100vMiN; }', + message: messages.rejected('vMiN'), + line: 1, + column: 12, + }, + { + code: 'a { width: 100VMIN; }', + message: messages.rejected('VMIN'), + line: 1, + column: 12, + }, + { + code: 'a { line-height: .1rem; }', + message: messages.rejected('rem'), + line: 1, + column: 18, + }, + { + code: 'a { line-height: 0.1rem; }', + message: messages.rejected('rem'), + line: 1, + column: 18, + }, + { + code: 'a { border-left: 1rem solid #ccc; }', + message: messages.rejected('rem'), + line: 1, + column: 18, + }, + { + code: 'a { margin: 0 20%; }', + message: messages.rejected('%'), + line: 1, + column: 15, + }, + { + code: 'a { margin: 0 0 0 20rem; }', + message: messages.rejected('rem'), + line: 1, + column: 19, + }, + { + code: 'a { background-position: top right, 1em 5rem; }', + message: messages.rejected('rem'), + line: 1, + column: 41, + }, + { + code: 'a { top: calc(2vh*3); }', + message: messages.rejected('vh'), + line: 1, + column: 15, + }, + { + code: 'a { top: calc(100px - 30vh); }', + message: messages.rejected('vh'), + line: 1, + column: 23, + }, + { + code: 'a { background-image: linear-gradient(to right, white calc(100px - 5vmin), silver); }', + message: messages.rejected('vmin'), + line: 1, + column: 68, + }, + { + code: 'a { margin: calc(100px - #{$margin * 2rem}); }', + message: messages.rejected('rem'), + line: 1, + column: 38, + }, + { + code: '@media (min-width: 13rem) {}', + message: messages.rejected('rem'), + description: '@media', + line: 1, + column: 20, + }, + { + code: '@media (min-width: 10em)\n and (max-width: 20rem) {}', + message: messages.rejected('rem'), + description: 'complex @media', + line: 2, + column: 19, + }, + { + code: '@media (width < 10.01REM) {}', + message: messages.rejected('REM'), + description: 'media feature range', + line: 1, + column: 17, + }, + ], +}); + +testRule({ + ruleName, + + config: ['px'], + + accept: [ + { + code: 'a { line-height: 1px; }', + }, + ], + + reject: [ + { + code: 'a { line-height: 1em; }', + message: messages.rejected('em'), + line: 1, + column: 18, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + ['px', 'em'], + { + ignoreProperties: { + rem: ['line-height', 'margin', '/^border/'], + '%': ['width', 'height'], + }, + }, + ], + + accept: [ + { + code: 'a { line-height: 0.1rem; }', + }, + { + code: 'a { line-height: 0.1rEm; }', + }, + { + code: 'a { margin: 0 20rem; }', + }, + { + code: 'a { margin: 0 0 0 20reM; }', + }, + { + code: 'a { width: 100%; }', + }, + { + code: 'a { height: 50%; }', + }, + { + code: 'a { border: 1rem solid purple; }', + }, + { + code: 'a { border-bottom-width: 6rem; }', + }, + ], + + reject: [ + { + code: 'a { font-size: 80%; }', + message: messages.rejected('%'), + line: 1, + column: 16, + }, + { + code: 'a { background-image: linear-gradient(to right, white calc(100px - 5rem), silver); }', + message: messages.rejected('rem'), + line: 1, + column: 68, + }, + { + code: 'a { -moz-border-radius-topright: 40rem; }', + message: messages.rejected('rem'), + line: 1, + column: 34, + }, + ], +}); + +testRule({ + ruleName, + + config: [ + ['px', 'em'], + { + ignoreProperties: { + rem: ['line-height', 'margin', /^border/], + '%': ['width', 'height'], + }, + }, + ], + + accept: [ + { + code: 'a { border: 1rem solid purple; }', + }, + { + code: 'a { border-bottom-width: 6rem; }', + }, + ], + + reject: [ + { + code: 'a { font-size: 80%; }', + message: messages.rejected('%'), + line: 1, + column: 16, + }, + ], +}); diff --git a/lib/rules/unit-whitelist/index.js b/lib/rules/unit-whitelist/index.js new file mode 100644 index 0000000000..e290e2c6d0 --- /dev/null +++ b/lib/rules/unit-whitelist/index.js @@ -0,0 +1,20 @@ +// @ts-nocheck + +'use strict'; + +const replacementRule = require('../unit-allowlist'); +const ruleMessages = require('../../utils/ruleMessages'); + +const ruleName = 'unit-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (unit) => `Unexpected unit "${unit}"`, +}); + +const rule = replacementRule.ruleFactory(ruleName, messages); + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule;