diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index be7dc668d7..418456e88e 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -198,6 +198,7 @@ Within each cateogory, the rules are grouped by the [_thing_](http://apps.workfl - [`selector-nested-pattern`](../../../lib/rules/selector-nested-pattern/README.md): Specify a pattern for the selectors of rules nested within rules. - [`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-not-notation`](../../../lib/rules/selector-not-notation/README.md): Specify simple or complex notation for the usage of `:not()`. - [`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-disallowed-list`](../../../lib/rules/selector-pseudo-class-disallowed-list/README.md): Specify a list of disallowed pseudo-class selectors. - [`selector-pseudo-element-allowed-list`](../../../lib/rules/selector-pseudo-element-allowed-list/README.md): Specify a list of allowed pseudo-element selectors. diff --git a/lib/rules/index.js b/lib/rules/index.js index a300e747b4..238b24a36c 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -214,6 +214,7 @@ const rules = { 'selector-nested-pattern': importLazy('./selector-nested-pattern'), 'selector-no-qualifying-type': importLazy('./selector-no-qualifying-type'), 'selector-no-vendor-prefix': importLazy('./selector-no-vendor-prefix'), + 'selector-not-notation': importLazy('./selector-not-notation'), 'selector-pseudo-class-allowed-list': importLazy('./selector-pseudo-class-allowed-list'), 'selector-pseudo-class-case': importLazy('./selector-pseudo-class-case'), 'selector-pseudo-class-disallowed-list': importLazy('./selector-pseudo-class-disallowed-list'), diff --git a/lib/rules/selector-not-notation/README.md b/lib/rules/selector-not-notation/README.md new file mode 100644 index 0000000000..d37c6d50de --- /dev/null +++ b/lib/rules/selector-not-notation/README.md @@ -0,0 +1,57 @@ +# selector-not-notation + +## Options + +`string`: `"simple"|"complex"` + +### `"simple"` + +The following patterns are considered problems: + + +```css +:not(a, div) {} +``` + + +```css +:not(a.foo) {} +``` + +The following patterns are _not_ considered problems: + + +```css +:not(a):not(div) {} +``` + + +```css +:not(a) {} +``` + +### `"complex"` + +The following pattern is considered a problem: + + +```css +:not(a):not(div) {} +``` + +The following patterns are _not_ considered problems: + + +```css +:not(a, div) {} +``` + + +```css +:not(a.foo) {} +``` + + +```css +:not(a).foo:not(:empty) {} +``` diff --git a/lib/rules/selector-not-notation/__tests__/index.js b/lib/rules/selector-not-notation/__tests__/index.js new file mode 100644 index 0000000000..3f69361911 --- /dev/null +++ b/lib/rules/selector-not-notation/__tests__/index.js @@ -0,0 +1,95 @@ +'use strict'; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: ['simple'], + fix: false, + + accept: [ + { + code: ':not() {}', + }, + { + code: ':not( a ) {}', + }, + { + code: ':nOt(a) {}', + }, + { + code: ':not(*) {}', + }, + { + code: ':not(:link) {}', + }, + { + code: ':not(.foo) {}', + }, + { + code: ':not([title]) {}', + }, + ], + + reject: [ + { + code: ':not(:not()) {}', + fixed: ':not()', + message: messages.expected('simple'), + line: 1, + column: 1, + }, + { + code: ':not(::before) {}', + fixed: ':not()', + message: messages.expected('simple'), + line: 1, + column: 1, + }, + { + code: ':not(a, div) {}', + fixed: ':not(a):not(div) {}', + message: messages.expected('simple'), + line: 1, + column: 1, + }, + { + code: ':not(a.foo) {}', + unfixable: true, + message: messages.expected('simple'), + line: 1, + column: 1, + }, + ], +}); + +testRule({ + ruleName, + config: ['complex'], + fix: false, + + accept: [ + { + code: ':not()::after {}', + }, + { + code: ':not(a, div) {}', + }, + { + code: ':not(a.foo) {}', + }, + { + code: ':not(a).foo:not(:empty) {}', + }, + ], + + reject: [ + { + code: ':not(.foo):not(a) {}', + fixed: ':not(.foo, a) {} {}', + message: messages.expected('complex'), + line: 1, + column: 11, + }, + ], +}); diff --git a/lib/rules/selector-not-notation/index.js b/lib/rules/selector-not-notation/index.js new file mode 100644 index 0000000000..27feed0c2c --- /dev/null +++ b/lib/rules/selector-not-notation/index.js @@ -0,0 +1,128 @@ +'use strict'; + +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 { + isPseudoClass: buggyIsPseudoClass, // cf https://github.com/postcss/postcss-selector-parser/blob/master/src/selectors/guards.js#L52 + isAttribute, + isClassName, + isUniversal, + isIdentifier, + isTag, +} = require('postcss-selector-parser'); + +const ruleName = 'selector-not-notation'; +const messages = ruleMessages(ruleName, { + expected: (type) => `Expected ${type} :not() pseudo-class notation`, +}); +const meta = { url: 'https://stylelint.io/user-guide/rules/list/selector-not-notation' }; + +/** @typedef {import('postcss-selector-parser').Node} Node */ +/** @typedef {import('postcss-selector-parser').Selector} Selector */ + +/** + * @param {Node} node + * @returns {boolean} + */ +const isPseudoClass = (node) => + buggyIsPseudoClass(node) && + node.value.toLowerCase() !== ':first-line' && + node.value.toLowerCase() !== ':first-letter'; + +/** + * @param {Node} node + * @returns {boolean} + */ +const isSimpleSelector = (node) => + isPseudoClass(node) || + isAttribute(node) || + isClassName(node) || + isUniversal(node) || + isIdentifier(node) || + isTag(node); + +/** + * @param {Node} node + * @returns {boolean} + */ +const isNot = (node) => + isPseudoClass(node) && node.value !== undefined && node.value.toLowerCase() === ':not'; + +/** + * @param {Selector[]} list + * @returns {boolean} + */ +const isSimple = (list) => { + if (list.length > 1) return false; + + const [first, second] = list[0].nodes; // list is never empty + + if (!first) return true; + + if (second) return false; + + return isSimpleSelector(first) && !isNot(first); +}; + +/** @type {import('stylelint').Rule} */ +const rule = (primary, _, context) => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primary, + possible: ['simple', 'complex'], + }); + + if (!validOptions) { + return; + } + + root.walkRules((ruleNode) => { + if (!isStandardSyntaxRule(ruleNode)) { + return; + } + + const selector = ruleNode.selector; + + /*const fixedSelector = */ parseSelector(selector, result, ruleNode, (container) => { + container.walkPseudos((pseudo) => { + if (!isNot(pseudo)) return; + + if (primary === 'complex') { + const prev = pseudo.prev(); + const hasConsecutiveNot = prev && isNot(prev); + + if (!hasConsecutiveNot) return; + + if (context.fix) { + /* TODO */ + return; + } + } else { + if (isSimple(pseudo.nodes)) return; + + if (context.fix) { + /* TODO */ + return; + } + } + + report({ + message: messages.expected(primary), + node: ruleNode, + index: pseudo.sourceIndex, + result, + ruleName, + }); + }); + }); + }); + }; +}; + +rule.ruleName = ruleName; +rule.messages = messages; +rule.meta = meta; +module.exports = rule;