|
|
|
import { has, keys } from './utils/object';
|
|
|
|
import { getName } from './utils/map-helpers';
|
|
|
|
import getLocation from './utils/getLocation';
|
|
|
|
import walk from './ast/walk';
|
|
|
|
import Scope from './ast/Scope';
|
|
|
|
|
|
|
|
export default class Statement {
|
|
|
|
constructor ( node, magicString, module ) {
|
|
|
|
this.node = node;
|
|
|
|
this.module = module;
|
|
|
|
this.magicString = magicString;
|
|
|
|
|
|
|
|
this.scope = new Scope();
|
|
|
|
this.defines = {};
|
|
|
|
this.modifies = {};
|
|
|
|
this.dependsOn = {};
|
|
|
|
|
|
|
|
this.isIncluded = false;
|
|
|
|
|
|
|
|
this.leadingComments = [];
|
|
|
|
this.trailingComment = null;
|
|
|
|
this.margin = [ 0, 0 ];
|
|
|
|
|
|
|
|
// some facts about this statement...
|
|
|
|
this.isImportDeclaration = node.type === 'ImportDeclaration';
|
|
|
|
this.isExportDeclaration = /^Export/.test( node.type );
|
|
|
|
}
|
|
|
|
|
|
|
|
analyse () {
|
|
|
|
if ( this.isImportDeclaration ) return; // nothing to analyse
|
|
|
|
|
|
|
|
const statement = this; // TODO use arrow functions instead
|
|
|
|
const magicString = this.magicString;
|
|
|
|
|
|
|
|
let scope = this.scope;
|
|
|
|
|
|
|
|
function addToScope ( declarator ) {
|
|
|
|
var name = declarator.id.name;
|
|
|
|
scope.add( name, false );
|
|
|
|
|
|
|
|
if ( !scope.parent ) {
|
|
|
|
statement.defines[ name ] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function addToBlockScope ( declarator ) {
|
|
|
|
var name = declarator.id.name;
|
|
|
|
scope.add( name, true );
|
|
|
|
|
|
|
|
if ( !scope.parent ) {
|
|
|
|
statement.defines[ name ] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
walk( this.node, {
|
|
|
|
enter ( node ) {
|
|
|
|
let newScope;
|
|
|
|
|
|
|
|
magicString.addSourcemapLocation( node.start );
|
|
|
|
|
|
|
|
switch ( node.type ) {
|
|
|
|
case 'FunctionExpression':
|
|
|
|
case 'FunctionDeclaration':
|
|
|
|
case 'ArrowFunctionExpression':
|
|
|
|
let names = node.params.map( getName );
|
|
|
|
|
|
|
|
if ( node.type === 'FunctionDeclaration' ) {
|
|
|
|
addToScope( node );
|
|
|
|
} else if ( node.type === 'FunctionExpression' && node.id ) {
|
|
|
|
names.push( node.id.name );
|
|
|
|
}
|
|
|
|
|
|
|
|
newScope = new Scope({
|
|
|
|
parent: scope,
|
|
|
|
params: names, // TODO rest params?
|
|
|
|
block: false
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'BlockStatement':
|
|
|
|
newScope = new Scope({
|
|
|
|
parent: scope,
|
|
|
|
block: true
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'CatchClause':
|
|
|
|
newScope = new Scope({
|
|
|
|
parent: scope,
|
|
|
|
params: [ node.param.name ],
|
|
|
|
block: true
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'VariableDeclaration':
|
|
|
|
node.declarations.forEach( node.kind === 'let' ? addToBlockScope : addToScope ); // TODO const?
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'ClassDeclaration':
|
|
|
|
addToScope( node );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( newScope ) {
|
|
|
|
Object.defineProperty( node, '_scope', { value: newScope });
|
|
|
|
scope = newScope;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
leave ( node ) {
|
|
|
|
if ( node._scope ) {
|
|
|
|
scope = scope.parent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if ( !this.isImportDeclaration ) {
|
|
|
|
walk( this.node, {
|
|
|
|
enter: ( node, parent ) => {
|
|
|
|
if ( node._scope ) scope = node._scope;
|
|
|
|
|
|
|
|
this.checkForReads( scope, node, parent );
|
|
|
|
this.checkForWrites( scope, node );
|
|
|
|
},
|
|
|
|
leave: ( node ) => {
|
|
|
|
if ( node._scope ) scope = scope.parent;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
checkForReads ( scope, node, parent ) {
|
|
|
|
if ( node.type === 'Identifier' ) {
|
|
|
|
// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
|
|
|
|
if ( parent.type === 'MemberExpression' && node !== parent.object ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// disregard the `bar` in { bar: foo }
|
|
|
|
if ( parent.type === 'Property' && node !== parent.value ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const definingScope = scope.findDefiningScope( node.name );
|
|
|
|
|
|
|
|
if ( ( !definingScope || definingScope.depth === 0 ) && !this.defines[ node.name ] ) {
|
|
|
|
this.dependsOn[ node.name ] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
checkForWrites ( scope, node ) {
|
|
|
|
const addNode = ( node, disallowImportReassignments ) => {
|
|
|
|
while ( node.type === 'MemberExpression' ) {
|
|
|
|
node = node.object;
|
|
|
|
}
|
|
|
|
|
|
|
|
// disallow assignments/updates to imported bindings and namespaces
|
|
|
|
if ( disallowImportReassignments && has( this.module.imports, node.name ) && !scope.contains( node.name ) ) {
|
|
|
|
const err = new Error( `Illegal reassignment to import '${node.name}'` );
|
|
|
|
err.file = this.module.path;
|
|
|
|
err.loc = getLocation( this.module.magicString.toString(), node.start );
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( node.type !== 'Identifier' ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.modifies[ node.name ] = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
if ( node.type === 'AssignmentExpression' ) {
|
|
|
|
addNode( node.left, true );
|
|
|
|
}
|
|
|
|
|
|
|
|
else if ( node.type === 'UpdateExpression' ) {
|
|
|
|
addNode( node.argument, true );
|
|
|
|
}
|
|
|
|
|
|
|
|
else if ( node.type === 'CallExpression' ) {
|
|
|
|
node.arguments.forEach( arg => addNode( arg, false ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
replaceIdentifiers ( names ) {
|
|
|
|
const magicString = this.magicString.clone().trim();
|
|
|
|
const replacementStack = [ names ];
|
|
|
|
const nameList = keys( names );
|
|
|
|
|
|
|
|
if ( nameList.length > 0 ) {
|
|
|
|
walk( this.node, {
|
|
|
|
enter ( node, parent ) {
|
|
|
|
const scope = node._scope;
|
|
|
|
|
|
|
|
if ( scope ) {
|
|
|
|
let newNames = {};
|
|
|
|
let hasReplacements;
|
|
|
|
|
|
|
|
nameList.forEach( key => {
|
|
|
|
if ( !~scope.names.indexOf( key ) ) {
|
|
|
|
newNames[ key ] = names[ key ];
|
|
|
|
hasReplacements = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if ( !hasReplacements ) {
|
|
|
|
return this.skip();
|
|
|
|
}
|
|
|
|
|
|
|
|
names = newNames;
|
|
|
|
replacementStack.push( newNames );
|
|
|
|
}
|
|
|
|
|
|
|
|
// We want to rewrite identifiers (that aren't property names)
|
|
|
|
if ( node.type !== 'Identifier' ) return;
|
|
|
|
if ( parent.type === 'MemberExpression' && node !== parent.object ) return;
|
|
|
|
if ( parent.type === 'Property' && node !== parent.value ) return;
|
|
|
|
// TODO others...?
|
|
|
|
|
|
|
|
const name = has( names, node.name ) && names[ node.name ];
|
|
|
|
|
|
|
|
if ( name && name !== node.name ) {
|
|
|
|
magicString.overwrite( node.start, node.end, name );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
leave ( node ) {
|
|
|
|
if ( node._scope ) {
|
|
|
|
replacementStack.pop();
|
|
|
|
names = replacementStack[ replacementStack.length - 1 ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return magicString;
|
|
|
|
}
|
|
|
|
}
|