UNPKG

4.09 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const findAtRuleContext = require('../../utils/findAtRuleContext');
5const isKeyframeRule = require('../../utils/isKeyframeRule');
6const nodeContextLookup = require('../../utils/nodeContextLookup');
7const normalizeSelector = require('normalize-selector');
8const report = require('../../utils/report');
9const resolvedNestedSelector = require('postcss-resolve-nested-selector');
10const ruleMessages = require('../../utils/ruleMessages');
11const validateOptions = require('../../utils/validateOptions');
12
13const ruleName = 'no-duplicate-selectors';
14
15const messages = ruleMessages(ruleName, {
16 rejected: (selector, firstDuplicateLine) =>
17 `Unexpected duplicate selector "${selector}", first used at line ${firstDuplicateLine}`,
18});
19
20function rule(actual, options) {
21 return (root, result) => {
22 const validOptions = validateOptions(
23 result,
24 ruleName,
25 { actual },
26 {
27 actual: options,
28 possible: {
29 disallowInList: _.isBoolean,
30 },
31 optional: true,
32 },
33 );
34
35 if (!validOptions) {
36 return;
37 }
38
39 // The top level of this map will be rule sources.
40 // Each source maps to another map, which maps rule parents to a set of selectors.
41 // This ensures that selectors are only checked against selectors
42 // from other rules that share the same parent and the same source.
43 const selectorContextLookup = nodeContextLookup();
44
45 root.walkRules((rule) => {
46 if (isKeyframeRule(rule)) {
47 return;
48 }
49
50 const contextSelectorSet = selectorContextLookup.getContext(rule, findAtRuleContext(rule));
51 const resolvedSelectors = rule.selectors.reduce((result, selector) => {
52 return _.union(result, resolvedNestedSelector(selector, rule));
53 }, []);
54
55 const normalizedSelectorList = resolvedSelectors.map(normalizeSelector);
56 const selectorLine = rule.source.start.line;
57
58 // Complain if the same selector list occurs twice
59
60 // Sort the selectors list so that the order of the constituents
61 // doesn't matter
62 const sortedSelectorList = normalizedSelectorList.slice().sort().join(',');
63
64 const checkPreviousDuplicationPosition = (
65 selectorList,
66 { shouldDisallowDuplicateInList },
67 ) => {
68 let duplicationPosition = null;
69
70 if (shouldDisallowDuplicateInList) {
71 // iterate throw Map for checking, was used this selector in a group selector
72 contextSelectorSet.forEach((selectorLine, selector) => {
73 if (selector.includes(selectorList)) {
74 duplicationPosition = selectorLine;
75 }
76 });
77 } else {
78 duplicationPosition = contextSelectorSet.get(selectorList);
79 }
80
81 return duplicationPosition;
82 };
83
84 const previousDuplicatePosition = checkPreviousDuplicationPosition(sortedSelectorList, {
85 shouldDisallowDuplicateInList: _.get(options, 'disallowInList'),
86 });
87
88 if (previousDuplicatePosition) {
89 // If the selector isn't nested we can use its raw value; otherwise,
90 // we have to approximate something for the message -- which is close enough
91 const isNestedSelector = resolvedSelectors.join(',') !== rule.selectors.join(',');
92 const selectorForMessage = isNestedSelector ? resolvedSelectors.join(', ') : rule.selector;
93
94 return report({
95 result,
96 ruleName,
97 node: rule,
98 message: messages.rejected(selectorForMessage, previousDuplicatePosition),
99 });
100 }
101
102 const presentedSelectors = new Set();
103 const reportedSelectors = new Set();
104
105 // Or complain if one selector list contains the same selector more than one
106 rule.selectors.forEach((selector) => {
107 const normalized = normalizeSelector(selector);
108
109 if (presentedSelectors.has(normalized)) {
110 if (reportedSelectors.has(normalized)) {
111 return;
112 }
113
114 report({
115 result,
116 ruleName,
117 node: rule,
118 message: messages.rejected(selector, selectorLine),
119 });
120 reportedSelectors.add(normalized);
121 } else {
122 presentedSelectors.add(normalized);
123 }
124 });
125
126 contextSelectorSet.set(sortedSelectorList, selectorLine);
127 });
128 };
129}
130
131rule.ruleName = ruleName;
132rule.messages = messages;
133module.exports = rule;