import wrap from './wrap.js'; import Node from './Node.js'; import Scope from './Scope.js'; import CompileError from '../utils/CompileError.js'; import destructure from '../utils/destructure.js'; function isUseStrict ( node ) { if ( !node ) return false; if ( node.type !== 'ExpressionStatement' ) return false; if ( node.expression.type !== 'Literal' ) return false; return node.expression.value === 'use strict'; } export default class BlockStatement extends Node { createScope () { this.parentIsFunction = /Function/.test( this.parent.type ); this.isFunctionBlock = this.parentIsFunction || this.parent.type === 'Root'; this.scope = new Scope({ block: !this.isFunctionBlock, parent: this.parent.findScope( false ) }); if ( this.parentIsFunction ) { this.parent.params.forEach( node => { this.scope.addDeclaration( node, 'param' ); }); } } initialise ( transforms ) { this.thisAlias = null; this.argumentsAlias = null; this.defaultParameters = []; // normally the scope gets created here, during initialisation, // but in some cases (e.g. `for` statements), we need to create // the scope early, as it pertains to both the init block and // the body of the statement if ( !this.scope ) this.createScope(); this.body.forEach( node => node.initialise( transforms ) ); this.scope.consolidate(); } findLexicalBoundary () { if ( this.type === 'Program' ) return this; if ( /^Function/.test( this.parent.type ) ) return this; return this.parent.findLexicalBoundary(); } findScope ( functionScope ) { if ( functionScope && !this.isFunctionBlock ) return this.parent.findScope( functionScope ); return this.scope; } getArgumentsAlias () { if ( !this.argumentsAlias ) { this.argumentsAlias = this.scope.createIdentifier( 'arguments' ); } return this.argumentsAlias; } getArgumentsArrayAlias () { if ( !this.argumentsArrayAlias ) { this.argumentsArrayAlias = this.scope.createIdentifier( 'argsArray' ); } return this.argumentsArrayAlias; } getThisAlias () { if ( !this.thisAlias ) { this.thisAlias = this.scope.createIdentifier( 'this' ); } return this.thisAlias; } getIndentation () { if ( this.indentation === undefined ) { const source = this.program.magicString.original; const useOuter = this.synthetic || !this.body.length; let c = useOuter ? this.start : this.body[0].start; while ( c && source[c] !== '\n' ) c -= 1; this.indentation = ''; while ( true ) { c += 1; const char = source[c]; if ( char !== ' ' && char !== '\t' ) break; this.indentation += char; } const indentString = this.program.magicString.getIndentString(); // account for dedented class constructors let parent = this.parent; while ( parent ) { if ( parent.kind === 'constructor' && !parent.parent.parent.superClass ) { this.indentation = this.indentation.replace( indentString, '' ); } parent = parent.parent; } if ( useOuter ) this.indentation += indentString; } return this.indentation; } transpile ( code, transforms ) { const indentation = this.getIndentation(); let introStatementGenerators = []; if ( this.argumentsAlias ) { introStatementGenerators.push( ( start, prefix, suffix ) => { const assignment = `${prefix}var ${this.argumentsAlias} = arguments;${suffix}`; code.insertLeft( start, assignment ); }); } if ( this.thisAlias ) { introStatementGenerators.push( ( start, prefix, suffix ) => { const assignment = `${prefix}var ${this.thisAlias} = this;${suffix}`; code.insertLeft( start, assignment ); }); } if ( this.argumentsArrayAlias ) { introStatementGenerators.push( ( start, prefix, suffix ) => { const i = this.scope.createIdentifier( 'i' ); const assignment = `${prefix}var ${i} = arguments.length, ${this.argumentsArrayAlias} = Array(${i});\n${indentation}while ( ${i}-- ) ${this.argumentsArrayAlias}[${i}] = arguments[${i}];${suffix}`; code.insertLeft( start, assignment ); }); } if ( /Function/.test( this.parent.type ) ) { this.transpileParameters( code, transforms, indentation, introStatementGenerators ); } if ( transforms.letConst && this.isFunctionBlock ) { this.transpileBlockScopedIdentifiers( code ); } super.transpile( code, transforms ); if ( this.synthetic ) { if ( this.parent.type === 'ArrowFunctionExpression' ) { const expr = this.body[0]; if ( introStatementGenerators.length ) { code.insertLeft( this.start, `{` ).insertRight( this.end, `${this.parent.getIndentation()}}` ); code.insertRight( expr.start, `\n${indentation}return ` ); code.insertLeft( expr.end, `;\n` ); } else if ( transforms.arrow ) { code.insertRight( expr.start, `{ return ` ); code.insertLeft( expr.end, `; }` ); } } else if ( introStatementGenerators.length ) { code.insertLeft( this.start, `{` ).insertRight( this.end, `}` ); } } let start; if ( isUseStrict( this.body[0] ) ) { start = this.body[0].end; } else if ( this.synthetic || this.parent.type === 'Root' ) { start = this.start; } else { start = this.start + 1; } let prefix = `\n${indentation}`; let suffix = ''; introStatementGenerators.forEach( ( fn, i ) => { if ( i === introStatementGenerators.length - 1 ) suffix = `\n`; fn( start, prefix, suffix ); }); } transpileParameters ( code, transforms, indentation, introStatementGenerators ) { const params = this.parent.params; params.forEach( param => { if ( param.type === 'AssignmentPattern' && param.left.type === 'Identifier' ) { if ( transforms.defaultParameter ) { introStatementGenerators.push( ( start, prefix, suffix ) => { const lhs = `${prefix}if ( ${param.left.name} === void 0 ) ${param.left.name}`; code .insertRight( param.left.end, `${lhs}` ) .move( param.left.end, param.right.end, start ) .insertLeft( param.right.end, `;${suffix}` ); }); } } else if ( param.type === 'RestElement' ) { if ( transforms.spreadRest ) { introStatementGenerators.push( ( start, prefix, suffix ) => { const penultimateParam = params[ params.length - 2 ]; if ( penultimateParam ) { code.remove( penultimateParam ? penultimateParam.end : param.start, param.end ); } else { let start = param.start, end = param.end; // TODO https://gitlab.com/Rich-Harris/buble/issues/8 while ( /\s/.test( code.original[ start - 1 ] ) ) start -= 1; while ( /\s/.test( code.original[ end ] ) ) end += 1; code.remove( start, end ); } const name = param.argument.name; const len = this.scope.createIdentifier( 'len' ); const count = params.length - 1; if ( count ) { code.insertLeft( start, `${prefix}var ${name} = [], ${len} = arguments.length - ${count};\n${indentation}while ( ${len}-- > 0 ) ${name}[ ${len} ] = arguments[ ${len} + ${count} ];${suffix}` ); } else { code.insertLeft( start, `${prefix}var ${name} = [], ${len} = arguments.length;\n${indentation}while ( ${len}-- ) ${name}[ ${len} ] = arguments[ ${len} ];${suffix}` ); } }); } } else if ( param.type !== 'Identifier' ) { if ( transforms.parameterDestructuring ) { const ref = this.scope.createIdentifier( 'ref' ); destructure( code, this.scope, param, ref, introStatementGenerators ); code.insertLeft( param.start, ref ); } } }); } transpileBlockScopedIdentifiers ( code ) { Object.keys( this.scope.blockScopedDeclarations ).forEach( name => { const declarations = this.scope.blockScopedDeclarations[ name ]; for ( let declaration of declarations ) { let cont = false; // TODO implement proper continue... if ( declaration.kind === 'for.let' ) { // special case const forStatement = declaration.node.findNearest( 'ForStatement' ); if ( forStatement.shouldRewriteAsFunction ) { const outerAlias = this.scope.createIdentifier( name ); const innerAlias = forStatement.reassigned[ name ] ? this.scope.createIdentifier( name ) : name; declaration.name = outerAlias; code.overwrite( declaration.node.start, declaration.node.end, outerAlias, true ); forStatement.aliases[ name ] = { outer: outerAlias, inner: innerAlias }; for ( const identifier of declaration.instances ) { const alias = forStatement.body.contains( identifier ) ? innerAlias : outerAlias; if ( name !== alias ) { code.overwrite( identifier.start, identifier.end, alias, true ); } } cont = true; } } if ( !cont ) { const alias = this.scope.createIdentifier( name ); if ( name !== alias ) { declaration.name = alias; code.overwrite( declaration.node.start, declaration.node.end, alias, true ); for ( const identifier of declaration.instances ) { identifier.rewritten = true; code.overwrite( identifier.start, identifier.end, alias, true ); } } } } }); } }