-
-
Notifications
You must be signed in to change notification settings - Fork 929
/
index.js
148 lines (125 loc) · 4.47 KB
/
index.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
138
139
140
141
142
143
144
145
146
147
148
"use strict";
const _ = require("lodash");
const findAtRuleContext = require("../../utils/findAtRuleContext");
const isKeyframeRule = require("../../utils/isKeyframeRule");
const nodeContextLookup = require("../../utils/nodeContextLookup");
const normalizeSelector = require("normalize-selector");
const report = require("../../utils/report");
const resolvedNestedSelector = require("postcss-resolve-nested-selector");
const ruleMessages = require("../../utils/ruleMessages");
const validateOptions = require("../../utils/validateOptions");
const ruleName = "no-duplicate-selectors";
const messages = ruleMessages(ruleName, {
rejected: (selector, firstDuplicateLine) =>
`Unexpected duplicate selector "${selector}", first used at line ${firstDuplicateLine}`
});
const rule = function(actual, options) {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual },
{
actual: options,
possible: {
disallowInList: _.isBoolean
},
optional: true
}
);
if (!validOptions) {
return;
}
// The top level of this map will be rule sources.
// Each source maps to another map, which maps rule parents to a set of selectors.
// This ensures that selectors are only checked against selectors
// from other rules that share the same parent and the same source.
const selectorContextLookup = nodeContextLookup();
root.walkRules(rule => {
if (isKeyframeRule(rule)) {
return;
}
const contextSelectorSet = selectorContextLookup.getContext(
rule,
findAtRuleContext(rule)
);
const resolvedSelectors = rule.selectors.reduce((result, selector) => {
return _.union(result, resolvedNestedSelector(selector, rule));
}, []);
const normalizedSelectorList = resolvedSelectors.map(normalizeSelector);
const selectorLine = rule.source.start.line;
// Complain if the same selector list occurs twice
// Sort the selectors list so that the order of the constituents
// doesn't matter
const sortedSelectorList = normalizedSelectorList
.slice()
.sort()
.join(",");
const checkPreviousDuplicationPosition = (
selectorList,
{ shouldDisallowDuplicateInList }
) => {
let duplicationPosition = null;
if (shouldDisallowDuplicateInList) {
// iterate throw Map for checking, was used this selector in a group selector
contextSelectorSet.forEach((selectorLine, selector) => {
if (_.includes(selector, selectorList)) {
duplicationPosition = selectorLine;
}
});
} else {
duplicationPosition = contextSelectorSet.get(selectorList);
}
return duplicationPosition;
};
const previousDuplicatePosition = checkPreviousDuplicationPosition(
sortedSelectorList,
{
shouldDisallowDuplicateInList: _.get(options, "disallowInList")
}
);
if (previousDuplicatePosition) {
// If the selector isn't nested we can use its raw value; otherwise,
// we have to approximate something for the message -- which is close enough
const isNestedSelector =
resolvedSelectors.join(",") !== rule.selectors.join(",");
const selectorForMessage = isNestedSelector
? resolvedSelectors.join(", ")
: rule.selector;
return report({
result,
ruleName,
node: rule,
message: messages.rejected(
selectorForMessage,
previousDuplicatePosition
)
});
}
const presentedSelectors = new Set();
const reportedSelectors = new Set();
// Or complain if one selector list contains the same selector more than one
rule.selectors.forEach(selector => {
const normalized = normalizeSelector(selector);
if (presentedSelectors.has(normalized)) {
if (reportedSelectors.has(normalized)) {
return;
}
report({
result,
ruleName,
node: rule,
message: messages.rejected(selector, selectorLine)
});
reportedSelectors.add(normalized);
} else {
presentedSelectors.add(normalized);
}
});
contextSelectorSet.set(sortedSelectorList, selectorLine);
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;