UNPKG

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