AST utility to collect scope info for variables
Scope detection is hard, especially when with
exists. This utility extracts all relevant info for making decisions. This project was built as part of esformatter-phonetic
, a esformatter
plugin that makes obfuscated variable names more comprehensible.
Features:
- Detect
with
usage - Does not mark
labels
- Support for
let
andconst
- Support for destructured variables (e.g.
var {hello, world} = obj;
) - Support for arrow expressions (e.g.
(hello) => hello.world
)
Install the module with: npm install ecma-variable-scope
// Gather an AST to analyze
var esprima = require('esprima');
var ecmaVariableScope = require('ecma-variable-scope');
var ast = esprima.parse([
'function logger(str) {',
'console.log(str);',
'}'
].join('\n'));
// Determine the scope of a variable
ecmaVariableScope(ast);
ast.body[0].id;
/*
// `logger` variable
scope:
{ type: 'lexical',
node: Program,
parent: undefined,
identifiers: { logger: [Circular] },
children: [ [Object] ] },
scopeInfo:
{ insideWith: false,
topLevel: true,
type: 'lexical',
usedInAWith: false }
*/
ast.body[0].body.body[0].expression.callee.object;
/*
// `console` variable
scope: undefined,
scopeInfo:
{ insideWith: false,
topLevel: true,
type: 'undeclared',
usedInAWith: false }
*/
ecma-variable-scope
exports ecmaVariableScope
as its module.exports
.
Walk an abstract syntax tree and add scope
and scopeInfo
properties to appropriate nodes.
- ast
Object
- Abstract syntax tree to walk over/mutate- We have developed against
esprima
which matches the Spidermonkey API
- We have developed against
Returns:
We return the same ast
variable but with the addition of scope
and scopeInfo
nodes on Identifiers
that are variables.
// `scope` and `scopeInfo` will be defined for all `hello`, `world`, and `moon` references
function hello(world) {
var moon;
hello(world, moon);
}
// `log` is an `Identifier` but will not have `scope` and `scopeInfo`
console.log('hello');
Object containing information about the outermost scope a variable can be accessed from:
- type
String
- Variant of scope that a variable is in- This can be
lexical
orblock
. When we traversescope.parent
, we can run intowith
but this is not directly found fromnode.scope
. - For example in
function a() { var b; }
, we havetype: 'lexical'
forb's scope
. - We make these values available via
exports.SCOPE_TYPES.LEXICAL
('lexical'
),exports.SCOPE_TYPES.WITH
('with'
), andexports.SCOPE_TYPES.BLOCK
('block'
).
- This can be
- node
Object<Node>
- AST node that corresponds to the top of scope- For example in
function a() { var b; }
, we haveb.scope.node === a
- For example in
- parent
Object<Scope>|undefined
- Next scope containing the currentscope
. This can be any other type (e.g.lexical
,block
,with
). - identifiers
Object
- Map from identifier name toIdentifier
reference of variables declared within this scope- For example in
function a() { var b; }
, we havea.scope.identifiers === {b: b's Identifier}
- This will not contain identifiers within child scopes
- For example in
- children
Scope[]
- Array of child scopes contained by this scope- For example in
function a() { }
,Program's scope
will containa.scope
- For example in
It is possible for an Identifier
to have scopeInfo
but not scope
. For example, console
is defined as a global outside of a script context. We cannot determine if it is defined or not and make the decision to leave it as undefined
.
Object containing information about the variable itself:
- insideWith
Boolean
- Indicator of whether a variable has awith
between its declaration and its containing scopetrue
if there was awith
,false
if there was not one- For example in
function a() { with (window) { document(); } }
, we havea.insideWith === false
anddocument.insideWith === true
. - We provide
exports.SCOPE_INFO_INSIDE_WITH.YES
(true)
andexports.SCOPE_INFO_INSIDE_WITH.NO
(false
).
- toplevel
Boolean
- Indicator of whether a variable was declared in theProgram
scope or nottrue
if it was,false
if it was not- For example in
function a() { var b; }
, we havea.scopeInfo.topLevel === true
andb.scopeInfo.topLevel === false
. - We provide
exports.SCOPE_INFO_TOP_LEVEL.YES
(true
) andexports.SCOPE_INFO_TOP_LEVEL.NO
(false
).
- type
String
- Reference to the type of scope given to a variable- This can vary from the containing scope (e.g. a
let
is scoped to afunction
;block
tolexical
) - This can be
lexical
(e.g.function
),block
(e.g.for
loop), orundeclared
(not declared in any containing scope) - For example in
function a() { let b; }
, we havea.scopeInfo.type === 'lexical'
andb.scopeInfo.type === 'block'
. - We provide
exports.SCOPE_INFO_TYPES.LEXICAL
('lexical'
),exports.SCOPE_INFO_TYPES.BLOCK
('block'
), andexports.SCOPE_INFO_TYPES.UNDECLARED
('undeclared'
).
- This can vary from the containing scope (e.g. a
- usedInAWith
Boolean
- Indicator of whether a variable has ever been referenced inside awith
in its entire scopetrue
if it has been,false
if it has not- For example in
var hello; var obj = {}; with (obj) { hello; } }
, we havehello1.usedInAWith === true
andobj1.usedInAWith === false
. - We provide
exports.SCOPE_INFO_USED_IN_A_WITH.YES
(true)
andexports.SCOPE_INFO_USED_IN_A_WITH.NO
(false
).
If you would like to determine if a variable can be renamed without causing other problems, use
usedInAWith
(false
),topLevel
(false
), andtype
(lexical
/block
).
There are a few extra properties that are thrown in for preparation of scope
and scopeInfo
. They could be replaced with a better algorithm but are there if you need them. If you are using them, please let us know via an issue.
_nearestScope
- Present on every node and points to the closest scope of any type up its parents. This is useful for jumping throughblock
scopes until reaching alexical
one._scopeType
- Stored on initial declarations of identifiers. This is the same asscopeInfo.type
but needs to be preserved beforescopeInfo
is generated.
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint via grunt and test via npm test
.
Support this project and others by twolfson via gratipay.
As of Nov 04 2014, Todd Wolfson has released this repository and its contents to the public domain.
It has been released under the UNLICENSE.