diff --git a/lib/rules/selector-max-attribute/index.js b/lib/rules/selector-max-attribute/index.js index ff4edf904a..d69db7eb47 100644 --- a/lib/rules/selector-max-attribute/index.js +++ b/lib/rules/selector-max-attribute/index.js @@ -2,6 +2,7 @@ const _ = require("lodash"); const hasUnresolvedNestedSelector = require("../../utils/hasUnresolvedNestedSelector"); +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const optionsMatches = require("../../utils/optionsMatches"); @@ -48,8 +49,8 @@ function rule(max, options) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, ruleNode); } diff --git a/lib/rules/selector-max-class/index.js b/lib/rules/selector-max-class/index.js index adc8492a91..4a58d203e5 100644 --- a/lib/rules/selector-max-class/index.js +++ b/lib/rules/selector-max-class/index.js @@ -1,5 +1,6 @@ "use strict"; +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const parseSelector = require("../../utils/parseSelector"); @@ -34,8 +35,8 @@ function rule(max) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, ruleNode); } diff --git a/lib/rules/selector-max-compound-selectors/index.js b/lib/rules/selector-max-compound-selectors/index.js index 383ee0ff40..9f845755b5 100644 --- a/lib/rules/selector-max-compound-selectors/index.js +++ b/lib/rules/selector-max-compound-selectors/index.js @@ -1,5 +1,6 @@ "use strict"; +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const parseSelector = require("../../utils/parseSelector"); @@ -37,8 +38,8 @@ const rule = function(max) { let compoundCount = 1; selectorNode.each(childNode => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, rule); } diff --git a/lib/rules/selector-max-id/index.js b/lib/rules/selector-max-id/index.js index 161b1a9787..a98268018c 100644 --- a/lib/rules/selector-max-id/index.js +++ b/lib/rules/selector-max-id/index.js @@ -1,5 +1,6 @@ "use strict"; +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const parseSelector = require("../../utils/parseSelector"); @@ -34,8 +35,8 @@ function rule(max) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, ruleNode); } diff --git a/lib/rules/selector-max-pseudo-class/index.js b/lib/rules/selector-max-pseudo-class/index.js index 98c6ea5f81..05b9fcef45 100644 --- a/lib/rules/selector-max-pseudo-class/index.js +++ b/lib/rules/selector-max-pseudo-class/index.js @@ -1,5 +1,6 @@ "use strict"; +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const keywordSets = require("../../reference/keywordSets"); @@ -35,8 +36,8 @@ function rule(max) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, ruleNode); } diff --git a/lib/rules/selector-max-type/index.js b/lib/rules/selector-max-type/index.js index eb6e74905a..5061dd587c 100644 --- a/lib/rules/selector-max-type/index.js +++ b/lib/rules/selector-max-type/index.js @@ -3,6 +3,7 @@ const _ = require("lodash"); const hasUnresolvedNestedSelector = require("../../utils/hasUnresolvedNestedSelector"); const isKeyframeSelector = require("../../utils/isKeyframeSelector"); +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isOnlyWhitespace = require("../../utils/isOnlyWhitespace"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); @@ -54,8 +55,8 @@ function rule(max, options) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, ruleNode); } diff --git a/lib/rules/selector-max-universal/index.js b/lib/rules/selector-max-universal/index.js index eb7d78b731..a332e2e85d 100644 --- a/lib/rules/selector-max-universal/index.js +++ b/lib/rules/selector-max-universal/index.js @@ -1,6 +1,7 @@ "use strict"; const hasUnresolvedNestedSelector = require("../../utils/hasUnresolvedNestedSelector"); +const isLogicalCombination = require("../../utils/isLogicalCombination"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const parseSelector = require("../../utils/parseSelector"); @@ -36,8 +37,8 @@ function rule(max) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and :not() - if (childNode.type === "selector" || childNode.value === ":not") { + // Only traverse inside actual selectors and logical combinations + if (childNode.type === "selector" || isLogicalCombination(childNode)) { checkSelector(childNode, ruleNode); } diff --git a/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js b/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js index bed8141e6b..1525970548 100644 --- a/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js +++ b/lib/rules/selector-pseudo-class-blacklist/__tests__/index.js @@ -5,7 +5,14 @@ const { messages, ruleName } = rule; testRule(rule, { ruleName, - config: ["focus", "global", "input-placeholder", "not", "nth-last-child"], + config: [ + "focus", + "global", + "input-placeholder", + "not", + "nth-last-child", + "has" + ], skipBasicChecks: true, accept: [ @@ -87,6 +94,12 @@ testRule(rule, { message: messages.rejected("not"), line: 1, column: 2 + }, + { + code: "a:has(> img) {}", + message: messages.rejected("has"), + line: 1, + column: 2 } ] }); @@ -145,6 +158,51 @@ testRule(rule, { ] }); +testRule(rule, { + 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):not(span) {}", + message: messages.rejected("not"), + line: 1, + column: 5 + }, + { + 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(rule, { ruleName, config: ["variable"], diff --git a/lib/rules/selector-pseudo-class-case/__tests__/index.js b/lib/rules/selector-pseudo-class-case/__tests__/index.js index e457879a31..94b55845b0 100644 --- a/lib/rules/selector-pseudo-class-case/__tests__/index.js +++ b/lib/rules/selector-pseudo-class-case/__tests__/index.js @@ -204,6 +204,34 @@ testRule(rule, { line: 1, column: 3 }, + { + code: ":matcheS(a, .foo) { }", + fixed: ":matches(a, .foo) { }", + message: messages.expected(":matcheS", ":matches"), + line: 1, + column: 1 + }, + { + code: ":Matches(a, .foo) { }", + fixed: ":matches(a, .foo) { }", + message: messages.expected(":Matches", ":matches"), + line: 1, + column: 1 + }, + { + code: "a:hAs(> img) { }", + fixed: "a:has(> img) { }", + message: messages.expected(":hAs", ":has"), + line: 1, + column: 2 + }, + { + code: "a:HAS(> img) {\n}", + fixed: "a:has(> img) {\n}", + message: messages.expected(":HAS", ":has"), + line: 1, + column: 2 + }, { code: ":Root { background: #ff0000; }", fixed: ":root { background: #ff0000; }", diff --git a/lib/rules/selector-pseudo-class-no-unknown/__tests__/index.js b/lib/rules/selector-pseudo-class-no-unknown/__tests__/index.js index f50a613044..69901bd5fb 100644 --- a/lib/rules/selector-pseudo-class-no-unknown/__tests__/index.js +++ b/lib/rules/selector-pseudo-class-no-unknown/__tests__/index.js @@ -36,6 +36,9 @@ testRule(rule, { { code: ":matches(section, article, aside, nav) h1 { }" }, + { + code: "a:has(> img) { }" + }, { code: "section:has(h1, h2, h3, h4, h5, h6) { }" }, @@ -75,6 +78,9 @@ testRule(rule, { { code: "@page foo:left { }" }, + { + code: "body:not(div):not(span) {}" + }, { code: ":root { --foo: 1px; }", description: "custom property in root" @@ -137,6 +143,12 @@ testRule(rule, { line: 1, column: 2 }, + { + code: "body:not(div):noot(span) {}", + message: messages.rejected(":noot"), + line: 1, + column: 14 + }, { code: "a:unknown::before { }", message: messages.rejected(":unknown"), diff --git a/lib/rules/selector-pseudo-class-parentheses-space-inside/__tests__/index.js b/lib/rules/selector-pseudo-class-parentheses-space-inside/__tests__/index.js index 3dc503188d..db683d3477 100644 --- a/lib/rules/selector-pseudo-class-parentheses-space-inside/__tests__/index.js +++ b/lib/rules/selector-pseudo-class-parentheses-space-inside/__tests__/index.js @@ -105,6 +105,13 @@ testRule(rule, { line: 1, column: 19 }, + { + code: ":matches( a, ul, :has(h1, h2 ) ) { }", + fixed: ":matches( a, ul, :has( h1, h2 ) ) { }", + message: messages.expectedOpening, + line: 1, + column: 23 + }, { code: "section:not( :has( h1, h2) ) { }", fixed: "section:not( :has( h1, h2 ) ) { }", @@ -197,6 +204,9 @@ testRule(rule, { }, { code: "a:hover:not(.active) { }" + }, + { + code: ":matches(a, ul, :has(h1, h2)) { }" } ], diff --git a/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js b/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js index ff79db866f..7ec080f6bb 100644 --- a/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js +++ b/lib/rules/selector-pseudo-class-whitelist/__tests__/index.js @@ -5,7 +5,7 @@ const { messages, ruleName } = rule; testRule(rule, { ruleName, - config: ["hover", "nth-child", "root", "placeholder"], + config: ["hover", "nth-child", "root", "placeholder", "has"], skipBasicChecks: true, accept: [ @@ -21,6 +21,9 @@ testRule(rule, { { code: ":root {}" }, + { + code: "a:has(#id) {}" + }, { code: "a:hover, a:nth-child(5) {}" }, diff --git a/lib/utils/__tests__/isLogicalCombination.test.js b/lib/utils/__tests__/isLogicalCombination.test.js new file mode 100644 index 0000000000..fe5d73aee5 --- /dev/null +++ b/lib/utils/__tests__/isLogicalCombination.test.js @@ -0,0 +1,39 @@ +"use strict"; + +const isLogicalCombination = require("../isLogicalCombination"); +const parseSelector = require("postcss-selector-parser"); +const postcss = require("postcss"); + +function selector(css, cb) { + postcss.parse(css).walkRules(rule => { + parseSelector(selectorAST => { + selectorAST.walkPseudos(cb); + }).processSync(rule.selector); + }); +} + +describe("isLogicalCombination", () => { + it("hover pseudo class is NOT logical combination", () => { + selector("a:hover {}", selector => { + expect(isLogicalCombination(selector)).toBe(false); + }); + }); + + it("not pseudo class is logical combination", () => { + selector("a:not(.foo) {}", selector => { + expect(isLogicalCombination(selector)).toBe(true); + }); + }); + + it("has pseudo class is logical combination", () => { + selector("a:has(.foo) {}", selector => { + expect(isLogicalCombination(selector)).toBe(true); + }); + }); + + it("matches pseudo class is logical combination", () => { + selector("a:matches(.foo) {}", selector => { + expect(isLogicalCombination(selector)).toBe(true); + }); + }); +}); diff --git a/lib/utils/isLogicalCombination.js b/lib/utils/isLogicalCombination.js new file mode 100644 index 0000000000..9f5e5123b2 --- /dev/null +++ b/lib/utils/isLogicalCombination.js @@ -0,0 +1,25 @@ +/* @flow */ +"use strict"; + +/** + * Check whether a node is logical combination (`:not`, `:has`, `:matches`) + * + * @param {Node} postcss-selector-parser node (of type pseudo) + * @return {boolean} If `true`, the combination is logical + */ +module.exports = function isLogicalCombination( + node /*: Object*/ +) /*: boolean*/ { + if (node.type === "pseudo") { + switch (node.value) { + case ":not": + case ":has": + case ":matches": + return true; + default: + return false; + } + } + + return false; +};