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..964c9a3e85 --- /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(input, select) { color: pink; } +``` + + +```css +:not(p.foo) { color: pink; } +``` + +The following patterns are _not_ considered problems: + + +```css +:not(input):not(select) { color: pink; } +``` + + +```css +:not(div) { color: pink; } +``` + +### `"complex"` + +The following pattern is considered a problem: + + +```css +:not(input):not(select) { color: pink; } +``` + +The following patterns are _not_ considered problems: + + +```css +:not(input, select) { color: pink; } +``` + + +```css +:not(p.foo) { color: pink; } +``` + + +```css +:not(div).bar:not(:empty) { color: pink; } +``` 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..04c50bf8bd --- /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( div ) {}', + }, + { + code: ':nOt(div) {}', + }, + { + 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(input, select) {}', + fixed: ':not(input):not(select) {}', + message: messages.expected('simple'), + line: 1, + column: 1, + }, + { + code: ':not(p.foo) {}', + fixed: '' /* how to fix this one? */, + message: messages.expected('simple'), + line: 1, + column: 1, + }, + ], +}); + +testRule({ + ruleName, + config: ['complex'], + fix: false, + + accept: [ + { + code: ':not():after {}', + }, + { + code: ':not(input, select) {}', + }, + { + code: ':not(p.foo) {}', + }, + { + code: ':not(div).bar:not(:empty) {}', + }, + ], + + reject: [ + { + code: ':not(.bar):not(span) {}', + fixed: ':not(.bar, span) {} {}', + 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..283dd3f335 --- /dev/null +++ b/lib/rules/selector-not-notation/index.js @@ -0,0 +1,117 @@ +'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, + 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 */ + +/** + * @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.toLowerCase() === ':not'; + +/** + * @param {import('postcss-selector-parser').Pseudo} node + * @returns {boolean} + */ +const isSimple = ({ nodes: 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)) 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;