From 1bc40a1599d645273c69a42839112bc6e9a1c8cd Mon Sep 17 00:00:00 2001 From: doing-art Date: Tue, 2 Nov 2021 17:40:39 +0200 Subject: [PATCH] Add rule-selector-property-disallowed-list (#5679) Co-authored-by: Richard Hallows --- docs/user-guide/rules/list.md | 4 + lib/rules/index.js | 3 + .../README.md | 61 +++++++++++++++ .../__tests__/index.js | 74 +++++++++++++++++++ .../index.js | 66 +++++++++++++++++ 5 files changed, 208 insertions(+) create mode 100644 lib/rules/rule-selector-property-disallowed-list/README.md create mode 100644 lib/rules/rule-selector-property-disallowed-list/__tests__/index.js create mode 100644 lib/rules/rule-selector-property-disallowed-list/index.js diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 171f9e3015..94a8cb34e3 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -194,6 +194,10 @@ Grouped first by the following categories and then by the [_thing_](http://apps. - [`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. +### Rules + +- [`rule-selector-property-disallowed-list`](../../../lib/rules/rule-selector-property-disallowed-list/README.md): Specify a list of disallowed properties for selectors within rules. + ### Media feature - [`media-feature-name-allowed-list`](../../../lib/rules/media-feature-name-allowed-list/README.md): Specify a list of allowed media feature names. diff --git a/lib/rules/index.js b/lib/rules/index.js index 1a5cb09e28..2295815ebc 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -235,6 +235,9 @@ const rules = { 'property-no-unknown': importLazy(() => require('./property-no-unknown'))(), 'property-no-vendor-prefix': importLazy(() => require('./property-no-vendor-prefix'))(), 'rule-empty-line-before': importLazy(() => require('./rule-empty-line-before'))(), + 'rule-selector-property-disallowed-list': importLazy(() => + require('./rule-selector-property-disallowed-list'), + )(), 'selector-attribute-brackets-space-inside': importLazy(() => require('./selector-attribute-brackets-space-inside'), )(), diff --git a/lib/rules/rule-selector-property-disallowed-list/README.md b/lib/rules/rule-selector-property-disallowed-list/README.md new file mode 100644 index 0000000000..1a98194335 --- /dev/null +++ b/lib/rules/rule-selector-property-disallowed-list/README.md @@ -0,0 +1,61 @@ +# rule-selector-property-disallowed-list + +Specify a list of disallowed properties for selectors within rules. + + +```css + a { color: red; } +/** ↑ ↑ + * Selector and property name */ +``` + +## Options + +`object`: `{ "selector": ["array", "of", "properties"]` + +If a selector name is surrounded with `"/"` (e.g. `"/anchor/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of all the potential anchors: `/anchor/` will match `.anchor`, `[data-anchor]`, etc. + +The same goes for properties. Keep in mind that a regular expression value is matched against the entire property name, not specific parts of it. For example, a value like `"animation-duration"` will _not_ match `"/^duration/"` (notice beginning of the line boundary) but _will_ match `"/duration/"`. + +Given: + +```json +{ + "a": ["color", "/margin/"], + "/foo/": ["/size/"] +} +``` + +The following patterns are considered problems: + + +```css +a { color: red; } +``` + + +```css +a { margin-top: 0px; } +``` + + +```css +html[data-foo] { font-size: 1px; } +``` + +The following patterns are _not_ considered problems: + + +```css +a { background: red; } +``` + + +```css +a { padding-top: 0px; } +``` + + +```css +html[data-foo] { color: red; } +``` diff --git a/lib/rules/rule-selector-property-disallowed-list/__tests__/index.js b/lib/rules/rule-selector-property-disallowed-list/__tests__/index.js new file mode 100644 index 0000000000..1cda63712c --- /dev/null +++ b/lib/rules/rule-selector-property-disallowed-list/__tests__/index.js @@ -0,0 +1,74 @@ +'use strict'; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: { + a: ['color', '/margin/'], + '/foo/': ['/size/'], + }, + + accept: [ + { + code: 'a { background: red; }', + }, + { + code: 'a { padding-top: 0px; }', + }, + { + code: 'a { background-color: red; }', + }, + { + code: 'a[href="#"] { color: red; }', + }, + { + code: 'html.foo { color: red; }', + }, + { + code: 'html[data-foo] { color: red; }', + }, + ], + + reject: [ + { + code: 'a { color: red; }', + message: messages.rejected('color', 'a'), + line: 1, + column: 5, + }, + { + code: 'a { background: red; color: red; }', + message: messages.rejected('color', 'a'), + line: 1, + column: 22, + }, + { + code: 'a { margin-top: 0px; }', + message: messages.rejected('margin-top', 'a'), + line: 1, + column: 5, + }, + { + code: 'a { color: red; margin-top: 0px; }', + warnings: [ + { message: messages.rejected('color', 'a'), line: 1, column: 5 }, + { message: messages.rejected('margin-top', 'a'), line: 1, column: 17 }, + ], + line: 1, + column: 5, + }, + { + code: '[data-foo] { font-size: 1rem; }', + message: messages.rejected('font-size', '[data-foo]'), + line: 1, + column: 14, + }, + { + code: 'html[data-foo] { font-size: 1px; }', + message: messages.rejected('font-size', 'html[data-foo]'), + line: 1, + column: 18, + }, + ], +}); diff --git a/lib/rules/rule-selector-property-disallowed-list/index.js b/lib/rules/rule-selector-property-disallowed-list/index.js new file mode 100644 index 0000000000..ef5e0cf44d --- /dev/null +++ b/lib/rules/rule-selector-property-disallowed-list/index.js @@ -0,0 +1,66 @@ +'use strict'; + +const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const { isPlainObject } = require('is-plain-object'); + +const ruleName = 'rule-selector-property-disallowed-list'; + +const messages = ruleMessages(ruleName, { + rejected: (property, selector) => `Unexpected property "${property}" for selector "${selector}"`, +}); + +/** @type {import('stylelint').Rule} */ +const rule = (primary) => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primary, + possible: [isPlainObject], + }); + + if (!validOptions) { + return; + } + + const selectors = Object.keys(primary); + + root.walkRules((ruleNode) => { + if (!isStandardSyntaxRule(ruleNode)) { + return; + } + + const selectorKey = selectors.find((selector) => + matchesStringOrRegExp(ruleNode.selector, selector), + ); + + if (!selectorKey) { + return; + } + + const disallowedProperties = primary[selectorKey]; + + for (const node of ruleNode.nodes) { + const isDisallowedProperty = + node.type === 'decl' && matchesStringOrRegExp(node.prop, disallowedProperties); + + if (isDisallowedProperty) { + report({ + message: messages.rejected(node.prop, ruleNode.selector), + node, + result, + ruleName, + }); + } + } + }); + }; +}; + +rule.primaryOptionArray = true; + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule;