Home Reference Source

src/findGlobals.js

import { ancestor as ancestorWalk } from 'acorn/dist/walk'

/*
  Finds variables and function calls that are not declared locally.
*/
export default function findGlobals (ast, options = {}) {
  let ignore = options.ignore || new Set()

  if (!(ast && typeof ast === 'object' && ast.type === 'Program')) {
    throw new TypeError('Source must be either a string of JavaScript or an acorn AST')
  }
  let candidates = []
  function _captureCandidate (node, parents) {
    node.parents = parents.slice()
    candidates.push(node)
  }
  // First pass: capture declared variables and record potential candidates
  ancestorWalk(ast, {
    'VariableDeclaration': function (node, parents) {
      let parent = _getParentScope(node, parents)
      // TODO: what does this?
      node.declarations.forEach(function (declaration) {
        _declarePattern(declaration.id, parent)
      })
    },
    'FunctionDeclaration': function (node, parents) {
      let parent = _getParentScope(node, parents, 1)
      parent.locals[node.id.name] = true
      _declareFunction(node)
    },
    'Function': _declareFunction,
    'ClassDeclaration': function (node, parents) {
      let parent = _getParentScope(node, parents, 1)
      parent.locals[node.id.name] = true
    },
    'TryStatement': function (node) {
      const handler = node.handler
      if (handler === null) return
      _initScope(handler)
      handler.locals[handler.param.name] = true
    },
    'ImportDefaultSpecifier': _declareModuleSpecifier,
    'ImportSpecifier': _declareModuleSpecifier,
    'ImportNamespaceSpecifier': _declareModuleSpecifier,
    // collect candidates
    'VariablePattern': _captureCandidate,
    'Identifier': _captureCandidate,
    'ThisExpression': _captureCandidate,
    'FunctionExpression': _captureCandidate
  })

  let globals = new Set()
  candidates.forEach(node => {
    if (_isGlobal(node, node.parents)) {
      const name = node.type === 'ThisExpression' ? 'this' : node.name
      // skip ignored globals, which is useful to ignore built-ins for instance
      if (ignore.has(name)) return
      globals.add(name)
    }
  })
  return globals
}

const BLOCK_DECLS = new Set(['let', 'const'])
const BLOCKS_WITH_DECLS = new Set(['ForInStatement'])

function _getParentScope (node, parents, skipLast = 0) {
  let scope
  if (BLOCK_DECLS.has(node.kind)) {
    scope = __getParentBlockScope(node, parents, skipLast)
  } else {
    scope = __getToplevelScope(node, parents, skipLast)
  }
  return _initScope(scope)
}

function __getParentBlockScope (node, parents, skipLast) {
  for (let i = parents.length - 1 - skipLast; i >= 0; i--) {
    const parent = parents[i]
    if (BLOCKS_WITH_DECLS.has(parent.type)) {
      return parent.body
    } else if (_isBlockScope(parent)) {
      return parent
    }
  }
}

function __getToplevelScope (node, parents, skipLast) {
  for (let i = parents.length - 1 - skipLast; i >= 0; i--) {
    const parent = parents[i]
    if (_isScope(parent)) {
      return parent
    }
  }
}

function _initScope (scope) {
  if (!scope.locals) scope.locals = {}
  return scope
}

function _isGlobal (node, parents) {
  if (node.type === 'ThisExpression') {
    for (let i = parents.length - 1; i >= 0; i--) {
      let parent = parents[i]
      if (_declaresThis(parent)) {
        return false
      }
    }
  } else {
    let name = node.name
    // TODO: what is this?
    if (name === 'undefined') return
    for (let i = parents.length - 1; i >= 0; i--) {
      let parent = parents[i]
      let scope
      // TODO: do we want this? using a keyword for variables is not good practise
      if (name === 'arguments' && _declaresArguments(parent)) {
        return false
      }
      if (BLOCKS_WITH_DECLS.has(parent.type)) {
        scope = parent.body
      } else {
        scope = parent
      }
      if (scope.locals && name in scope.locals) {
        return false
      }
    }
  }
  return true
}

function _isScope (node) {
  return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression' || node.type === 'Program'
}

function _isBlockScope (node) {
  return node.type === 'BlockStatement' || _isScope(node)
}

function _declaresArguments (node) {
  return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration'
}

function _declaresThis (node) {
  return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration'
}

function _declareFunction (node) {
  let fn = node
  fn.locals = fn.locals || {}
  node.params.forEach(function (node) {
    _declarePattern(node, fn)
  })
  if (node.id) {
    fn.locals[node.id.name] = true
  }
}

function _declarePattern (node, scope) {
  switch (node.type) {
    case 'Identifier':
      scope.locals[node.name] = true
      break
    case 'ObjectPattern':
      node.properties.forEach(function (node) {
        _declarePattern(node.value, scope)
      })
      break
    case 'ArrayPattern':
      node.elements.forEach(function (node) {
        if (node) _declarePattern(node, scope)
      })
      break
    case 'RestElement':
      _declarePattern(node.argument, scope)
      break
    case 'AssignmentPattern':
      _declarePattern(node.left, scope)
      break
    // istanbul ignore next
    default:
      throw new Error('Unrecognized pattern type: ' + node.type)
  }
}

function _declareModuleSpecifier (ast, node) {
  ast.locals = ast.locals || {}
  ast.locals[node.local.name] = true
}