UNPKG

3.4 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * @param { import("css-what").Selector[] } expressions
5 * @returns { number }
6 */
7function getExpressionsLength(expressions) {
8 // body -> 1
9 // ul li -> 2
10 // ol:lang(or) li -> 2
11 // .class + foo a -> 3
12 return (
13 expressions.filter((item) => {
14 return ["child", "descendant", "adjacent"].includes(item.type);
15 }).length + 1
16 );
17}
18
19/**
20 * Report redundant child selectors, e.g.:
21 *
22 * ul li
23 * ul > li
24 * table > tr
25 * tr td
26 *
27 * @param { import("../lib/css-analyzer") } analyzer
28 */
29function rule(analyzer) {
30 // definition of redundant child nodes selectors (see #51 for the initial idea):
31 // ul li
32 // ol li
33 // table tr
34 // table th
35 const redundantChildSelectors = {
36 ul: ["li"],
37 ol: ["li"],
38 select: ["option"],
39 table: ["tr", "th"], // e.g. table can not be followed by any of tr / th
40 tr: ["td", "th"],
41 };
42
43 analyzer.setMetric("redundantChildNodesSelectors");
44
45 analyzer.on("selector", (_, selector, expressions) => {
46 // there only a single descendant / child selector
47 // e.g. "body > foo" or "html h1"
48 //
49 // check more complex selectors only
50 if (getExpressionsLength(expressions) < 3) {
51 return;
52 }
53
54 Object.keys(redundantChildSelectors).forEach((tagName) => {
55 // find the tagName in our selector
56 const tagInSelectorIndex = expressions
57 .map((expr) => expr.type == "tag" && expr.name)
58 .indexOf(tagName);
59
60 // tag not found in the selector
61 if (tagInSelectorIndex < 0) {
62 return;
63 }
64
65 // converts "ul#foo > li.test" selector into [{tag: 'ul'}, {combinator:'child'}, {tag: 'li'}] list
66 const selectorNodeNames = expressions
67 .filter((expr) =>
68 [
69 "tag",
70 "descendant" /* */,
71 "child" /* > */,
72 "adjacent" /* + */,
73 ].includes(expr.type)
74 )
75 .map((expr) =>
76 expr.name ? { tag: expr.name } : { combinator: expr.type }
77 );
78
79 // console.log(selector, expressions, selectorNodeNames);
80
81 const tagIndex = selectorNodeNames
82 .map((item) => item.tag)
83 .indexOf(tagName);
84
85 const nextTagInSelector = selectorNodeNames[tagIndex + 2]?.tag;
86 const nextCombinator = selectorNodeNames[tagIndex + 1]?.combinator;
87 const previousCombinator = selectorNodeNames[tagIndex - 1]?.combinator;
88
89 // our tag is not followed by the tag listed in redundantChildSelectors
90 const followedByRedundantTag =
91 redundantChildSelectors[tagName].includes(nextTagInSelector);
92 if (!followedByRedundantTag) {
93 return;
94 }
95
96 // ignore cases like "article > ul li"
97 if (previousCombinator === "child") {
98 return;
99 }
100
101 // console.log(
102 // tagName, {selector, expressions}, selectorNodeNames,
103 // {tagIndex, prreviousTagInSelector, previousCombinator, nextTagInSelector, nextCombinator, followedByRedundantTag}
104 // );
105
106 // only the following combinator can match:
107 // ul li
108 // ul > li
109 if (
110 followedByRedundantTag &&
111 ["descendant", "child"].includes(nextCombinator)
112 ) {
113 analyzer.incrMetric("redundantChildNodesSelectors");
114 analyzer.addOffender("redundantChildNodesSelectors", selector);
115 }
116 });
117 });
118}
119
120rule.description = "Reports redundant child nodes selectors";
121module.exports = rule;