UNPKG

3.33 kBJavaScriptView Raw
1"use strict";
2
3const Collection = require("../lib/collection"),
4 debug = require("debug")("analyze-css:duplicated"),
5 format = require("util").format;
6
7/**
8 * @param { import("../lib/css-analyzer") } analyzer
9 */
10function rule(analyzer) {
11 var selectors = new Collection(),
12 mediaQueryStack = [],
13 browserPrefixRegEx = /^-(moz|o|webkit|ms)-/;
14
15 analyzer.setMetric("duplicatedSelectors");
16 analyzer.setMetric("duplicatedProperties");
17
18 // handle nested media queries
19 analyzer.on("media", function (query) {
20 mediaQueryStack.push(query);
21 debug("push: %j", mediaQueryStack);
22 });
23
24 analyzer.on("mediaEnd", function (query) {
25 mediaQueryStack.pop(query);
26 debug("pop: %j", mediaQueryStack);
27 });
28
29 // register each rule's selectors
30 analyzer.on("rule", function (rule) {
31 selectors.push(
32 // @media foo
33 (mediaQueryStack.length > 0
34 ? "@media " + mediaQueryStack.join(" @media ") + " "
35 : "") +
36 // #foo
37 rule.selectors.join(", ")
38 );
39 });
40
41 // find duplicated properties (issue #60)
42 analyzer.on("rule", function (rule) {
43 var propertiesHash = {};
44
45 /* istanbul ignore else */
46 if (rule.declarations) {
47 rule.declarations.forEach(function (declaration) {
48 var propertyName;
49
50 if (declaration.type === "declaration") {
51 propertyName = declaration.property;
52
53 // skip properties that require browser prefixes
54 // background-image:-moz-linear-gradient(...)
55 // background-image:-webkit-gradient(...)
56 if (browserPrefixRegEx.test(declaration.value) === true) {
57 return;
58 }
59
60 // property was already used in the current selector - report it
61 if (propertiesHash[propertyName] === true) {
62 // report the position of the offending property
63 analyzer.setCurrentPosition(declaration.position);
64
65 analyzer.incrMetric("duplicatedProperties");
66 analyzer.addOffender(
67 "duplicatedProperties",
68 format(
69 "%s {%s: %s}",
70 rule.selectors.join(", "),
71 declaration.property,
72 declaration.value
73 )
74 );
75 } else {
76 // mark given property as defined in the context of the current selector
77 propertiesHash[propertyName] = true;
78 }
79 }
80 });
81 }
82 });
83
84 // special handling for @font-face (#52)
85 // include URL when detecting duplicates
86 analyzer.on("font-face", function (rule) {
87 rule.declarations.forEach(function (declaration) {
88 if (declaration.property === "src") {
89 selectors.push("@font-face src: " + declaration.value);
90
91 debug(
92 "special handling for @font-face, provided src: %s",
93 declaration.value
94 );
95 return false;
96 }
97 });
98 });
99
100 analyzer.on("report", function () {
101 analyzer.setCurrentPosition(undefined);
102
103 selectors.sort().forEach((selector, cnt) => {
104 if (cnt > 1) {
105 analyzer.incrMetric("duplicatedSelectors");
106 analyzer.addOffender(
107 "duplicatedSelectors",
108 format("%s (%d times)", selector, cnt)
109 );
110 }
111 });
112 });
113}
114
115rule.description = "Reports duplicated CSS selectors and properties";
116module.exports = rule;