-
-
Notifications
You must be signed in to change notification settings - Fork 929
/
index.js
144 lines (118 loc) · 4.3 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
// @ts-nocheck
'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 parseSelector = require('../../utils/parseSelector');
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}`,
});
function rule(actual, options) {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual },
{
actual: options,
possible: {
disallowInList: _.isBoolean,
},
optional: true,
},
);
if (!validOptions) {
return;
}
const shouldDisallowDuplicateInList = _.get(options, 'disallowInList');
// 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);
// Sort the selectors list so that the order of the constituents
// doesn't matter
const sortedSelectorList = normalizedSelectorList.slice().sort().join(',');
const selectorLine = rule.source.start.line;
// Complain if the same selector list occurs twice
let previousDuplicatePosition;
// When `disallowInList` is true, we must parse `sortedSelectorList` into
// list items.
let selectorListParsed = [];
if (shouldDisallowDuplicateInList) {
parseSelector(sortedSelectorList, result, rule, (selectors) => {
selectors.each((s) => {
const selector = String(s);
selectorListParsed.push(selector);
if (contextSelectorSet.get(selector)) {
previousDuplicatePosition = contextSelectorSet.get(selector);
}
});
});
} else {
previousDuplicatePosition = contextSelectorSet.get(sortedSelectorList);
}
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 once
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);
}
});
if (shouldDisallowDuplicateInList) {
for (let selector of selectorListParsed) {
// [selectorLine] will not really be accurate for multi-line
// selectors, such as "bar" in "foo,\nbar {}".
contextSelectorSet.set(selector, selectorLine);
}
} else {
contextSelectorSet.set(sortedSelectorList, selectorLine);
}
});
};
}
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;