All files / plugins no-unused-vars.js

100% Statements 51/51
94.74% Branches 18/19
100% Functions 11/11
100% Lines 50/50

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 973x 3x 3x 3x 3x   3x   3x 3x 3x   3x 2x     3x   3x 16x 1x     15x   15x 15x 15x   15x 15x 20x 4x 2x             2x 2x               15x 15x 3x 3x 3x   3x 3x 15x 15x 21x 21x 21x 6x   15x       3x   3x 6x 6x 3x   3x       3x             12x       2x       8x    
const TapMap = require('tap-map')
const globby = require('globby')
const matchAll = require('string.prototype.matchall')
const stylelint = require('stylelint')
const {readFileSync} = require('fs')
 
const ruleName = 'primer/no-unused-vars'
 
const cwd = process.cwd()
const COLON = ':'
const SCSS_VARIABLE_PATTERN = /(\$[-\w]+)/g
 
const messages = stylelint.utils.ruleMessages(ruleName, {
  rejected: name => `The variable "${name}" is not referenced.`
})
 
const cache = new TapMap()
 
module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}) => {
  if (!enabled) {
    return noop
  }
 
  const {files = ['**/*.scss', '!node_modules'], variablePattern = SCSS_VARIABLE_PATTERN, verbose = false} = options
  // eslint-disable-next-line no-console
  const log = verbose ? (...args) => console.warn(...args) : noop
  const cacheOptions = {files, variablePattern, cwd}
  const {refs} = getCachedVariables(cacheOptions, log)
 
  return (root, result) => {
    root.walkDecls(decl => {
      for (const [name] of matchAll(decl.prop, variablePattern)) {
        if (!refs.has(name)) {
          stylelint.utils.report({
            message: messages.rejected(name),
            node: decl,
            result,
            ruleName
          })
        } else {
          const path = stripCwd(decl.source.input.file)
          log(`${name} declared in ${path} ref'd in ${pluralize(refs.get(name).size, 'file')}`)
        }
      }
    })
  }
})
 
function getCachedVariables(options, log) {
  const key = JSON.stringify(options)
  return cache.tap(key, () => {
    const {files, variablePattern} = options
    const decs = new TapMap()
    const refs = new TapMap()
 
    log(`Looking for variables in ${files} ...`)
    for (const file of globby.sync(files)) {
      const css = readFileSync(file, 'utf8')
      for (const match of matchAll(css, variablePattern)) {
        const after = css.substr(match.index + match[0].length)
        const name = match[0]
        if (after.startsWith(COLON)) {
          decs.tap(name, set).add(file)
        } else {
          refs.tap(name, set).add(file)
        }
      }
    }
    log(`Found ${decs.size} declarations, ${pluralize(refs.size, 'reference')}.`)
 
    for (const [name, files] of decs.entries()) {
      const fileRefs = refs.get(name)
      if (fileRefs) {
        log(`variable "${name}" declared in ${pluralize(files.size, 'file')}, ref'd in ${fileRefs.size}`)
      } else {
        log(`[!] variable "${name}" declared in ${Array.from(files)[0]} is not referenced`)
      }
    }
 
    return {decs, refs}
  })
}
 
function noop() {}
 
function set() {
  return new Set()
}
 
function stripCwd(path) {
  return path.startsWith(cwd) ? path.substr(cwd.length + 1) : path
}
 
function pluralize(num, str, plural = `${str}s`) {
  return num === 1 ? `${num} ${str}` : `${num} ${plural}`
}