All files / plugins no-undefined-vars.js

92.86% Statements 52/56
58.82% Branches 10/17
91.67% Functions 11/12
94.44% Lines 51/54

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 1093x 3x 3x 3x 3x   3x 3x 6x       3x     3x       3x   3x 10x       10x   10x 10x     10x   10x   12x 3x                 10x 2x 2x 2x       2x 6x   2x 4x 4x           10x 8x 8x     8x     8x 8x             3x 3x     10x 10x 1x   1x 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 = /(--[\w|-]*):/g
 
// Match CSS variables defined with the color-mode-var mixin (e.g. @include color-mode-var(my-feature, ...))
const colorModeVariableDefinitionRegex = /color-mode-var\s*\(\s*['"]?([^'",]+)['"]?/g
 
// 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*$/)
        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(files, log) {
  const cacheKey = JSON.stringify({files, cwd})
  return cache.tap(cacheKey, () => {
    const definedVariables = new Set()
 
    for (const file of globby.sync(files)) {
      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