/
bodySelectors.js
137 lines (116 loc) · 3.57 KB
/
bodySelectors.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"use strict";
/**
*
* @param { import("css-what").AttributeSelector[] } expressions
* @returns {int}
*/
function getBodyIndex(expressions) {
let idx = 0;
// body.foo h1 -> 0
// .foo body -> 1
// html.css body -> 1
for (let i = 0; i < expressions.length; i++) {
switch (expressions[i].type) {
case "tag":
if (expressions[i].name === "body") {
return idx;
}
break;
case "child":
case "descendant":
idx++;
}
}
return -1;
}
/**
* @param { import("../lib/css-analyzer") } analyzer
*/
function rule(analyzer) {
const debug = require("debug")("analyze-css:bodySelectors");
analyzer.setMetric("redundantBodySelectors");
analyzer.on("selector", function (_, selector, expressions) {
const noExpressions = expressions.length;
// check more complex selectors only
if (noExpressions < 2) {
return;
}
const firstTag = expressions[0].type === "tag" && expressions[0].name;
// remove any non-class selectors
const firstHasClass =
expressions[0].type === "tag"
? // h1.foo
expressions[1].type === "attribute" && expressions[1].name === "class"
: // .foo
expressions[0].type === "attribute" &&
expressions[0].name === "class";
// body > .foo
// {"type":"child"}
const isDescendantCombinator =
expressions
.filter((item) => {
return !["tag", "attribute", "pseudo"].includes(item.type);
})
.map((item) => {
return item.type;
})
.indexOf("child") === 0;
// there only a single descendant / child selector
// e.g. "body > foo" or "html h1"
const isShortExpression =
expressions.filter((item) => {
return ["child", "descendant"].includes(item.type);
}).length === 1;
let isRedundant = true; // always expect the worst ;)
// first, let's find the body tag selector in the expression
const bodyIndex = getBodyIndex(expressions);
debug("selector: %s %j", selector, {
firstTag,
firstHasClass,
isDescendantCombinator,
isShortExpression,
bodyIndex,
});
// body selector not found - skip the rules that follow
if (bodyIndex < 0) {
return;
}
// matches "html > body"
// {"type":"tag","name":"html","namespace":null}
// {"type":"child"}
// {"type":"tag","name":"body","namespace":null}
//
// matches "html.modal-popup-mode body" (issue #44)
// {"type":"tag","name":"html","namespace":null}
// {"type":"attribute","name":"class","action":"element","value":"modal-popup-mode","namespace":null,"ignoreCase":false}
// {"type":"descendant"}
// {"type":"tag","name":"body","namespace":null}
if (
firstTag === "html" &&
bodyIndex === 1 &&
(isDescendantCombinator || isShortExpression)
) {
isRedundant = false;
}
// matches "body > .bar" (issue #82)
else if (bodyIndex === 0 && isDescendantCombinator) {
isRedundant = false;
}
// matches "body.foo ul li a"
else if (bodyIndex === 0 && firstHasClass) {
isRedundant = false;
}
// matches ".has-modal > body" (issue #49)
else if (firstHasClass && bodyIndex === 1 && isDescendantCombinator) {
isRedundant = false;
}
// report he redundant body selector
if (isRedundant) {
debug("selector %s - is redundant", selector);
analyzer.incrMetric("redundantBodySelectors");
analyzer.addOffender("redundantBodySelectors", selector);
}
});
}
rule.description = "Reports redundant body selectors";
module.exports = rule;