All files / plugins no-override.js

100% Statements 53/53
100% Branches 31/31
100% Functions 13/13
100% Lines 52/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 1212x 2x   2x 2x 2x 2x   2x 14x 1x     13x     13x     7x 4x       13x 13x           13x 13x   13x 16x 5x   11x 11x 11x 13731x 13731x 13624x         13x   3x 3x 3x 3x       13x 13x 2x 1x   2x           13x 3x             13x 15x 15x 6x 5x 2x                   13x 13x 4x 1x                         13744x 13744x       6x       6x        
const stylelint = require('stylelint')
const {requirePrimerFile} = require('./lib/primer')
 
const ruleName = 'primer/no-override'
const CLASS_PATTERN = /(\.[-\w]+)/
const CLASS_PATTERN_ALL = new RegExp(CLASS_PATTERN, 'g')
const CLASS_PATTERN_ONLY = /^\.[-\w]+(:{1,2}[-\w]+)?$/
 
module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}) => {
  if (!enabled) {
    return noop
  }
 
  const {bundles = ['utilities'], ignoreSelectors = []} = options
 
  const isSelectorIgnored =
    typeof ignoreSelectors === 'function'
      ? ignoreSelectors
      : selector => {
          return ignoreSelectors.some(pattern => {
            return pattern instanceof RegExp ? pattern.test(selector) : selector.includes(pattern)
          })
        }
 
  const primerMeta = requirePrimerFile('dist/meta.json')
  const availableBundles = Object.keys(primerMeta.bundles)
 
  // These map selectors to the bundle in which they're defined.
  // If there's no entry for a given selector, it means that it's not defined
  // in one of the *specified* bundles, since we're iterating over the list of
  // bundle names in the options.
  const immutableSelectors = new Map()
  const immutableClassSelectors = new Map()
 
  for (const bundle of bundles) {
    if (!availableBundles.includes(bundle)) {
      continue
    }
    const stats = requirePrimerFile(`dist/stats/${bundle}.json`)
    const selectors = stats.selectors.values
    for (const selector of selectors) {
      immutableSelectors.set(selector, bundle)
      for (const classSelector of getClassSelectors(selector)) {
        immutableClassSelectors.set(classSelector, bundle)
      }
    }
  }
 
  const messages = stylelint.utils.ruleMessages(ruleName, {
    rejected: ({rule, selector, bundle}) => {
      const definedIn = ` (defined in @primer/css/${bundle})`
      const ruleSelector = collapseWhitespace(rule.selector)
      const context = selector === rule.selector ? '' : ` in "${ruleSelector}"`
      return `"${collapseWhitespace(selector)}" should not be overridden${context}${definedIn}.`
    }
  })
 
  return (root, result) => {
    if (!Array.isArray(bundles) || bundles.some(bundle => !availableBundles.includes(bundle))) {
      const invalidBundles = Array.isArray(bundles)
        ? `"${bundles.filter(bundle => !availableBundles.includes(bundle)).join('", "')}"`
        : '(not an array)'
      result.warn(`The "bundles" option must be an array of valid bundles; got: ${invalidBundles}`, {
        stylelintType: 'invalidOption',
        stylelintReference: 'https://github.com/primer/stylelint-config-primer#options'
      })
    }
 
    const report = subject =>
      stylelint.utils.report({
        message: messages.rejected(subject),
        node: subject.rule,
        result,
        ruleName
      })
 
    root.walkRules(rule => {
      const {selector} = rule
      if (immutableSelectors.has(selector)) {
        if (isClassSelector(selector)) {
          if (!isSelectorIgnored(selector)) {
            return report({
              rule,
              bundle: immutableSelectors.get(selector),
              selector
            })
          }
        } else {
          // console.log(`not a class selector: "${selector}"`)
        }
      }
      for (const classSelector of getClassSelectors(selector)) {
        if (immutableClassSelectors.has(classSelector)) {
          if (!isSelectorIgnored(classSelector)) {
            return report({
              rule,
              bundle: immutableClassSelectors.get(classSelector),
              selector: classSelector
            })
          }
        }
      }
    })
  }
})
 
function getClassSelectors(selector) {
  const match = selector.match(CLASS_PATTERN_ALL)
  return match ? [...match] : []
}
 
function isClassSelector(selector) {
  return CLASS_PATTERN_ONLY.test(selector)
}
 
function collapseWhitespace(str) {
  return str.trim().replace(/\s+/g, ' ')
}
 
function noop() {}