[AST ESlint] Prevent Console log, edge case, variable reference

tozeroblog 2020-02-17

For eslint prevent console plugin, we also want to prevent user do so:

var csl = console
csl.log()

Code:

const disallowedMethods = [‘log‘, ‘info‘, ‘warn‘, ‘error‘, ‘dir‘]

module.exports = {
  meta: {
    docs: {
      description: ‘Disallow use of console‘,
      category: ‘Best Practices‘,
      recommended: true,
    },
    schema: [
      {
        type: ‘object‘,
        properties: {
          allowedMethods: {
            type: ‘array‘,
            items: {
              enum: [‘log‘, ‘info‘, ‘warn‘, ‘error‘, ‘dir‘],
            },
            minItems: 1,
            uniqueItems: true,
          },
        },
      },
    ],
  },
  create(context) {
    const config = context.options[0] || {}
    const allowedMethods = config.allowedMethods || []
    const consoleUsage = []
    return {
      Identifier(node) {
        if (node.name !== ‘console‘) {
          return
        }
        consoleUsage.push(node)
      },
      /**
       * By the time
       *  var csl = console
          csl.log()

          compiler hasn‘t exected csl.log
          We have to do in ‘Program:exit‘
       */
      ‘Program:exit‘() {
        consoleUsage.forEach(identifier => {
          if (isDisallowedFunctionCall(identifier)) {
            context.report({
              node: identifier.parent.property,
              message: ‘Using console is not allowed‘,
            })
          } else {
            const variableDeclaratorParent = findParent(
              identifier,
              parent => parent.type === ‘VariableDeclarator‘,
            )
            if (variableDeclaratorParent) {
              const references = context
                .getDeclaredVariables(variableDeclaratorParent)[0]
                .references.slice(1)
              references.forEach(reference => {
                if (
                  !looksLike(reference, {
                    identifier: {
                      parent: {
                        property: isDisallowedFunctionCall,
                      },
                    },
                  })
                ) {
                  return
                }
                context.report({
                  node: reference.identifier.parent.property,
                  message: ‘Using console is not allowed‘,
                })
              })
            }
          }
        })
      },
    }

    function isDisallowedFunctionCall(identifier) {
      return looksLike(identifier, {
        parent: {
          type: ‘MemberExpression‘,
          parent: {type: ‘CallExpression‘},
          property: {
            name: val =>
              !allowedMethods.includes(val) && disallowedMethods.includes(val),
          },
        },
      })
    }
  },
}

function findParent(node, test) {
  if (test(node)) {
    return node
  } else if (node.parent) {
    return findParent(node.parent, test)
  }
  return null
}

function looksLike(a, b) {
  return (
    a &&
    b &&
    Object.keys(b).every(bKey => {
      const bVal = b[bKey]
      const aVal = a[bKey]
      if (typeof bVal === ‘function‘) {
        return bVal(aVal)
      }
      return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal)
    })
  )
}

function isPrimitive(val) {
  return val == null || /^[sbn]/.test(typeof val)
}

Test:

const {RuleTester} = require(‘eslint‘)
const rule = require(‘./no-console-5‘)

const ruleTester = new RuleTester()
ruleTester.run(‘no-console‘, rule, {
  valid: [
    ‘info()‘,
    ‘console‘,
    ‘console.log‘,
    ‘console.baz()‘,
    {code: ‘console.warn()‘, options: [{allowedMethods: [‘warn‘]}]},
  ],
  invalid: [
    invalid(‘console.log()‘),
    invalid(‘console.info()‘),
    invalid(‘console.warn()‘),
    invalid(
      `
        var csl = console
        csl.log()
      `,
    ),
  ],
})

function invalid(code) {
  return {
    code,
    errors: [{message: ‘Using console is not allowed‘}],
  }
}

相关推荐