All files / plugins no-undefined-vars.js

93.22% Statements 55/59
58.82% Branches 10/17
91.67% Functions 11/12
94.74% Lines 54/57

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 1123x 3x 3x 3x 3x   3x 3x 12x       3x     3x       3x   3x 13x       13x   13x 13x     13x   13x   15x 6x                 13x 2x 2x 2x       2x 6x   2x 4x 4x           13x 11x 11x     11x     11x 11x             3x 3x     13x 13x 1x   1x 1x 1x 3x 3x 3x 286x 286x   3x 2x 2x       1x           3x 3x  
const fs = require('fs')
const stylelint = require('stylelint')
const matchAll = require('string.prototype.matchall')
const globby = require('globby')
const TapMap = require('tap-map')
 
const ruleName = 'primer/no-undefined-vars'
const messages = stylelint.utils.ruleMessages(ruleName, {
  rejected: varName => `${varName} is not defined`
})
 
// Match CSS variable definitions (e.g. --color-text-primary:)
const variableDefinitionRegex = /^\s*(--[\w|-]+):/gm
 
// Match CSS variables defined with the color-mode-var mixin (e.g. @include color-mode-var(my-feature, ...))
const colorModeVariableDefinitionRegex = /^\s*@include\s+color-mode-var\s*\(\s*['"]?([^'",]+)['"]?/gm
 
// Match CSS variable references (e.g var(--color-text-primary))
// eslint-disable-next-line no-useless-escape
const variableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g
 
module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}) => {
  Iif (!enabled) {
    return noop
  }
 
  const {files = ['**/*.scss', '!node_modules'], verbose = false} = options
  // eslint-disable-next-line no-console
  const log = verbose ? (...args) => console.warn(...args) : noop
  const definedVariables = getDefinedVariables(files, log)
 
  // Keep track of declarations we've already seen
  const seen = new WeakMap()
 
  return (root, result) => {
    function checkVariable(variableName, node) {
      if (!definedVariables.has(variableName)) {
        stylelint.utils.report({
          message: messages.rejected(variableName),
          node,
          result,
          ruleName
        })
      }
    }
 
    root.walkAtRules(rule => {
      Eif (rule.name === 'include' && rule.params.startsWith('color-mode-var')) {
        const innerMatch = rule.params.match(/^color-mode-var\s*\(\s*(.*)\s*\);?\s*$/)
        Iif (innerMatch.length !== 2) {
          return
        }
 
        const [, params] = innerMatch
        const [, lightValue, darkValue] = params.split(',').map(str => str.trim())
 
        for (const v of [lightValue, darkValue]) {
          for (const [, variableName] of matchAll(v, variableReferenceRegex)) {
            checkVariable(variableName, rule)
          }
        }
      }
    })
 
    root.walkRules(rule => {
      rule.walkDecls(decl => {
        Iif (seen.has(decl)) {
          return
        } else {
          seen.set(decl, true)
        }
 
        for (const [, variableName] of matchAll(decl.value, variableReferenceRegex)) {
          checkVariable(variableName, decl)
        }
      })
    })
  }
})
 
const cwd = process.cwd()
const cache = new TapMap()
 
function getDefinedVariables(globs, log) {
  const cacheKey = JSON.stringify({globs, cwd})
  return cache.tap(cacheKey, () => {
    const definedVariables = new Set()
 
    const files = globby.sync(globs)
    log(`Scanning ${files.length} SCSS files for CSS variables`)
    for (const file of files) {
      log(`==========\nLooking for CSS variable definitions in ${file}`)
      const css = fs.readFileSync(file, 'utf-8')
      for (const [, variableName] of matchAll(css, variableDefinitionRegex)) {
        log(`${variableName} defined in ${file}`)
        definedVariables.add(variableName)
      }
      for (const [, variableName] of matchAll(css, colorModeVariableDefinitionRegex)) {
        log(`--color-${variableName} defined via color-mode-var in ${file}`)
        definedVariables.add(`--color-${variableName}`)
      }
    }
 
    return definedVariables
  })
}
 
function noop() {}
 
module.exports.ruleName = ruleName
module.exports.messages = messages