ember-introjs
Version:
An Ember Component for intro.js
148 lines (120 loc) • 3.25 kB
JavaScript
import { walk } from 'estree-walker';
const blockDeclarations = {
'const': true,
'let': true
};
const extractors = {
Identifier ( names, param ) {
names.push( param.name );
},
ObjectPattern ( names, param ) {
param.properties.forEach( prop => {
extractors[ prop.key.type ]( names, prop.key );
});
},
ArrayPattern ( names, param ) {
param.elements.forEach( element => {
if ( element ) extractors[ element.type ]( names, element );
});
},
RestElement ( names, param ) {
extractors[ param.argument.type ]( names, param.argument );
},
AssignmentPattern ( names, param ) {
return extractors[ param.left.type ]( names, param.left );
}
};
function extractNames ( param ) {
let names = [];
extractors[ param.type ]( names, param );
return names;
}
class Scope {
constructor ( options ) {
options = options || {};
this.parent = options.parent;
this.isBlockScope = !!options.block;
this.declarations = Object.create( null );
if ( options.params ) {
options.params.forEach( param => {
extractNames( param ).forEach( name => {
this.declarations[ name ] = true;
});
});
}
}
addDeclaration ( node, isBlockDeclaration, isVar ) {
if ( !isBlockDeclaration && this.isBlockScope ) {
// it's a `var` or function node, and this
// is a block scope, so we need to go up
this.parent.addDeclaration( node, isBlockDeclaration, isVar );
} else {
extractNames( node.id ).forEach( name => {
this.declarations[ name ] = true;
});
}
}
contains ( name ) {
return this.declarations[ name ] ||
( this.parent ? this.parent.contains( name ) : false );
}
}
export default function attachScopes ( ast, propertyName = 'scope' ) {
let scope = new Scope();
walk( ast, {
enter ( node, parent ) {
// function foo () {...}
// class Foo {...}
if ( /(Function|Class)Declaration/.test( node.type ) ) {
scope.addDeclaration( node, false, false );
}
// var foo = 1
if ( node.type === 'VariableDeclaration' ) {
const isBlockDeclaration = blockDeclarations[ node.kind ];
node.declarations.forEach( declaration => {
scope.addDeclaration( declaration, isBlockDeclaration, true );
});
}
let newScope;
// create new function scope
if ( /Function/.test( node.type ) ) {
newScope = new Scope({
parent: scope,
block: false,
params: node.params
});
// named function expressions - the name is considered
// part of the function's scope
if ( node.type === 'FunctionExpression' && node.id ) {
newScope.addDeclaration( node, false, false );
}
}
// create new block scope
if ( node.type === 'BlockStatement' && !/Function/.test( parent.type ) ) {
newScope = new Scope({
parent: scope,
block: true
});
}
// catch clause has its own block scope
if ( node.type === 'CatchClause' ) {
newScope = new Scope({
parent: scope,
params: [ node.param ],
block: true
});
}
if ( newScope ) {
Object.defineProperty( node, propertyName, {
value: newScope,
configurable: true
});
scope = newScope;
}
},
leave ( node ) {
if ( node[ propertyName ] ) scope = scope.parent;
}
});
return scope;
}