From 2b7e8ad8b43634c9d75a229ac29b3cb2e0d008ac Mon Sep 17 00:00:00 2001 From: Kevin Dew Date: Mon, 31 Aug 2020 12:09:06 +0100 Subject: [PATCH] Deprecate *-blacklist/*-requirelist/*-whitelist (#4892) * Make copies for *-blacklist/*-requirelist/*-whitelist rules As discussed in issue #4854, the desired approach is to deprecate the *-blacklist/*-requirelist/*-whitelist rules. The first step I've taken to doing this is to make copies of all the rules. The commands used to generate this commit were: - find . -name '*-allowed-list' -exec sh -c 'cp -R "$1" "${1%-allowed-list}-whitelist"' _ {} \; - find . -name '*-disallowed-list' -exec sh -c 'cp -R "$1" "${1%-disallowed-list}-blacklist"' _ {} \; - find . -name '*-required-list' -exec sh -c 'cp -R "$1" "${1%-required-list}-requirelist"' _ {} \; * Re-instate *-blacklist/*-requirelist/*-whitelist rules The aliasing approach taken for these rule renaming presented user experience problems [1]. To resolve these problems the preference is to revert on the aliasing strategy and instead return to a deprecation strategy. The first step towards this is re-instating these rules. As per the preference of jeddy3 [2] these rules are re-instated using a copy and paste strategy, with the expectation these will be removed in the next major release. An approach that didn't involve copy and pasting was previously introduced in [3] and could be looked at again if maintaining this duplicate code proves problematic. [1]: https://github.com/stylelint/stylelint/issues/4854#issuecomment-654332844 [2]: https://github.com/stylelint/stylelint/issues/4854#issuecomment-670935654 [3]: https://github.com/stylelint/stylelint/commit/e93e44c938d77021eec4893fc20327337a60d49f * Deprecate *-blacklist/*-requirelist/*-whitelist rules This applies deprecation warnings when these rules are used, tests to check there is a deprecation warning and re-includes them in documentation. * Deprecation links point to GitHub tag This reflects the documentation on deprecating stylelint rules [1] by linking to the GitHub website rather than stylelint website, so that the link can continue operating indefinitely. As suggested in the PR [2] these have been linked to the anticipated next release of stylelint, so these links will not be actually operational until this tag is made. [1]: https://github.com/stylelint/stylelint/blob/858dcd584224042654d80ce8fa8ad71f41f20808/docs/developer-guide/rules.md#deprecate-a-rule [2]: https://github.com/stylelint/stylelint/pull/4892#issuecomment-679375149 --- docs/user-guide/rules/list.md | 26 + lib/__tests__/aliasedRules.test.js | 41 -- lib/__tests__/integration.test.js | 13 - lib/rules/at-rule-allowed-list/README.md | 2 - lib/rules/at-rule-blacklist/README.md | 52 ++ .../at-rule-blacklist/__tests__/index.js | 197 ++++++ lib/rules/at-rule-blacklist/index.js | 62 ++ lib/rules/at-rule-disallowed-list/README.md | 2 - .../at-rule-property-required-list/README.md | 2 - .../at-rule-property-requirelist/README.md | 55 ++ .../__tests__/index.js | 84 +++ .../at-rule-property-requirelist/index.js | 73 +++ lib/rules/at-rule-whitelist/README.md | 67 +++ .../at-rule-whitelist/__tests__/index.js | 190 ++++++ lib/rules/at-rule-whitelist/index.js | 62 ++ lib/rules/comment-word-blacklist/README.md | 50 ++ .../comment-word-blacklist/__tests__/index.js | 347 +++++++++++ lib/rules/comment-word-blacklist/index.js | 64 ++ .../comment-word-disallowed-list/README.md | 2 - .../README.md | 2 - .../README.md | 76 +++ .../__tests__/index.js | 195 ++++++ .../index.js | 84 +++ .../README.md | 2 - .../README.md | 87 +++ .../__tests__/index.js | 200 +++++++ .../index.js | 84 +++ .../README.md | 2 - .../README.md | 107 ++++ .../__tests__/index.js | 210 +++++++ .../index.js | 66 ++ .../README.md | 2 - .../README.md | 98 +++ .../__tests__/index.js | 101 ++++ .../index.js | 66 ++ lib/rules/function-allowed-list/README.md | 2 - lib/rules/function-blacklist/README.md | 54 ++ .../function-blacklist/__tests__/index.js | 243 ++++++++ lib/rules/function-blacklist/index.js | 69 +++ lib/rules/function-disallowed-list/README.md | 2 - .../README.md | 2 - .../function-url-scheme-blacklist/README.md | 73 +++ .../__tests__/index.js | 280 +++++++++ .../function-url-scheme-blacklist/index.js | 74 +++ .../README.md | 2 - .../function-url-scheme-whitelist/README.md | 78 +++ .../__tests__/index.js | 342 +++++++++++ .../function-url-scheme-whitelist/index.js | 74 +++ lib/rules/function-whitelist/README.md | 75 +++ .../function-whitelist/__tests__/index.js | 204 +++++++ lib/rules/function-whitelist/index.js | 71 +++ lib/rules/index.js | 90 +-- .../media-feature-name-allowed-list/README.md | 2 - .../media-feature-name-blacklist/README.md | 66 ++ .../__tests__/index.js | 228 +++++++ .../media-feature-name-blacklist/index.js | 84 +++ .../README.md | 2 - .../README.md | 2 - .../README.md | 86 +++ .../__tests__/index.js | 195 ++++++ .../index.js | 97 +++ .../media-feature-name-whitelist/README.md | 66 ++ .../__tests__/index.js | 218 +++++++ .../media-feature-name-whitelist/index.js | 84 +++ lib/rules/property-allowed-list/README.md | 2 - lib/rules/property-blacklist/README.md | 66 ++ .../property-blacklist/__tests__/index.js | 182 ++++++ lib/rules/property-blacklist/index.js | 65 ++ lib/rules/property-disallowed-list/README.md | 2 - lib/rules/property-whitelist/README.md | 77 +++ .../property-whitelist/__tests__/index.js | 191 ++++++ lib/rules/property-whitelist/index.js | 65 ++ .../README.md | 2 - .../README.md | 46 ++ .../__tests__/index.js | 114 ++++ .../index.js | 73 +++ .../README.md | 2 - .../README.md | 56 ++ .../__tests__/index.js | 120 ++++ .../index.js | 73 +++ .../README.md | 2 - .../selector-combinator-blacklist/README.md | 56 ++ .../__tests__/index.js | 88 +++ .../selector-combinator-blacklist/index.js | 78 +++ .../README.md | 2 - .../selector-combinator-whitelist/README.md | 56 ++ .../__tests__/index.js | 113 ++++ .../selector-combinator-whitelist/index.js | 78 +++ .../README.md | 2 - .../selector-pseudo-class-blacklist/README.md | 55 ++ .../__tests__/index.js | 247 ++++++++ .../selector-pseudo-class-blacklist/index.js | 83 +++ .../README.md | 2 - .../selector-pseudo-class-whitelist/README.md | 55 ++ .../__tests__/index.js | 196 ++++++ .../selector-pseudo-class-whitelist/index.js | 82 +++ .../README.md | 2 - .../README.md | 56 ++ .../__tests__/index.js | 126 ++++ .../index.js | 82 +++ .../README.md | 2 - .../README.md | 56 ++ .../__tests__/index.js | 148 +++++ .../index.js | 82 +++ lib/rules/unit-allowed-list/README.md | 2 - lib/rules/unit-blacklist/README.md | 161 +++++ lib/rules/unit-blacklist/__tests__/index.js | 563 ++++++++++++++++++ lib/rules/unit-blacklist/index.js | 130 ++++ lib/rules/unit-disallowed-list/README.md | 2 - lib/rules/unit-whitelist/README.md | 117 ++++ lib/rules/unit-whitelist/__tests__/index.js | 365 ++++++++++++ lib/rules/unit-whitelist/index.js | 90 +++ lib/utils/__tests__/checkAgainstRule.test.js | 20 - 113 files changed, 9782 insertions(+), 191 deletions(-) delete mode 100644 lib/__tests__/aliasedRules.test.js create mode 100644 lib/rules/at-rule-blacklist/README.md create mode 100644 lib/rules/at-rule-blacklist/__tests__/index.js create mode 100644 lib/rules/at-rule-blacklist/index.js create mode 100644 lib/rules/at-rule-property-requirelist/README.md create mode 100644 lib/rules/at-rule-property-requirelist/__tests__/index.js create mode 100644 lib/rules/at-rule-property-requirelist/index.js create mode 100644 lib/rules/at-rule-whitelist/README.md create mode 100644 lib/rules/at-rule-whitelist/__tests__/index.js create mode 100644 lib/rules/at-rule-whitelist/index.js create mode 100644 lib/rules/comment-word-blacklist/README.md create mode 100644 lib/rules/comment-word-blacklist/__tests__/index.js create mode 100644 lib/rules/comment-word-blacklist/index.js create mode 100644 lib/rules/declaration-property-unit-blacklist/README.md create mode 100644 lib/rules/declaration-property-unit-blacklist/__tests__/index.js create mode 100644 lib/rules/declaration-property-unit-blacklist/index.js create mode 100644 lib/rules/declaration-property-unit-whitelist/README.md create mode 100644 lib/rules/declaration-property-unit-whitelist/__tests__/index.js create mode 100644 lib/rules/declaration-property-unit-whitelist/index.js create mode 100644 lib/rules/declaration-property-value-blacklist/README.md create mode 100644 lib/rules/declaration-property-value-blacklist/__tests__/index.js create mode 100644 lib/rules/declaration-property-value-blacklist/index.js create mode 100644 lib/rules/declaration-property-value-whitelist/README.md create mode 100644 lib/rules/declaration-property-value-whitelist/__tests__/index.js create mode 100644 lib/rules/declaration-property-value-whitelist/index.js create mode 100644 lib/rules/function-blacklist/README.md create mode 100644 lib/rules/function-blacklist/__tests__/index.js create mode 100644 lib/rules/function-blacklist/index.js create mode 100644 lib/rules/function-url-scheme-blacklist/README.md create mode 100644 lib/rules/function-url-scheme-blacklist/__tests__/index.js create mode 100644 lib/rules/function-url-scheme-blacklist/index.js create mode 100644 lib/rules/function-url-scheme-whitelist/README.md create mode 100644 lib/rules/function-url-scheme-whitelist/__tests__/index.js create mode 100644 lib/rules/function-url-scheme-whitelist/index.js create mode 100644 lib/rules/function-whitelist/README.md create mode 100644 lib/rules/function-whitelist/__tests__/index.js create mode 100644 lib/rules/function-whitelist/index.js create mode 100644 lib/rules/media-feature-name-blacklist/README.md create mode 100644 lib/rules/media-feature-name-blacklist/__tests__/index.js create mode 100644 lib/rules/media-feature-name-blacklist/index.js create mode 100644 lib/rules/media-feature-name-value-whitelist/README.md create mode 100644 lib/rules/media-feature-name-value-whitelist/__tests__/index.js create mode 100644 lib/rules/media-feature-name-value-whitelist/index.js create mode 100644 lib/rules/media-feature-name-whitelist/README.md create mode 100644 lib/rules/media-feature-name-whitelist/__tests__/index.js create mode 100644 lib/rules/media-feature-name-whitelist/index.js create mode 100644 lib/rules/property-blacklist/README.md create mode 100644 lib/rules/property-blacklist/__tests__/index.js create mode 100644 lib/rules/property-blacklist/index.js create mode 100644 lib/rules/property-whitelist/README.md create mode 100644 lib/rules/property-whitelist/__tests__/index.js create mode 100644 lib/rules/property-whitelist/index.js create mode 100644 lib/rules/selector-attribute-operator-blacklist/README.md create mode 100644 lib/rules/selector-attribute-operator-blacklist/__tests__/index.js create mode 100644 lib/rules/selector-attribute-operator-blacklist/index.js create mode 100644 lib/rules/selector-attribute-operator-whitelist/README.md create mode 100644 lib/rules/selector-attribute-operator-whitelist/__tests__/index.js create mode 100644 lib/rules/selector-attribute-operator-whitelist/index.js create mode 100644 lib/rules/selector-combinator-blacklist/README.md create mode 100644 lib/rules/selector-combinator-blacklist/__tests__/index.js create mode 100644 lib/rules/selector-combinator-blacklist/index.js create mode 100644 lib/rules/selector-combinator-whitelist/README.md create mode 100644 lib/rules/selector-combinator-whitelist/__tests__/index.js create mode 100644 lib/rules/selector-combinator-whitelist/index.js create mode 100644 lib/rules/selector-pseudo-class-blacklist/README.md create mode 100644 lib/rules/selector-pseudo-class-blacklist/__tests__/index.js create mode 100644 lib/rules/selector-pseudo-class-blacklist/index.js create mode 100644 lib/rules/selector-pseudo-class-whitelist/README.md create mode 100644 lib/rules/selector-pseudo-class-whitelist/__tests__/index.js create mode 100644 lib/rules/selector-pseudo-class-whitelist/index.js create mode 100644 lib/rules/selector-pseudo-element-blacklist/README.md create mode 100644 lib/rules/selector-pseudo-element-blacklist/__tests__/index.js create mode 100644 lib/rules/selector-pseudo-element-blacklist/index.js create mode 100644 lib/rules/selector-pseudo-element-whitelist/README.md create mode 100644 lib/rules/selector-pseudo-element-whitelist/__tests__/index.js create mode 100644 lib/rules/selector-pseudo-element-whitelist/index.js create mode 100644 lib/rules/unit-blacklist/README.md create mode 100644 lib/rules/unit-blacklist/__tests__/index.js create mode 100644 lib/rules/unit-blacklist/index.js create mode 100644 lib/rules/unit-whitelist/README.md create mode 100644 lib/rules/unit-whitelist/__tests__/index.js create mode 100644 lib/rules/unit-whitelist/index.js diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 6cd7c29dd7..9c4f023149 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-allowed-list`](../../../lib/rules/function-allowed-list/README.md): Specify a list of allowed functions. +- [`function-blacklist`](../../../lib/rules/function-blacklist/README.md): Specify a list of disallowed functions. **(deprecated)** - [`function-disallowed-list`](../../../lib/rules/function-disallowed-list/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-allowed-list`](../../../lib/rules/function-url-scheme-allowed-list/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-disallowed-list`](../../../lib/rules/function-url-scheme-disallowed-list/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-allowed-list`](../../../lib/rules/unit-allowed-list/README.md): Specify a list of allowed units. +- [`unit-blacklist`](../../../lib/rules/unit-blacklist/README.md): Specify a list of disallowed units. **(deprecated)** - [`unit-disallowed-list`](../../../lib/rules/unit-disallowed-list/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-allowed-list`](../../../lib/rules/property-allowed-list/README.md): Specify a list of allowed properties. +- [`property-blacklist`](../../../lib/rules/property-blacklist/README.md): Specify a list of disallowed properties. **(deprecated)** - [`property-disallowed-list`](../../../lib/rules/property-disallowed-list/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 (Autofixable). +- [`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-allowed-list`](../../../lib/rules/declaration-property-unit-allowed-list/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-disallowed-list`](../../../lib/rules/declaration-property-unit-disallowed-list/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-allowed-list`](../../../lib/rules/declaration-property-value-allowed-list/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-disallowed-list`](../../../lib/rules/declaration-property-value-disallowed-list/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-allowed-list`](../../../lib/rules/selector-attribute-operator-allowed-list/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-disallowed-list`](../../../lib/rules/selector-attribute-operator-disallowed-list/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-allowed-list`](../../../lib/rules/selector-combinator-allowed-list/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-disallowed-list`](../../../lib/rules/selector-combinator-disallowed-list/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,23 @@ 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 (Autofixable). - [`selector-pseudo-class-allowed-list`](../../../lib/rules/selector-pseudo-class-allowed-list/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-disallowed-list`](../../../lib/rules/selector-pseudo-class-disallowed-list/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-allowed-list`](../../../lib/rules/selector-pseudo-element-allowed-list/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-disallowed-list`](../../../lib/rules/selector-pseudo-element-disallowed-list/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-allowed-list`](../../../lib/rules/media-feature-name-allowed-list/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-disallowed-list`](../../../lib/rules/media-feature-name-disallowed-list/README.md): Specify a list of disallowed media feature names. - [`media-feature-name-no-vendor-prefix`](../../../lib/rules/media-feature-name-no-vendor-prefix/README.md): Disallow vendor prefixes for media feature names (Autofixable). - [`media-feature-name-value-allowed-list`](../../../lib/rules/media-feature-name-value-allowed-list/README.md): Specify a list of allowed media feature name and value pairs. +- [`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 +218,16 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### At-rule - [`at-rule-allowed-list`](../../../lib/rules/at-rule-allowed-list/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-disallowed-list`](../../../lib/rules/at-rule-disallowed-list/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 (Autofixable). - [`at-rule-property-required-list`](../../../lib/rules/at-rule-property-required-list/README.md): Specify a list of required properties for an at-rule. +- [`at-rule-property-requirelist`](../../../lib/rules/at-rule-property-requirelist/README.md): Specify a list of required properties for an at-rule. **(deprecated)** +- [`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-disallowed-list`](../../../lib/rules/comment-word-disallowed-list/README.md): Specify a list of disallowed words within comments. ### General / Sheet diff --git a/lib/__tests__/aliasedRules.test.js b/lib/__tests__/aliasedRules.test.js deleted file mode 100644 index 50a2eecaa4..0000000000 --- a/lib/__tests__/aliasedRules.test.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const rules = require('../rules'); - -const whitelistAndBlacklistRulePrefixes = [ - 'at-rule', - 'declaration-property-unit', - 'declaration-property-value', - 'function', - 'function-url-scheme', - 'media-feature-name', - 'property', - 'selector-attribute-operator', - 'selector-combinator', - 'selector-pseudo-class', - 'selector-pseudo-element', - 'unit', -]; - -const whitelistRulePrefixes = whitelistAndBlacklistRulePrefixes.concat([ - 'media-feature-name-value', -]); - -whitelistRulePrefixes.forEach((prefix) => { - it(`aliases ${prefix}-whitelist to ${prefix}-allowed-list`, () => - expect(rules[`${prefix}-whitelist`].ruleName).toEqual(`${prefix}-allowed-list`)); -}); - -const blacklistRulePrefixes = whitelistAndBlacklistRulePrefixes.concat(['comment-word']); - -blacklistRulePrefixes.forEach((prefix) => { - it(`aliases ${prefix}-blacklist to ${prefix}-disallowed-list`, () => - expect(rules[`${prefix}-blacklist`].ruleName).toEqual(`${prefix}-disallowed-list`)); -}); - -const requirelistRulePrefixes = ['at-rule-property']; - -requirelistRulePrefixes.forEach((prefix) => { - it(`aliases ${prefix}-requirelist to ${prefix}-required-list`, () => - expect(rules[`${prefix}-requirelist`].ruleName).toEqual(`${prefix}-required-list`)); -}); diff --git a/lib/__tests__/integration.test.js b/lib/__tests__/integration.test.js index df73623bf7..feddcbf36a 100644 --- a/lib/__tests__/integration.test.js +++ b/lib/__tests__/integration.test.js @@ -143,19 +143,6 @@ it('Scss integration test', () => { }); }); -it('rule aliasing integration test', () => { - return postcss() - .use(stylelint({ rules: { 'unit-blacklist': ['px'] } })) - .process('a { top: 10px; }', { from: undefined }) - .then((result) => { - const error = result.messages[0]; - - expect(error).toBeTruthy(); - expect(error.rule).toBe('unit-disallowed-list'); - expect(error.text).toBe('Unexpected unit "px" (unit-disallowed-list)'); - }); -}); - describe('integration test null option', () => { let results; diff --git a/lib/rules/at-rule-allowed-list/README.md b/lib/rules/at-rule-allowed-list/README.md index 8755baf322..995c21d40e 100644 --- a/lib/rules/at-rule-allowed-list/README.md +++ b/lib/rules/at-rule-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed at-rules. * At-rules like this */ ``` -This rule was previously called, and is aliased as, `at-rule-whitelist`. - ## Options `array|string`: `["array", "of", "unprefixed", "at-rules"]|"at-rule"` diff --git a/lib/rules/at-rule-blacklist/README.md b/lib/rules/at-rule-blacklist/README.md new file mode 100644 index 0000000000..454160549b --- /dev/null +++ b/lib/rules/at-rule-blacklist/README.md @@ -0,0 +1,52 @@ +# at-rule-blacklist + +**_Deprecated: Instead use the [`at-rule-disallowed-list`](../at-rule-disallowed-list/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..b7a1af7846 --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['extend', 'supports', 'keyframes'], + + accept: [ + { + code: 'a { color: pink; }', + description: 'Some random code.', + }, + { + code: '@mixin name ($p) {}', + description: '@rule not from a disallowed list.', + }, + ], + + reject: [ + { + code: 'a { @extend %placeholder; }', + message: messages.rejected('extend'), + line: 1, + column: 5, + description: '@rule from a disallowed list, is a Sass directive.', + }, + { + code: ` + a { + @extend + %placeholder; + } + `, + message: messages.rejected('extend'), + line: 3, + column: 9, + description: '@rule from a disallowed list; newline after its name.', + }, + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('keyframes'), + line: 2, + description: '@rule from a disallowed list; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('Keyframes'), + line: 2, + column: 7, + description: '@rule from a disallowed list; 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 disallowed list; 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 disallowed list; 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 disallowed list.', + }, + ], + + reject: [ + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('keyframes'), + line: 2, + column: 7, + description: '@rule from a disallowed list; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + message: messages.rejected('Keyframes'), + line: 2, + column: 7, + description: '@rule from a disallowed list; 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 disallowed list; 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 disallowed list; 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..3e056130e4 --- /dev/null +++ b/lib/rules/at-rule-blacklist/index.js @@ -0,0 +1,62 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'at-rule-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected at-rule "${name}"`, +}); + +function rule(listInput) { + // To allow for just a string as a parameter (not only arrays of strings) + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'at-rule-disallowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkAtRules((atRule) => { + const name = atRule.name; + + if (!isStandardSyntaxAtRule(atRule)) { + return; + } + + if (!list.includes(postcss.vendor.unprefixed(name).toLowerCase())) { + return; + } + + report({ + message: messages.rejected(name), + node: atRule, + result, + ruleName, + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/at-rule-disallowed-list/README.md b/lib/rules/at-rule-disallowed-list/README.md index 0baefb6134..336dc9d1a5 100644 --- a/lib/rules/at-rule-disallowed-list/README.md +++ b/lib/rules/at-rule-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed at-rules. * At-rules like this */ ``` -This rule was previously called, and is aliased as, `at-rule-blacklist`. - ## Options `array|string`: `["array", "of", "unprefixed", "at-rules"]|"at-rule"` diff --git a/lib/rules/at-rule-property-required-list/README.md b/lib/rules/at-rule-property-required-list/README.md index 002bf1b02c..eb4eb944c6 100644 --- a/lib/rules/at-rule-property-required-list/README.md +++ b/lib/rules/at-rule-property-required-list/README.md @@ -9,8 +9,6 @@ Specify a list of required properties for an at-rule. * At-rule and required property names */ ``` -This rule was previously called, and is aliased as, `at-rule-requirelist`. - ## Options `object`: `{ "at-rule-name": ["array", "of", "properties"] }` diff --git a/lib/rules/at-rule-property-requirelist/README.md b/lib/rules/at-rule-property-requirelist/README.md new file mode 100644 index 0000000000..6108023b72 --- /dev/null +++ b/lib/rules/at-rule-property-requirelist/README.md @@ -0,0 +1,55 @@ +# at-rule-property-requirelist + +**_Deprecated: Instead use the [`at-rule-property-required-list`](../at-rule-property-required-list/README.md) rule._** + +Specify a list of required properties for an at-rule. + + +```css + @font-face { font-display: swap; font-family: 'foo'; } +/** ↑ ↑ ↑ + * At-rule and required property names */ +``` + +## Options + +`object`: `{ "at-rule-name": ["array", "of", "properties"] }` + +Given: + +``` +{ + "font-face": ["font-display", "font-family", "font-style"] +} +``` + +The following patterns are considered violations: + + +```css +@font-face { + font-family: 'foo'; + src: url('./fonts/foo.woff2') format('woff2'); +} +``` + + +```css +@font-face { + font-family: 'foo'; + font-style: normal; + src: url('./fonts/foo.woff2') format('woff2'); +} +``` + +The following patterns are _not_ considered violations: + + +```css +@font-face { + font-display: swap; + font-family: 'foo'; + font-style: normal; + src: url('./fonts/foo.woff2') format('woff2'); +} +``` diff --git a/lib/rules/at-rule-property-requirelist/__tests__/index.js b/lib/rules/at-rule-property-requirelist/__tests__/index.js new file mode 100644 index 0000000000..183e7643b4 --- /dev/null +++ b/lib/rules/at-rule-property-requirelist/__tests__/index.js @@ -0,0 +1,84 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('warns that the rule is deprecated', () => { + const config = { + rules: { + [ruleName]: { page: ['margin'] }, + }, + }; + + 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-property-required-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +testRule({ + ruleName, + config: { + 'font-face': ['font-display', 'font-family'], + page: ['margin'], + }, + + accept: [ + { + code: "@font-face { font-display: auto; font-family: 'Arvo'; }", + description: '@font-face with both required properties', + }, + { + code: "@font-face { font-display: auto; font-family: 'Arvo'; src: url('abc'); /* IE9 */ }", + description: '@font-face with inner comment', + }, + { + code: "@FONT-FACE { FONT-DISPLAY: AUTO; FONT-FAMILY: 'ARVO'; }", + description: '@font-face with both required properties (case-sensitive)', + }, + { + code: '@page { padding: 0.5cm; margin: 1cm; }', + description: '@page with required property', + }, + { + code: '@counter-style counter { system: cyclic; }', + description: 'at-rule not specified in config', + }, + { + code: '@mixin invalid-at-rule { @content; }', + description: '@mixin with invalid at-rule', + }, + ], + + reject: [ + { + code: '@font-face { font-display: auto; }', + description: '@font-face with missing property', + message: messages.expected('font-family', 'font-face'), + }, + { + code: '@FONT-FACE { FONT-DISPLAY: AUTO; }', + description: '@font-face with missing property (case-sensitive)', + message: messages.expected('font-family', 'font-face'), + }, + { + code: "@font-face { font-family: 'Arvo'; font-weight: normal }", + description: '@font-face with missing property', + message: messages.expected('font-display', 'font-face'), + }, + { + code: '@page { padding: 0.5cm }', + description: '@page with missing property', + message: messages.expected('margin', 'page'), + }, + ], +}); diff --git a/lib/rules/at-rule-property-requirelist/index.js b/lib/rules/at-rule-property-requirelist/index.js new file mode 100644 index 0000000000..e979f78b41 --- /dev/null +++ b/lib/rules/at-rule-property-requirelist/index.js @@ -0,0 +1,73 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'at-rule-property-requirelist'; + +const messages = ruleMessages(ruleName, { + expected: (property, atRule) => `Expected property "${property}" for at-rule "${atRule}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'at-rule-property-required-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkAtRules((atRule) => { + if (!isStandardSyntaxAtRule(atRule)) { + return; + } + + const { name, nodes } = atRule; + const atRuleName = name.toLowerCase(); + + if (!list[atRuleName]) { + return; + } + + list[atRuleName].forEach((property) => { + const propertyName = property.toLowerCase(); + + const hasProperty = nodes.find( + ({ type, prop }) => type === 'decl' && prop.toLowerCase() === propertyName, + ); + + if (hasProperty) { + return; + } + + return report({ + message: messages.expected(propertyName, atRuleName), + node: atRule, + result, + ruleName, + }); + }); + }); + }; +} + +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..f6e0a006a3 --- /dev/null +++ b/lib/rules/at-rule-whitelist/README.md @@ -0,0 +1,67 @@ +# at-rule-whitelist + +**_Deprecated: Instead use the [`at-rule-allowed-list`](../at-rule-allowed-list/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..74f79a3e8c --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +testRule({ + ruleName, + + config: ['extend', 'import', 'keyframes'], + + accept: [ + { + code: 'a { color: pink; }', + description: 'Some random code.', + }, + { + code: 'a { @extend %placeholder; }', + description: '@rule from an allowed list, is a Sass directive.', + }, + { + code: ` + a { + @extend + %placeholder; + } + `, + description: '@rule from an allowed list; newline after its name.', + }, + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule; messed case.', + }, + { + code: ` + @-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule; has vendor prefix.', + }, + { + code: ` + @-WEBKET-KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule; has vendor prefix.', + }, + ], + + reject: [ + { + code: ` + @mixin name () {} + `, + line: 2, + columt: 7, + message: messages.rejected('mixin'), + description: '@rule not from an allowed list; independent rule.', + }, + ], +}); + +testRule({ + ruleName, + skipBasicChecks: true, + + config: ['keyframes'], + + accept: [ + { + code: ` + @keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule.', + }, + { + code: ` + @Keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule; messed case.', + }, + { + code: ` + @-moz-keyframes name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule; has vendor prefix.', + }, + { + code: ` + @-WEBKET-KEYFRAMES name { + from { top: 10px; } + to { top: 20px; } + } + `, + description: '@rule from an allowed list; independent rule; has vendor prefix.', + }, + ], + + reject: [ + { + code: ` + @mixin name ($p) {} + `, + message: messages.rejected('mixin'), + line: 2, + column: 7, + description: '@rule not from an allowed list.', + }, + { + code: "@import 'path/to/file.css';", + message: messages.rejected('import'), + line: 1, + column: 1, + description: '@rule not from an allowed list.', + }, + { + code: '@media screen and (max-witdh: 1000px) {}', + message: messages.rejected('media'), + line: 1, + column: 1, + description: '@rule not from an allowed list.', + }, + ], +}); + +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..716ee578b7 --- /dev/null +++ b/lib/rules/at-rule-whitelist/index.js @@ -0,0 +1,62 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'at-rule-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected at-rule "${name}"`, +}); + +function rule(listInput) { + // To allow for just a string as a parameter (not only arrays of strings) + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'at-rule-allowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkAtRules((atRule) => { + const name = atRule.name; + + if (!isStandardSyntaxAtRule(atRule)) { + return; + } + + if (list.includes(postcss.vendor.unprefixed(name).toLowerCase())) { + return; + } + + report({ + message: messages.rejected(name), + node: atRule, + result, + ruleName, + }); + }); + }; +} + +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..bcac8e8b90 --- /dev/null +++ b/lib/rules/comment-word-blacklist/README.md @@ -0,0 +1,50 @@ +# comment-word-blacklist + +**_Deprecated: Instead use the [`comment-word-disallowed-list`](../comment-word-disallowed-list/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..70a331716c --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..b58df52d93 --- /dev/null +++ b/lib/rules/comment-word-blacklist/index.js @@ -0,0 +1,64 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const containsString = require('../../utils/containsString'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'comment-word-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (pattern) => `Unexpected word matching pattern "${pattern}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'comment-word-disallowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkComments((comment) => { + const text = comment.text; + const rawComment = comment.toString(); + const firstFourChars = rawComment.substr(0, 4); + + // Return early if sourcemap + if (firstFourChars === '/*# ') { + return; + } + + const matchesWord = matchesStringOrRegExp(text, list) || containsString(text, list); + + if (!matchesWord) { + return; + } + + report({ + message: messages.rejected(matchesWord.pattern), + node: comment, + result, + ruleName, + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/comment-word-disallowed-list/README.md b/lib/rules/comment-word-disallowed-list/README.md index 5578dc894a..814667b43c 100644 --- a/lib/rules/comment-word-disallowed-list/README.md +++ b/lib/rules/comment-word-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed words within comments. * These three words */ ``` -This rule was previously called, and is aliased as, `comment-word-blacklist`. - **Caveat:** Comments within _selector and value lists_ are currently ignored. ## Options diff --git a/lib/rules/declaration-property-unit-allowed-list/README.md b/lib/rules/declaration-property-unit-allowed-list/README.md index 8ff7d6d235..48a95c973d 100644 --- a/lib/rules/declaration-property-unit-allowed-list/README.md +++ b/lib/rules/declaration-property-unit-allowed-list/README.md @@ -9,8 +9,6 @@ a { width: 100px; } * These properties and these units */ ``` -This rule was previously called, and is aliased as, `declaration-property-unit-whitelist`. - ## Options `object`: `{ "unprefixed-property-name": ["array", "of", "units"] }` 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..7537255e43 --- /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-disallowed-list`](../declaration-property-unit-disallowed-list/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..bf4f9a2fff --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..db1d840dd2 --- /dev/null +++ b/lib/rules/declaration-property-unit-blacklist/index.js @@ -0,0 +1,84 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const getUnitFromValueNode = require('../../utils/getUnitFromValueNode'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'declaration-property-unit-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, unit) => `Unexpected unit "${unit}" for property "${property}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-unit-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkDecls((decl) => { + const prop = decl.prop; + const value = decl.value; + + const unprefixedProp = postcss.vendor.unprefixed(prop); + + const propList = _.find(list, (units, propIdentifier) => + matchesStringOrRegExp(unprefixedProp, propIdentifier), + ); + + if (!propList) { + return; + } + + valueParser(value).walk((node) => { + // Ignore wrong units within `url` function + if (node.type === 'function' && node.value.toLowerCase() === 'url') { + return false; + } + + if (node.type === 'string') { + return; + } + + const unit = getUnitFromValueNode(node); + + if (!unit || (unit && !propList.includes(unit.toLowerCase()))) { + return; + } + + report({ + message: messages.rejected(prop, unit), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-unit-disallowed-list/README.md b/lib/rules/declaration-property-unit-disallowed-list/README.md index 50d0cc81f0..088d42a3af 100644 --- a/lib/rules/declaration-property-unit-disallowed-list/README.md +++ b/lib/rules/declaration-property-unit-disallowed-list/README.md @@ -9,8 +9,6 @@ a { width: 100px; } * These properties and these units */ ``` -This rule was previously called, and is aliased as, `declaration-property-unit-blacklist`. - ## Options `object`: `{ "unprefixed-property-name": ["array", "of", "units"] }` 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..da73901d63 --- /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-allowed-list`](../declaration-property-unit-allowed-list/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..9850fb4cfc --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..cd2dc9eef0 --- /dev/null +++ b/lib/rules/declaration-property-unit-whitelist/index.js @@ -0,0 +1,84 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const getUnitFromValueNode = require('../../utils/getUnitFromValueNode'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'declaration-property-unit-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, unit) => `Unexpected unit "${unit}" for property "${property}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-unit-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkDecls((decl) => { + const prop = decl.prop; + const value = decl.value; + + const unprefixedProp = postcss.vendor.unprefixed(prop); + + const propList = _.find(list, (units, propIdentifier) => + matchesStringOrRegExp(unprefixedProp, propIdentifier), + ); + + if (!propList) { + return; + } + + valueParser(value).walk((node) => { + // Ignore wrong units within `url` function + if (node.type === 'function' && node.value.toLowerCase() === 'url') { + return false; + } + + if (node.type === 'string') { + return; + } + + const unit = getUnitFromValueNode(node); + + if (!unit || (unit && propList.indexOf(unit.toLowerCase())) !== -1) { + return; + } + + report({ + message: messages.rejected(prop, unit), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-value-allowed-list/README.md b/lib/rules/declaration-property-value-allowed-list/README.md index 32ff898097..41371ddd09 100644 --- a/lib/rules/declaration-property-value-allowed-list/README.md +++ b/lib/rules/declaration-property-value-allowed-list/README.md @@ -9,8 +9,6 @@ a { text-transform: uppercase; } * These properties and these values */ ``` -This rule was previously called, and is aliased as, `declaration-property-value-whitelist`. - ## Options `object`: `{ "unprefixed-property-name": ["array", "of", "values"], "unprefixed-property-name": ["/regex/", "non-regex"] }` 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..9edd6c9b1d --- /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-disallowed-list`](../declaration-property-value-disallowed-list/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..a1d3005a29 --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..3550bf70e4 --- /dev/null +++ b/lib/rules/declaration-property-value-blacklist/index.js @@ -0,0 +1,66 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'declaration-property-value-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-value-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkDecls((decl) => { + const prop = decl.prop; + const value = decl.value; + + const unprefixedProp = postcss.vendor.unprefixed(prop); + const propList = _.find(list, (values, propIdentifier) => + matchesStringOrRegExp(unprefixedProp, propIdentifier), + ); + + if (_.isEmpty(propList)) { + return; + } + + if (!matchesStringOrRegExp(value, propList)) { + return; + } + + report({ + message: messages.rejected(prop, value), + node: decl, + result, + ruleName, + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/declaration-property-value-disallowed-list/README.md b/lib/rules/declaration-property-value-disallowed-list/README.md index ea583a2f9f..982799a29d 100644 --- a/lib/rules/declaration-property-value-disallowed-list/README.md +++ b/lib/rules/declaration-property-value-disallowed-list/README.md @@ -9,8 +9,6 @@ a { text-transform: uppercase; } * These properties and these values */ ``` -This rule was previously called, and is aliased as, `declaration-property-value-blacklist`. - ## Options `object`: `{ "unprefixed-property-name": ["array", "of", "values"], "unprefixed-property-name": ["/regex/", "non-regex", /regex/] }` 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..2e7f2b9848 --- /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-allowed-list`](../declaration-property-value-allowed-list/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 the listed 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..7f674ab4b7 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..3a20e2b340 --- /dev/null +++ b/lib/rules/declaration-property-value-whitelist/index.js @@ -0,0 +1,66 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'declaration-property-value-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'declaration-property-value-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkDecls((decl) => { + const prop = decl.prop; + const value = decl.value; + + const unprefixedProp = postcss.vendor.unprefixed(prop); + const propList = _.find(list, (values, propIdentifier) => + matchesStringOrRegExp(unprefixedProp, propIdentifier), + ); + + if (_.isEmpty(propList)) { + return; + } + + if (matchesStringOrRegExp(value, propList)) { + return; + } + + report({ + message: messages.rejected(prop, value), + node: decl, + result, + ruleName, + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-allowed-list/README.md b/lib/rules/function-allowed-list/README.md index e8229a835a..92b304d491 100644 --- a/lib/rules/function-allowed-list/README.md +++ b/lib/rules/function-allowed-list/README.md @@ -9,8 +9,6 @@ a { transform: scale(1); } * This function */ ``` -This rule was previously called, and is aliased as, `function-whitelist`. - ## Options `array|string`: `["array", "of", "unprefixed", /functions/ or "regex"]|"function"|"/regex/"` diff --git a/lib/rules/function-blacklist/README.md b/lib/rules/function-blacklist/README.md new file mode 100644 index 0000000000..9cb80e361c --- /dev/null +++ b/lib/rules/function-blacklist/README.md @@ -0,0 +1,54 @@ +# function-blacklist + +**_Deprecated: Instead use the [`function-disallowed-list`](../function-disallowed-list/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..7d7db8d4ff --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..e26654a22d --- /dev/null +++ b/lib/rules/function-blacklist/index.js @@ -0,0 +1,69 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'function-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected function "${name}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'function-disallowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkDecls((decl) => { + const value = decl.value; + + valueParser(value).walk((node) => { + if (node.type !== 'function') { + return; + } + + if (!isStandardSyntaxFunction(node)) { + return; + } + + if (!matchesStringOrRegExp(postcss.vendor.unprefixed(node.value), list)) { + return; + } + + report({ + message: messages.rejected(node.value), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-disallowed-list/README.md b/lib/rules/function-disallowed-list/README.md index 8f622db3ae..e6dad004a2 100644 --- a/lib/rules/function-disallowed-list/README.md +++ b/lib/rules/function-disallowed-list/README.md @@ -9,8 +9,6 @@ a { transform: scale(1); } * This function */ ``` -This rule was previously called, and is aliased as, `function-blacklist`. - ## Options `array|string`: `["array", "of", "unprefixed", /functions/ or "regex"]|"function"|"/regex/"` diff --git a/lib/rules/function-url-scheme-allowed-list/README.md b/lib/rules/function-url-scheme-allowed-list/README.md index 8a2d49154c..64cbbdcfd6 100644 --- a/lib/rules/function-url-scheme-allowed-list/README.md +++ b/lib/rules/function-url-scheme-allowed-list/README.md @@ -9,8 +9,6 @@ a { background-image: url('http://www.example.com/file.jpg'); } * This URL scheme */ ``` -This rule was previously called, and is aliased as, `function-url-scheme-whitelist`. - 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: 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..14fb678310 --- /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-disallowed-list`](../function-url-scheme-disallowed-list/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..d4ec6f2dbc --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..c85bb0f68a --- /dev/null +++ b/lib/rules/function-url-scheme-blacklist/index.js @@ -0,0 +1,74 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const functionArgumentsSearch = require('../../utils/functionArgumentsSearch'); +const getSchemeFromUrl = require('../../utils/getSchemeFromUrl'); +const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'function-url-scheme-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (scheme) => `Unexpected URL scheme "${scheme}:"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'function-url-scheme-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkDecls((decl) => { + functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => { + const unspacedUrlString = _.trim(args, ' '); + + if (!isStandardSyntaxUrl(unspacedUrlString)) { + return; + } + + const urlString = _.trim(unspacedUrlString, '\'"'); + const scheme = getSchemeFromUrl(urlString); + + if (scheme === null) { + return; + } + + if (!matchesStringOrRegExp(scheme, list)) { + return; + } + + report({ + message: messages.rejected(scheme), + node: decl, + index, + result, + ruleName, + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/function-url-scheme-disallowed-list/README.md b/lib/rules/function-url-scheme-disallowed-list/README.md index a6dadce430..11e4d73d7e 100644 --- a/lib/rules/function-url-scheme-disallowed-list/README.md +++ b/lib/rules/function-url-scheme-disallowed-list/README.md @@ -9,8 +9,6 @@ a { background-image: url('http://www.example.com/file.jpg'); } * This URL scheme */ ``` -This rule was previously called, and is aliased as, `function-url-scheme-blacklist`. - 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: 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..ece68797d7 --- /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-allowed-list`](../function-url-scheme-allowed-list/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..f6e3823f86 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..320453c993 --- /dev/null +++ b/lib/rules/function-url-scheme-whitelist/index.js @@ -0,0 +1,74 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const functionArgumentsSearch = require('../../utils/functionArgumentsSearch'); +const getSchemeFromUrl = require('../../utils/getSchemeFromUrl'); +const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'function-url-scheme-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (scheme) => `Unexpected URL scheme "${scheme}:"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'function-url-scheme-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkDecls((decl) => { + functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => { + const unspacedUrlString = _.trim(args, ' '); + + if (!isStandardSyntaxUrl(unspacedUrlString)) { + return; + } + + const urlString = _.trim(unspacedUrlString, '\'"'); + const scheme = getSchemeFromUrl(urlString); + + if (scheme === null) { + return; + } + + if (matchesStringOrRegExp(scheme, list)) { + return; + } + + report({ + message: messages.rejected(scheme), + node: decl, + index, + result, + ruleName, + }); + }); + }); + }; +} + +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..f3620b3b4b --- /dev/null +++ b/lib/rules/function-whitelist/README.md @@ -0,0 +1,75 @@ +# function-whitelist + +**_Deprecated: Instead use the [`function-allowed-list`](../function-allowed-list/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..e2843265d8 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..95627bb108 --- /dev/null +++ b/lib/rules/function-whitelist/index.js @@ -0,0 +1,71 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'function-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected function "${name}"`, +}); + +function rule(listInput) { + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'function-allowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkDecls((decl) => { + const value = decl.value; + + valueParser(value).walk((node) => { + if (node.type !== 'function') { + return; + } + + if (!isStandardSyntaxFunction(node)) { + return; + } + + if (matchesStringOrRegExp(postcss.vendor.unprefixed(node.value), list)) { + return; + } + + report({ + message: messages.rejected(node.value), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +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 390bb34432..a2ee0d5260 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -10,8 +10,7 @@ const importLazy = require('import-lazy'); const rules = { 'alpha-value-notation': importLazy(() => require('./alpha-value-notation'))(), 'at-rule-allowed-list': importLazy(() => require('./at-rule-allowed-list'))(), - // Renamed to at-rule-disallowed-list - 'at-rule-blacklist': importLazy(() => require('./at-rule-disallowed-list'))(), + 'at-rule-blacklist': importLazy(() => require('./at-rule-blacklist'))(), 'at-rule-disallowed-list': importLazy(() => require('./at-rule-disallowed-list'))(), 'at-rule-empty-line-before': importLazy(() => require('./at-rule-empty-line-before'))(), 'at-rule-name-case': importLazy(() => require('./at-rule-name-case'))(), @@ -21,13 +20,11 @@ const rules = { 'at-rule-no-unknown': importLazy(() => require('./at-rule-no-unknown'))(), 'at-rule-no-vendor-prefix': importLazy(() => require('./at-rule-no-vendor-prefix'))(), 'at-rule-property-required-list': importLazy(() => require('./at-rule-property-required-list'))(), - // Renamed to at-rule-required-list - 'at-rule-property-requirelist': importLazy(() => require('./at-rule-property-required-list'))(), + 'at-rule-property-requirelist': importLazy(() => require('./at-rule-property-requirelist'))(), 'at-rule-semicolon-newline-after': importLazy(() => require('./at-rule-semicolon-newline-after'), )(), - // Renamed to at-rule-allowed-list - 'at-rule-whitelist': importLazy(() => require('./at-rule-allowed-list'))(), + 'at-rule-whitelist': importLazy(() => require('./at-rule-whitelist'))(), 'block-closing-brace-empty-line-before': importLazy(() => require('./block-closing-brace-empty-line-before'), )(), @@ -65,8 +62,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'))(), - // Renamed to comment-word-disallowed-list - 'comment-word-blacklist': importLazy(() => require('./comment-word-disallowed-list'))(), + 'comment-word-blacklist': importLazy(() => require('./comment-word-blacklist'))(), 'comment-word-disallowed-list': importLazy(() => require('./comment-word-disallowed-list'))(), 'custom-media-pattern': importLazy(() => require('./custom-media-pattern'))(), 'custom-property-empty-line-before': importLazy(() => @@ -112,30 +108,26 @@ const rules = { 'declaration-property-unit-allowed-list': importLazy(() => require('./declaration-property-unit-allowed-list'), )(), - // Renamed to declaration-property-unit-disallowed-list 'declaration-property-unit-blacklist': importLazy(() => - require('./declaration-property-unit-disallowed-list'), + require('./declaration-property-unit-blacklist'), )(), 'declaration-property-unit-disallowed-list': importLazy(() => require('./declaration-property-unit-disallowed-list'), )(), - // Renamed to declaration-property-unit-allowed-list 'declaration-property-unit-whitelist': importLazy(() => - require('./declaration-property-unit-allowed-list'), + require('./declaration-property-unit-whitelist'), )(), 'declaration-property-value-allowed-list': importLazy(() => require('./declaration-property-value-allowed-list'), )(), - // Renamed to declaration-property-value-disallowed-list 'declaration-property-value-blacklist': importLazy(() => - require('./declaration-property-value-disallowed-list'), + require('./declaration-property-value-blacklist'), )(), 'declaration-property-value-disallowed-list': importLazy(() => require('./declaration-property-value-disallowed-list'), )(), - // Renamed to declaration-property-value-allowed-list 'declaration-property-value-whitelist': importLazy(() => - require('./declaration-property-value-allowed-list'), + require('./declaration-property-value-whitelist'), )(), 'font-family-no-missing-generic-family-keyword': importLazy(() => require('./font-family-no-missing-generic-family-keyword'), @@ -144,8 +136,7 @@ const rules = { 'font-family-no-duplicate-names': importLazy(() => require('./font-family-no-duplicate-names'))(), 'font-weight-notation': importLazy(() => require('./font-weight-notation'))(), 'function-allowed-list': importLazy(() => require('./function-allowed-list'))(), - // Renamed to function-disallowed-list - 'function-blacklist': importLazy(() => require('./function-disallowed-list'))(), + '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'), @@ -173,20 +164,13 @@ const rules = { 'function-url-scheme-allowed-list': importLazy(() => require('./function-url-scheme-allowed-list'), )(), - // Renamed to function-url-scheme-disallowed-list - 'function-url-scheme-blacklist': importLazy(() => - require('./function-url-scheme-disallowed-list'), - )(), + 'function-url-scheme-blacklist': importLazy(() => require('./function-url-scheme-blacklist'))(), 'function-url-scheme-disallowed-list': importLazy(() => require('./function-url-scheme-disallowed-list'), )(), - // Renamed to function-url-scheme-allowed-list - 'function-url-scheme-whitelist': importLazy(() => - require('./function-url-scheme-allowed-list'), - )(), + 'function-url-scheme-whitelist': importLazy(() => require('./function-url-scheme-whitelist'))(), 'function-whitespace-after': importLazy(() => require('./function-whitespace-after'))(), - // Renamed to function-allowed-list - 'function-whitelist': importLazy(() => require('./function-allowed-list'))(), + 'function-whitelist': importLazy(() => require('./function-whitelist'))(), 'hue-degree-notation': importLazy(() => require('./hue-degree-notation'))(), 'keyframe-declaration-no-important': importLazy(() => require('./keyframe-declaration-no-important'), @@ -206,10 +190,7 @@ const rules = { 'media-feature-name-allowed-list': importLazy(() => require('./media-feature-name-allowed-list'), )(), - // Renamed to media-feature-name-disallowed-list - 'media-feature-name-blacklist': importLazy(() => - require('./media-feature-name-disallowed-list'), - )(), + 'media-feature-name-blacklist': importLazy(() => require('./media-feature-name-blacklist'))(), 'media-feature-name-case': importLazy(() => require('./media-feature-name-case'))(), 'media-feature-name-disallowed-list': importLazy(() => require('./media-feature-name-disallowed-list'), @@ -222,10 +203,9 @@ const rules = { require('./media-feature-name-value-allowed-list'), )(), 'media-feature-name-value-whitelist': importLazy(() => - require('./media-feature-name-value-allowed-list'), + require('./media-feature-name-value-whitelist'), )(), - // Renamed to media-feature-name-allowed-list - 'media-feature-name-whitelist': importLazy(() => require('./media-feature-name-allowed-list'))(), + 'media-feature-name-whitelist': importLazy(() => require('./media-feature-name-whitelist'))(), 'media-feature-parentheses-space-inside': importLazy(() => require('./media-feature-parentheses-space-inside'), )(), @@ -265,14 +245,12 @@ const rules = { 'number-max-precision': importLazy(() => require('./number-max-precision'))(), 'number-no-trailing-zeros': importLazy(() => require('./number-no-trailing-zeros'))(), 'property-allowed-list': importLazy(() => require('./property-allowed-list'))(), - // Renamed to property-disallowed-list - 'property-blacklist': importLazy(() => require('./property-disallowed-list'))(), + 'property-blacklist': importLazy(() => require('./property-blacklist'))(), 'property-case': importLazy(() => require('./property-case'))(), 'property-disallowed-list': importLazy(() => require('./property-disallowed-list'))(), 'property-no-unknown': importLazy(() => require('./property-no-unknown'))(), 'property-no-vendor-prefix': importLazy(() => require('./property-no-vendor-prefix'))(), - // Renamed to property-allowed-list - 'property-whitelist': importLazy(() => require('./property-allowed-list'))(), + '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'), @@ -280,9 +258,8 @@ const rules = { 'selector-attribute-operator-allowed-list': importLazy(() => require('./selector-attribute-operator-allowed-list'), )(), - // Renamed to selector-attribute-operator-disallowed-list 'selector-attribute-operator-blacklist': importLazy(() => - require('./selector-attribute-operator-disallowed-list'), + require('./selector-attribute-operator-blacklist'), )(), 'selector-attribute-operator-disallowed-list': importLazy(() => require('./selector-attribute-operator-disallowed-list'), @@ -293,19 +270,15 @@ const rules = { 'selector-attribute-operator-space-before': importLazy(() => require('./selector-attribute-operator-space-before'), )(), - // Renamed to selector-attribute-operator-allowed-list 'selector-attribute-operator-whitelist': importLazy(() => - require('./selector-attribute-operator-allowed-list'), + require('./selector-attribute-operator-whitelist'), )(), 'selector-attribute-quotes': importLazy(() => require('./selector-attribute-quotes'))(), 'selector-class-pattern': importLazy(() => require('./selector-class-pattern'))(), 'selector-combinator-allowed-list': importLazy(() => require('./selector-combinator-allowed-list'), )(), - // Renamed to selector-combinator-disallowed-list - 'selector-combinator-blacklist': importLazy(() => - require('./selector-combinator-disallowed-list'), - )(), + 'selector-combinator-blacklist': importLazy(() => require('./selector-combinator-blacklist'))(), 'selector-combinator-disallowed-list': importLazy(() => require('./selector-combinator-disallowed-list'), )(), @@ -315,10 +288,7 @@ const rules = { 'selector-combinator-space-before': importLazy(() => require('./selector-combinator-space-before'), )(), - // Renamed to selector-combinator-allowed-list - 'selector-combinator-whitelist': importLazy(() => - require('./selector-combinator-allowed-list'), - )(), + 'selector-combinator-whitelist': importLazy(() => require('./selector-combinator-whitelist'))(), 'selector-descendant-combinator-no-non-space': importLazy(() => require('./selector-descendant-combinator-no-non-space'), )(), @@ -353,9 +323,8 @@ const rules = { 'selector-pseudo-class-allowed-list': importLazy(() => require('./selector-pseudo-class-allowed-list'), )(), - // Renamed to selector-pseudo-class-disallowed-list 'selector-pseudo-class-blacklist': importLazy(() => - require('./selector-pseudo-class-disallowed-list'), + require('./selector-pseudo-class-blacklist'), )(), 'selector-pseudo-class-case': importLazy(() => require('./selector-pseudo-class-case'))(), 'selector-pseudo-class-disallowed-list': importLazy(() => @@ -367,16 +336,14 @@ const rules = { 'selector-pseudo-class-parentheses-space-inside': importLazy(() => require('./selector-pseudo-class-parentheses-space-inside'), )(), - // Renamed to selector-pseudo-class-allowed-list 'selector-pseudo-class-whitelist': importLazy(() => - require('./selector-pseudo-class-allowed-list'), + require('./selector-pseudo-class-whitelist'), )(), 'selector-pseudo-element-allowed-list': importLazy(() => require('./selector-pseudo-element-allowed-list'), )(), - // Renamed to selector-pseudo-element-disallowed-list 'selector-pseudo-element-blacklist': importLazy(() => - require('./selector-pseudo-element-disallowed-list'), + require('./selector-pseudo-element-blacklist'), )(), 'selector-pseudo-element-case': importLazy(() => require('./selector-pseudo-element-case'))(), 'selector-pseudo-element-colon-notation': importLazy(() => @@ -388,9 +355,8 @@ const rules = { 'selector-pseudo-element-no-unknown': importLazy(() => require('./selector-pseudo-element-no-unknown'), )(), - // Renamed to selector-pseudo-element-allowed-list 'selector-pseudo-element-whitelist': importLazy(() => - require('./selector-pseudo-element-allowed-list'), + require('./selector-pseudo-element-whitelist'), )(), 'selector-type-case': importLazy(() => require('./selector-type-case'))(), 'selector-type-no-unknown': importLazy(() => require('./selector-type-no-unknown'))(), @@ -402,13 +368,11 @@ const rules = { 'time-min-milliseconds': importLazy(() => require('./time-min-milliseconds'))(), 'unicode-bom': importLazy(() => require('./unicode-bom'))(), 'unit-allowed-list': importLazy(() => require('./unit-allowed-list'))(), - // Renamed to unit-disallowed-list - 'unit-blacklist': importLazy(() => require('./unit-disallowed-list'))(), + 'unit-blacklist': importLazy(() => require('./unit-blacklist'))(), 'unit-case': importLazy(() => require('./unit-case'))(), 'unit-disallowed-list': importLazy(() => require('./unit-disallowed-list'))(), 'unit-no-unknown': importLazy(() => require('./unit-no-unknown'))(), - // Renamed to unit-allowed-list - 'unit-whitelist': importLazy(() => require('./unit-allowed-list'))(), + '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-allowed-list/README.md b/lib/rules/media-feature-name-allowed-list/README.md index 4c5c28abcf..255b865f1f 100644 --- a/lib/rules/media-feature-name-allowed-list/README.md +++ b/lib/rules/media-feature-name-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed media feature names. * This media feature name */ ``` -This rule was previously called, and is aliased as, `media-feature-name-whitelist`. - ## Options `array|string|regex`: `["array", "of", "unprefixed", /media-features/ or "regex"]|"media-feature"|/regex/` 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..e89f5753cf --- /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-disallowed-list`](../media-feature-name-disallowed-list/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..bd28913d70 --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..40f0a54db1 --- /dev/null +++ b/lib/rules/media-feature-name-blacklist/index.js @@ -0,0 +1,84 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const isCustomMediaQuery = require('../../utils/isCustomMediaQuery'); +const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeature'); +const isStandardSyntaxMediaFeatureName = require('../../utils/isStandardSyntaxMediaFeatureName'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const mediaParser = require('postcss-media-query-parser').default; +const rangeContextNodeParser = require('../rangeContextNodeParser'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'media-feature-name-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected media feature name "${name}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'media-feature-name-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkAtRules(/^media$/i, (atRule) => { + mediaParser(atRule.params).walk(/^media-feature$/i, (mediaFeatureNode) => { + const parent = mediaFeatureNode.parent; + const mediaFeatureRangeContext = isRangeContextMediaFeature(parent.value); + + let value; + let sourceIndex; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + value = parsedRangeContext.name.value; + sourceIndex = parsedRangeContext.name.sourceIndex; + } else { + value = mediaFeatureNode.value; + sourceIndex = mediaFeatureNode.sourceIndex; + } + + if (!isStandardSyntaxMediaFeatureName(value) || isCustomMediaQuery(value)) { + return; + } + + if (!matchesStringOrRegExp(value, list)) { + return; + } + + report({ + index: atRuleParamIndex(atRule) + sourceIndex, + message: messages.rejected(value), + node: atRule, + ruleName, + result, + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/media-feature-name-disallowed-list/README.md b/lib/rules/media-feature-name-disallowed-list/README.md index aa4b012208..cd232e1736 100644 --- a/lib/rules/media-feature-name-disallowed-list/README.md +++ b/lib/rules/media-feature-name-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed media feature names. * This media feature name */ ``` -This rule was previously called, and is aliased as, `media-feature-name-blacklist`. - ## Options `array|string|regex`: `["array", "of", "unprefixed", /media-features/ or "regex"]|"media-feature"|/regex/` diff --git a/lib/rules/media-feature-name-value-allowed-list/README.md b/lib/rules/media-feature-name-value-allowed-list/README.md index 0e45c430d8..e81fd3cf29 100644 --- a/lib/rules/media-feature-name-value-allowed-list/README.md +++ b/lib/rules/media-feature-name-value-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed media feature name and value pairs. * These features and values */ ``` -This rule was previously called, and is aliased as, `media-feature-name-value-whitelist`. - ## Options ```js 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..3a975e64d7 --- /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-allowed-list`](../media-feature-name-value-allowed-list/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 allowed-listed 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..e7d1f0baf2 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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 allowed list', + }, + { + code: '@media (color) {}', + description: 'Boolean context, media feature in allowed list', + }, + { + code: '@media (update) {}', + description: 'Boolean context, media feature NOT in allowed list', + }, + { + 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 allowed list', + }, + ], + + 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 allowed list', + 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..3eb18856e5 --- /dev/null +++ b/lib/rules/media-feature-name-value-whitelist/index.js @@ -0,0 +1,97 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeature'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const mediaParser = require('postcss-media-query-parser').default; +const postcss = require('postcss'); +const rangeContextNodeParser = require('../rangeContextNodeParser'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'media-feature-name-value-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name, value) => `Unexpected value "${value}" for name "${name}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'media-feature-name-value-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkAtRules(/^media$/i, (atRule) => { + mediaParser(atRule.params).walk(/^media-feature-expression$/i, (node) => { + const mediaFeatureRangeContext = isRangeContextMediaFeature(node.parent.value); + + // Ignore boolean + if (!node.value.includes(':') && !mediaFeatureRangeContext) { + return; + } + + const mediaFeatureNode = _.find(node.nodes, { type: 'media-feature' }); + + let mediaFeatureName; + let values = []; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + mediaFeatureName = parsedRangeContext.name.value; + values = parsedRangeContext.values; + } else { + mediaFeatureName = mediaFeatureNode.value; + values.push(_.find(node.nodes, { type: 'value' })); + } + + for (let i = 0; i < values.length; i++) { + const valueNode = values[i]; + const value = valueNode.value; + const unprefixedMediaFeatureName = postcss.vendor.unprefixed(mediaFeatureName); + + const allowedValues = _.find(list, (v, featureName) => + matchesStringOrRegExp(unprefixedMediaFeatureName, featureName), + ); + + if (allowedValues === undefined) { + return; + } + + if (matchesStringOrRegExp(value, allowedValues)) { + return; + } + + report({ + index: atRuleParamIndex(atRule) + valueNode.sourceIndex, + message: messages.rejected(mediaFeatureName, value), + node: atRule, + ruleName, + result, + }); + } + }); + }); + }; +} + +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..292ab9afe9 --- /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-allowed-list`](../media-feature-name-allowed-list/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..f0af8f625b --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..ebfdff9fb6 --- /dev/null +++ b/lib/rules/media-feature-name-whitelist/index.js @@ -0,0 +1,84 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const isCustomMediaQuery = require('../../utils/isCustomMediaQuery'); +const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeature'); +const isStandardSyntaxMediaFeatureName = require('../../utils/isStandardSyntaxMediaFeatureName'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const mediaParser = require('postcss-media-query-parser').default; +const rangeContextNodeParser = require('../rangeContextNodeParser'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'media-feature-name-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (name) => `Unexpected media feature name "${name}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'media-feature-name-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkAtRules(/^media$/i, (atRule) => { + mediaParser(atRule.params).walk(/^media-feature$/i, (mediaFeatureNode) => { + const parent = mediaFeatureNode.parent; + const mediaFeatureRangeContext = isRangeContextMediaFeature(parent.value); + + let value; + let sourceIndex; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + value = parsedRangeContext.name.value; + sourceIndex = parsedRangeContext.name.sourceIndex; + } else { + value = mediaFeatureNode.value; + sourceIndex = mediaFeatureNode.sourceIndex; + } + + if (!isStandardSyntaxMediaFeatureName(value) || isCustomMediaQuery(value)) { + return; + } + + if (matchesStringOrRegExp(value, list)) { + return; + } + + report({ + index: atRuleParamIndex(atRule) + sourceIndex, + message: messages.rejected(value), + node: atRule, + ruleName, + result, + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/property-allowed-list/README.md b/lib/rules/property-allowed-list/README.md index 4383c3164c..e2b2250e1a 100644 --- a/lib/rules/property-allowed-list/README.md +++ b/lib/rules/property-allowed-list/README.md @@ -9,8 +9,6 @@ a { display: block; } * This property */ ``` -This rule was previously called, and is aliased as, `property-whitelist`. - This rule ignores variables (`$sass`, `@less`, `--custom-property`). ## Options diff --git a/lib/rules/property-blacklist/README.md b/lib/rules/property-blacklist/README.md new file mode 100644 index 0000000000..b244fb8584 --- /dev/null +++ b/lib/rules/property-blacklist/README.md @@ -0,0 +1,66 @@ +# property-blacklist + +**_Deprecated: Instead use the [`property-disallowed-list`](../property-disallowed-list/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..68a3c450aa --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..6744c9dc16 --- /dev/null +++ b/lib/rules/property-blacklist/index.js @@ -0,0 +1,65 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isCustomProperty = require('../../utils/isCustomProperty'); +const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'property-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (property) => `Unexpected property "${property}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'property-disallowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkDecls((decl) => { + const prop = decl.prop; + + if (!isStandardSyntaxProperty(prop)) { + return; + } + + if (isCustomProperty(prop)) { + return; + } + + if (!matchesStringOrRegExp(postcss.vendor.unprefixed(prop), list)) { + return; + } + + report({ + message: messages.rejected(prop), + node: decl, + result, + ruleName, + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/property-disallowed-list/README.md b/lib/rules/property-disallowed-list/README.md index 012981f710..81edc3444f 100644 --- a/lib/rules/property-disallowed-list/README.md +++ b/lib/rules/property-disallowed-list/README.md @@ -9,8 +9,6 @@ a { text-rendering: optimizeLegibility; } * This property */ ``` -This rule was previously called, and is aliased as, `property-blacklist`. - ## Options `array|string`: `["array", "of", "unprefixed", /properties/ or "regex"]|"property"|"/regex/"`|/regex/ diff --git a/lib/rules/property-whitelist/README.md b/lib/rules/property-whitelist/README.md new file mode 100644 index 0000000000..1500c0ca19 --- /dev/null +++ b/lib/rules/property-whitelist/README.md @@ -0,0 +1,77 @@ +# property-whitelist + +**_Deprecated: Instead use the [`property-allowed-list`](../property-allowed-list/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..65efbc1efb --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..81f4fb0f8a --- /dev/null +++ b/lib/rules/property-whitelist/index.js @@ -0,0 +1,65 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isCustomProperty = require('../../utils/isCustomProperty'); +const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'property-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (property) => `Unexpected property "${property}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'property-allowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + root.walkDecls((decl) => { + const prop = decl.prop; + + if (!isStandardSyntaxProperty(prop)) { + return; + } + + if (isCustomProperty(prop)) { + return; + } + + if (matchesStringOrRegExp(postcss.vendor.unprefixed(prop), list)) { + return; + } + + report({ + message: messages.rejected(prop), + node: decl, + result, + ruleName, + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-attribute-operator-allowed-list/README.md b/lib/rules/selector-attribute-operator-allowed-list/README.md index 6cdbae223b..557d9ec0a8 100644 --- a/lib/rules/selector-attribute-operator-allowed-list/README.md +++ b/lib/rules/selector-attribute-operator-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed attribute operators. * This operator */ ``` -This rule was previously called, and is aliased as, `selector-attribute-operator-whitelist`. - ## Options `array|string`: `["array", "of", "operators"]|"operator"` 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..f3e54f4629 --- /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-disallowed-list`](../selector-attribute-operator-disallowed-list/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..e6d9f654d3 --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..d28b3d09ea --- /dev/null +++ b/lib/rules/selector-attribute-operator-blacklist/index.js @@ -0,0 +1,73 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const parseSelector = require('../../utils/parseSelector'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-attribute-operator-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (operator) => `Unexpected operator "${operator}"`, +}); + +function rule(listInput) { + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-attribute-operator-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + if (!rule.selector.includes('[') || !rule.selector.includes('=')) { + return; + } + + parseSelector(rule.selector, result, rule, (selectorTree) => { + selectorTree.walkAttributes((attributeNode) => { + const operator = attributeNode.operator; + + if (!operator || (operator && !list.includes(operator))) { + return; + } + + report({ + message: messages.rejected(operator), + node: rule, + index: attributeNode.sourceIndex + attributeNode.offsetOf('operator'), + result, + ruleName, + }); + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-attribute-operator-disallowed-list/README.md b/lib/rules/selector-attribute-operator-disallowed-list/README.md index 97b51a25d3..62cbb512bc 100644 --- a/lib/rules/selector-attribute-operator-disallowed-list/README.md +++ b/lib/rules/selector-attribute-operator-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed attribute operators. * This operator */ ``` -This rule was previously called, and is aliased as, `selector-attribute-operator-blacklist`. - ## Options `array|string`: `["array", "of", "operators"]|"operator"` 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..364d8266e6 --- /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-allowed-list`](../selector-attribute-operator-allowed-list/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..e43b0c1683 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..34a39dfcc9 --- /dev/null +++ b/lib/rules/selector-attribute-operator-whitelist/index.js @@ -0,0 +1,73 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const parseSelector = require('../../utils/parseSelector'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-attribute-operator-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (operator) => `Unexpected operator "${operator}"`, +}); + +function rule(listInput) { + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-attribute-operator-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + if (!rule.selector.includes('[') || !rule.selector.includes('=')) { + return; + } + + parseSelector(rule.selector, result, rule, (selectorTree) => { + selectorTree.walkAttributes((attributeNode) => { + const operator = attributeNode.operator; + + if (!operator || (operator && list.includes(operator))) { + return; + } + + report({ + message: messages.rejected(operator), + node: rule, + index: attributeNode.sourceIndex + attributeNode.offsetOf('operator'), + result, + ruleName, + }); + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-combinator-allowed-list/README.md b/lib/rules/selector-combinator-allowed-list/README.md index 9181f40b93..5fdce86ffa 100644 --- a/lib/rules/selector-combinator-allowed-list/README.md +++ b/lib/rules/selector-combinator-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed combinators. * This combinator */ ``` -This rule was previously called, and is aliased as, `selector-combinator-whitelist`. - 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/`. diff --git a/lib/rules/selector-combinator-blacklist/README.md b/lib/rules/selector-combinator-blacklist/README.md new file mode 100644 index 0000000000..cb64dfe82b --- /dev/null +++ b/lib/rules/selector-combinator-blacklist/README.md @@ -0,0 +1,56 @@ +# selector-combinator-blacklist + +**_Deprecated: Instead use the [`selector-combinator-disallowed-list`](../selector-combinator-disallowed-list/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..e3ff4b7c43 --- /dev/null +++ b/lib/rules/selector-combinator-blacklist/__tests__/index.js @@ -0,0 +1,88 @@ +'use strict'; + +const standalone = require('../../../standalone'); +const { messages, ruleName } = require('..'); + +it('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); +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..698dc7df4c --- /dev/null +++ b/lib/rules/selector-combinator-blacklist/index.js @@ -0,0 +1,78 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxCombinator = require('../../utils/isStandardSyntaxCombinator'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const parseSelector = require('../../utils/parseSelector'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-combinator-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (combinator) => `Unexpected combinator "${combinator}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-combinator-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + const selector = rule.selector; + + parseSelector(selector, result, rule, (fullSelector) => { + fullSelector.walkCombinators((combinatorNode) => { + if (!isStandardSyntaxCombinator(combinatorNode)) { + return; + } + + const value = normalizeCombinator(combinatorNode.value); + + if (!list.includes(value)) { + return; + } + + report({ + result, + ruleName, + message: messages.rejected(value), + node: rule, + index: combinatorNode.sourceIndex, + }); + }); + }); + }); + }; +} + +function normalizeCombinator(value) { + return value.replace(/\s+/g, ' '); +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-combinator-disallowed-list/README.md b/lib/rules/selector-combinator-disallowed-list/README.md index 98685b9867..ae4fe2a419 100644 --- a/lib/rules/selector-combinator-disallowed-list/README.md +++ b/lib/rules/selector-combinator-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed combinators. * This combinator */ ``` -This rule was previously called, and is aliased as, `selector-combinator-blacklist`. - 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/`. diff --git a/lib/rules/selector-combinator-whitelist/README.md b/lib/rules/selector-combinator-whitelist/README.md new file mode 100644 index 0000000000..49454ca37a --- /dev/null +++ b/lib/rules/selector-combinator-whitelist/README.md @@ -0,0 +1,56 @@ +# selector-combinator-whitelist + +**_Deprecated: Instead use the [`selector-combinator-allowed-list`](../selector-combinator-allowed-list/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..e08d23c145 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..a2d28034c4 --- /dev/null +++ b/lib/rules/selector-combinator-whitelist/index.js @@ -0,0 +1,78 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxCombinator = require('../../utils/isStandardSyntaxCombinator'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const parseSelector = require('../../utils/parseSelector'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-combinator-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (combinator) => `Unexpected combinator "${combinator}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-combinator-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + const selector = rule.selector; + + parseSelector(selector, result, rule, (fullSelector) => { + fullSelector.walkCombinators((combinatorNode) => { + if (!isStandardSyntaxCombinator(combinatorNode)) { + return; + } + + const value = normalizeCombinator(combinatorNode.value); + + if (list.includes(value)) { + return; + } + + report({ + result, + ruleName, + message: messages.rejected(value), + node: rule, + index: combinatorNode.sourceIndex, + }); + }); + }); + }); + }; +} + +function normalizeCombinator(value) { + return value.replace(/\s+/g, ' '); +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-class-allowed-list/README.md b/lib/rules/selector-pseudo-class-allowed-list/README.md index b22b2711fa..5932b92623 100644 --- a/lib/rules/selector-pseudo-class-allowed-list/README.md +++ b/lib/rules/selector-pseudo-class-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed pseudo-class selectors. * This pseudo-class selector */ ``` -This rule was previously called, and is aliased as, `selector-pseudo-class-whitelist`. - This rule ignores selectors that use variable interpolation e.g. `:#{$variable} {}`. ## Options 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..be91d9a071 --- /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-disallowed-list`](../selector-pseudo-class-disallowed-list/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..9ee952df21 --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..73040ba475 --- /dev/null +++ b/lib/rules/selector-pseudo-class-blacklist/index.js @@ -0,0 +1,83 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const parseSelector = require('../../utils/parseSelector'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-pseudo-class-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-class "${selector}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-class-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + const selector = rule.selector; + + if (!selector.includes(':')) { + return; + } + + parseSelector(selector, result, rule, (selectorTree) => { + selectorTree.walkPseudos((pseudoNode) => { + const value = pseudoNode.value; + + // Ignore pseudo-elements + + if (value.slice(0, 2) === '::') { + return; + } + + const name = value.slice(1); + + if (!matchesStringOrRegExp(postcss.vendor.unprefixed(name), list)) { + return; + } + + report({ + index: pseudoNode.sourceIndex, + message: messages.rejected(name), + node: rule, + result, + ruleName, + }); + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-class-disallowed-list/README.md b/lib/rules/selector-pseudo-class-disallowed-list/README.md index bb21ebf65a..dbd5a98d31 100644 --- a/lib/rules/selector-pseudo-class-disallowed-list/README.md +++ b/lib/rules/selector-pseudo-class-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed pseudo-class selectors. * This pseudo-class selector */ ``` -This rule was previously called, and is aliased as, `selector-pseudo-class-blacklist`. - This rule ignores selectors that use variable interpolation e.g. `:#{$variable} {}`. ## Options 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..294f80300a --- /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-allowed-list`](../selector-pseudo-class-allowed-list/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..136f7392a1 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..8f9d8d8773 --- /dev/null +++ b/lib/rules/selector-pseudo-class-whitelist/index.js @@ -0,0 +1,82 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const parseSelector = require('../../utils/parseSelector'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-pseudo-class-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-class "${selector}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-class-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + const selector = rule.selector; + + if (!selector.includes(':')) { + return; + } + + parseSelector(selector, result, rule, (selectorTree) => { + selectorTree.walkPseudos((pseudoNode) => { + const value = pseudoNode.value; + + // Ignore pseudo-elements + if (value.slice(0, 2) === '::') { + return; + } + + const name = value.slice(1); + + if (matchesStringOrRegExp(postcss.vendor.unprefixed(name), list)) { + return; + } + + report({ + index: pseudoNode.sourceIndex, + message: messages.rejected(name), + node: rule, + result, + ruleName, + }); + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-element-allowed-list/README.md b/lib/rules/selector-pseudo-element-allowed-list/README.md index 2ed1068195..c920727f8a 100644 --- a/lib/rules/selector-pseudo-element-allowed-list/README.md +++ b/lib/rules/selector-pseudo-element-allowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of allowed pseudo-element selectors. * This pseudo-element selector */ ``` -This rule was previously called, and is aliased as, `selector-pseudo-element-whitelist`. - This rule ignores: - CSS2 pseudo-elements i.e. those prefixed with a single colon 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..bcb9cf2908 --- /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-disallowed-list`](../selector-pseudo-element-disallowed-list/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..77cf01b66b --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..8333f37bbb --- /dev/null +++ b/lib/rules/selector-pseudo-element-blacklist/index.js @@ -0,0 +1,82 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const parseSelector = require('../../utils/parseSelector'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-pseudo-element-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-element "${selector}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-element-disallowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + const selector = rule.selector; + + if (!selector.includes('::')) { + return; + } + + parseSelector(selector, result, rule, (selectorTree) => { + selectorTree.walkPseudos((pseudoNode) => { + const value = pseudoNode.value; + + // Ignore pseudo-classes + if (value[1] !== ':') { + return; + } + + const name = value.slice(2); + + if (!matchesStringOrRegExp(postcss.vendor.unprefixed(name), list)) { + return; + } + + report({ + index: pseudoNode.sourceIndex, + message: messages.rejected(name), + node: rule, + result, + ruleName, + }); + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/selector-pseudo-element-disallowed-list/README.md b/lib/rules/selector-pseudo-element-disallowed-list/README.md index 0db5b8df2c..36516836e7 100644 --- a/lib/rules/selector-pseudo-element-disallowed-list/README.md +++ b/lib/rules/selector-pseudo-element-disallowed-list/README.md @@ -9,8 +9,6 @@ Specify a list of disallowed pseudo-element selectors. * This pseudo-element selector */ ``` -This rule was previously called, and is aliased as, `selector-pseudo-element-blacklist`. - This rule ignores: - CSS2 pseudo-elements i.e. those prefixed with a single colon 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..2a24022cb0 --- /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-allowed-list`](../selector-pseudo-element-allowed-list/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..20823c8a20 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..949785a55b --- /dev/null +++ b/lib/rules/selector-pseudo-element-whitelist/index.js @@ -0,0 +1,82 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const parseSelector = require('../../utils/parseSelector'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'selector-pseudo-element-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (selector) => `Unexpected pseudo-element "${selector}"`, +}); + +function rule(list) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: list, + possible: [_.isString, _.isRegExp], + }); + + if (!validOptions) { + return; + } + + result.warn( + `'${ruleName}' has been deprecated. Instead use 'selector-pseudo-element-allowed-list'.`, + { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }, + ); + + root.walkRules((rule) => { + if (!isStandardSyntaxRule(rule)) { + return; + } + + const selector = rule.selector; + + if (!selector.includes('::')) { + return; + } + + parseSelector(selector, result, rule, (selectorTree) => { + selectorTree.walkPseudos((pseudoNode) => { + const value = pseudoNode.value; + + // Ignore pseudo-classes + if (value[1] !== ':') { + return; + } + + const name = value.slice(2); + + if (matchesStringOrRegExp(postcss.vendor.unprefixed(name), list)) { + return; + } + + report({ + index: pseudoNode.sourceIndex, + message: messages.rejected(name), + node: rule, + result, + ruleName, + }); + }); + }); + }); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/unit-allowed-list/README.md b/lib/rules/unit-allowed-list/README.md index 2c34eda820..3f5e2de93c 100644 --- a/lib/rules/unit-allowed-list/README.md +++ b/lib/rules/unit-allowed-list/README.md @@ -9,8 +9,6 @@ a { width: 100px; } * These units */ ``` -This rule was previously called, and is aliased as, `unit-whitelist`. - ## Options `array|string`: `["array", "of", "units"]|"unit"` diff --git a/lib/rules/unit-blacklist/README.md b/lib/rules/unit-blacklist/README.md new file mode 100644 index 0000000000..bcfa8710b3 --- /dev/null +++ b/lib/rules/unit-blacklist/README.md @@ -0,0 +1,161 @@ +# unit-blacklist + +**_Deprecated: Instead use the [`unit-disallowed-list`](../unit-disallowed-list/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..728701be94 --- /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('warns that the rule is deprecated', () => { + 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-disallowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..b27a5ae3e7 --- /dev/null +++ b/lib/rules/unit-blacklist/index.js @@ -0,0 +1,130 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const getUnitFromValueNode = require('../../utils/getUnitFromValueNode'); +const mediaParser = require('postcss-media-query-parser').default; +const optionsMatches = require('../../utils/optionsMatches'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'unit-blacklist'; + +const messages = ruleMessages(ruleName, { + rejected: (unit) => `Unexpected unit "${unit}"`, +}); + +// a function to retrieve only the media feature name +// could be externalized in an utils function if needed in other code +const getMediaFeatureName = (mediaFeatureNode) => { + const value = mediaFeatureNode.value.toLowerCase(); + + return /((-?\w*)*)/i.exec(value)[1]; +}; + +function rule(listInput, options) { + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions( + result, + ruleName, + { + actual: list, + possible: [_.isString], + }, + { + optional: true, + actual: options, + possible: { + ignoreProperties: validateObjectWithArrayProps([_.isString, _.isRegExp]), + ignoreMediaFeatureNames: validateObjectWithArrayProps([_.isString, _.isRegExp]), + }, + }, + ); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'unit-disallowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + function check(node, nodeIndex, valueNode, input, option) { + const unit = getUnitFromValueNode(valueNode); + + // There is not unit or it is not configured as a violation + if (!unit || (unit && !list.includes(unit.toLowerCase()))) { + return; + } + + // The unit has an ignore option for the specific input + if (optionsMatches(option, unit.toLowerCase(), input)) { + return; + } + + report({ + index: nodeIndex + valueNode.sourceIndex, + message: messages.rejected(unit), + node, + result, + ruleName, + }); + } + + function checkMedia(node, value, getIndex) { + mediaParser(node.params).walk(/^media-feature$/i, (mediaFeatureNode) => { + const mediaName = getMediaFeatureName(mediaFeatureNode); + const parentValue = mediaFeatureNode.parent.value; + + valueParser(value).walk((valueNode) => { + // Ignore all non-word valueNode and + // the values not included in the parentValue string + if (valueNode.type !== 'word' || !parentValue.includes(valueNode.value)) { + return; + } + + check( + node, + getIndex(node), + valueNode, + mediaName, + options ? options.ignoreMediaFeatureNames : {}, + ); + }); + }); + } + + function checkDecl(node, value, getIndex) { + // make sure multiplication operations (*) are divided - not handled + // by postcss-value-parser + value = value.replace(/\*/g, ','); + + valueParser(value).walk((valueNode) => { + // Ignore wrong units within `url` function + if (valueNode.type === 'function' && valueNode.value.toLowerCase() === 'url') { + return false; + } + + check(node, getIndex(node), valueNode, node.prop, options ? options.ignoreProperties : {}); + }); + } + + root.walkAtRules(/^media$/i, (atRule) => checkMedia(atRule, atRule.params, atRuleParamIndex)); + root.walkDecls((decl) => checkDecl(decl, decl.value, declarationValueIndex)); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/unit-disallowed-list/README.md b/lib/rules/unit-disallowed-list/README.md index 2f20017030..e38d426968 100644 --- a/lib/rules/unit-disallowed-list/README.md +++ b/lib/rules/unit-disallowed-list/README.md @@ -9,8 +9,6 @@ a { width: 100px; } * These units */ ``` -This rule was previously called, and is aliased as, `unit-blacklist`. - ## Options `array|string`: `["array", "of", "units"]|"unit"` diff --git a/lib/rules/unit-whitelist/README.md b/lib/rules/unit-whitelist/README.md new file mode 100644 index 0000000000..c32c1333d7 --- /dev/null +++ b/lib/rules/unit-whitelist/README.md @@ -0,0 +1,117 @@ +# unit-whitelist + +**_Deprecated: Instead use the [`unit-allowed-list`](../unit-allowed-list/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..07ef614ca0 --- /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('warns that the rule is deprecated', () => { + 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-allowed-list'.`, + ); + expect(result.deprecations[0].reference).toEqual( + `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + ); + }); +}); + +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..b9f6b18b9f --- /dev/null +++ b/lib/rules/unit-whitelist/index.js @@ -0,0 +1,90 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const getUnitFromValueNode = require('../../utils/getUnitFromValueNode'); +const optionsMatches = require('../../utils/optionsMatches'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'unit-whitelist'; + +const messages = ruleMessages(ruleName, { + rejected: (unit) => `Unexpected unit "${unit}"`, +}); + +function rule(listInput, options) { + const list = [].concat(listInput); + + return (root, result) => { + const validOptions = validateOptions( + result, + ruleName, + { + actual: list, + possible: [_.isString], + }, + { + optional: true, + actual: options, + possible: { + ignoreProperties: validateObjectWithArrayProps([_.isString, _.isRegExp]), + }, + }, + ); + + if (!validOptions) { + return; + } + + result.warn(`'${ruleName}' has been deprecated. Instead use 'unit-allowed-list'.`, { + stylelintType: 'deprecation', + stylelintReference: `https://github.com/stylelint/stylelint/blob/13.7.0/lib/rules/${ruleName}/README.md`, + }); + + function check(node, value, getIndex) { + // make sure multiplication operations (*) are divided - not handled + // by postcss-value-parser + value = value.replace(/\*/g, ','); + valueParser(value).walk((valueNode) => { + // Ignore wrong units within `url` function + if (valueNode.type === 'function' && valueNode.value.toLowerCase() === 'url') { + return false; + } + + const unit = getUnitFromValueNode(valueNode); + + if (!unit || (unit && list.includes(unit.toLowerCase()))) { + return; + } + + if (options && optionsMatches(options.ignoreProperties, unit.toLowerCase(), node.prop)) { + return; + } + + report({ + index: getIndex(node) + valueNode.sourceIndex, + message: messages.rejected(unit), + node, + result, + ruleName, + }); + }); + } + + root.walkAtRules(/^media$/i, (atRule) => check(atRule, atRule.params, atRuleParamIndex)); + root.walkDecls((decl) => check(decl, decl.value, declarationValueIndex)); + }; +} + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/utils/__tests__/checkAgainstRule.test.js b/lib/utils/__tests__/checkAgainstRule.test.js index df353eb45f..13a2ee22a6 100644 --- a/lib/utils/__tests__/checkAgainstRule.test.js +++ b/lib/utils/__tests__/checkAgainstRule.test.js @@ -60,24 +60,4 @@ describe('checkAgainstRule', () => { expect(warnings[0].line).toBe(3); expect(warnings[0].column).toBe(1); }); - - it('warns against a renamed rule when an aliased rule is used', () => { - const root = postcss.parse('a { top: 10px; }'); - - const warnings = []; - - checkAgainstRule( - { - ruleName: 'unit-whitelist', - ruleSettings: ['em'], - root, - }, - (warning) => warnings.push(warning), - ); - - expect(warnings).toHaveLength(1); - expect(warnings[0].rule).toBe('unit-allowed-list'); - expect(warnings[0].line).toBe(1); - expect(warnings[0].column).toBe(10); - }); });