You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
4.4 KiB
149 lines
4.4 KiB
// @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((ruleNode) => { |
|
if (isKeyframeRule(ruleNode)) { |
|
return; |
|
} |
|
|
|
const contextSelectorSet = selectorContextLookup.getContext( |
|
ruleNode, |
|
findAtRuleContext(ruleNode), |
|
); |
|
const resolvedSelectors = ruleNode.selectors.reduce((selectors, selector) => { |
|
return _.union(selectors, resolvedNestedSelector(selector, ruleNode)); |
|
}, []); |
|
|
|
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 = ruleNode.source.start.line; |
|
|
|
// Complain if the same selector list occurs twice |
|
|
|
let previousDuplicatePosition; |
|
// When `disallowInList` is true, we must parse `sortedSelectorList` into |
|
// list items. |
|
const selectorListParsed = []; |
|
|
|
if (shouldDisallowDuplicateInList) { |
|
parseSelector(sortedSelectorList, result, ruleNode, (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(',') !== ruleNode.selectors.join(','); |
|
const selectorForMessage = isNestedSelector |
|
? resolvedSelectors.join(', ') |
|
: ruleNode.selector; |
|
|
|
return report({ |
|
result, |
|
ruleName, |
|
node: ruleNode, |
|
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 |
|
ruleNode.selectors.forEach((selector) => { |
|
const normalized = normalizeSelector(selector); |
|
|
|
if (presentedSelectors.has(normalized)) { |
|
if (reportedSelectors.has(normalized)) { |
|
return; |
|
} |
|
|
|
report({ |
|
result, |
|
ruleName, |
|
node: ruleNode, |
|
message: messages.rejected(selector, selectorLine), |
|
}); |
|
reportedSelectors.add(normalized); |
|
} else { |
|
presentedSelectors.add(normalized); |
|
} |
|
}); |
|
|
|
if (shouldDisallowDuplicateInList) { |
|
for (const 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;
|
|
|