Skip to content

Commit

Permalink
Add support for all logical combinations (:matches, :has) in selector…
Browse files Browse the repository at this point in the history
…-* (#4179)
  • Loading branch information
vankop authored and hudochenkov committed Jul 31, 2019
1 parent 7a0c036 commit b658f41
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 16 deletions.
5 changes: 3 additions & 2 deletions lib/rules/selector-max-attribute/index.js
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions 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");
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions 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");
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions 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");
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions 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");
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions lib/rules/selector-max-type/index.js
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions 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");
Expand Down Expand Up @@ -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);
}

Expand Down
60 changes: 59 additions & 1 deletion lib/rules/selector-pseudo-class-blacklist/__tests__/index.js
Expand Up @@ -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: [
Expand Down Expand Up @@ -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
}
]
});
Expand Down Expand Up @@ -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"],
Expand Down
28 changes: 28 additions & 0 deletions lib/rules/selector-pseudo-class-case/__tests__/index.js
Expand Up @@ -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; }",
Expand Down
12 changes: 12 additions & 0 deletions lib/rules/selector-pseudo-class-no-unknown/__tests__/index.js
Expand Up @@ -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) { }"
},
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"),
Expand Down
Expand Up @@ -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 ) ) { }",
Expand Down Expand Up @@ -197,6 +204,9 @@ testRule(rule, {
},
{
code: "a:hover:not(.active) { }"
},
{
code: ":matches(a, ul, :has(h1, h2)) { }"
}
],

Expand Down
5 changes: 4 additions & 1 deletion lib/rules/selector-pseudo-class-whitelist/__tests__/index.js
Expand Up @@ -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: [
Expand All @@ -21,6 +21,9 @@ testRule(rule, {
{
code: ":root {}"
},
{
code: "a:has(#id) {}"
},
{
code: "a:hover, a:nth-child(5) {}"
},
Expand Down
39 changes: 39 additions & 0 deletions 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);
});
});
});
25 changes: 25 additions & 0 deletions 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;
};

0 comments on commit b658f41

Please sign in to comment.