You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

242 lines
5.7 KiB

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;
}
}