From 25bcfef8781772f3e1e0dae90412ebbf2dee43c3 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Thu, 2 Mar 2017 07:31:46 -0500 Subject: [PATCH] allow [arrow] function expressions inside tags (#269) --- src/generators/Generator.js | 14 +++ src/generators/annotateWithScopes.js | 103 ++++++++++++++++++ test/generate.js | 16 +-- .../function-in-expression/_config.js | 19 ++++ .../function-in-expression/main.html | 1 + 5 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/generators/annotateWithScopes.js create mode 100644 test/generator/function-in-expression/_config.js create mode 100644 test/generator/function-in-expression/main.html diff --git a/src/generators/Generator.js b/src/generators/Generator.js index 2300323b1f..12081a0369 100644 --- a/src/generators/Generator.js +++ b/src/generators/Generator.js @@ -6,6 +6,7 @@ import flattenReference from '../utils/flattenReference.js'; import globalWhitelist from '../utils/globalWhitelist.js'; import getIntro from './shared/utils/getIntro.js'; import getOutro from './shared/utils/getOutro.js'; +import annotateWithScopes from './annotateWithScopes.js'; export default class Generator { constructor ( parsed, source, names, visitors ) { @@ -49,10 +50,18 @@ export default class Generator { const { code, helpers } = this; const { contextDependencies, contexts, indexes } = this.current; + let scope = annotateWithScopes( expression ); + walk( expression, { enter ( node, parent, key ) { + if ( node._scope ) { + scope = node._scope; + return; + } + if ( isReference( node, parent ) ) { const { name } = flattenReference( node ); + if ( scope.has( name ) ) return; if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers[ name ] ) { code.prependRight( node.start, `template.helpers.` ); @@ -100,6 +109,10 @@ export default class Generator { this.skip(); } + }, + + leave ( node ) { + if ( node._scope ) scope = scope.parent; } }); @@ -347,3 +360,4 @@ export default class Generator { if ( visitor.leave ) visitor.leave( this, node ); } } + diff --git a/src/generators/annotateWithScopes.js b/src/generators/annotateWithScopes.js new file mode 100644 index 0000000000..36894b5060 --- /dev/null +++ b/src/generators/annotateWithScopes.js @@ -0,0 +1,103 @@ +import { walk } from 'estree-walker'; + +export default function annotateWithScopes ( expression ) { + let scope = new Scope( null, false ); + + walk( expression, { + enter ( node ) { + if ( /Function/.test( node.type ) ) { + if ( node.type === 'FunctionDeclaration' ) { + scope.declarations[ node.id.name ] = true; + } else { + node._scope = scope = new Scope( scope, false ); + if ( node.id ) scope.declarations[ node.id.name ] = true; + } + + node.params.forEach( param => { + extractNames( param ).forEach( name => { + scope.declarations[ name ] = true; + }); + }); + } + + else if ( /For(?:In|Of)Statement/.test( node.type ) ) { + node._scope = scope = new Scope( scope, true ); + } + + else if ( node.type === 'BlockStatement' ) { + node._scope = scope = new Scope( scope, true ); + } + + else if ( /Declaration/.test( node.type ) ) { + scope.addDeclaration( node ); + } + }, + + leave ( node ) { + if ( node._scope ) { + scope = scope.parent; + } + } + }); + + return scope; +} + +class Scope { + constructor ( parent, block ) { + this.parent = parent; + this.block = block; + this.declarations = Object.create( null ); + } + + addDeclaration ( node ) { + if ( node.kind === 'var' && !this.block && this.parent ) { + this.parent.addDeclaration( node ); + } else if ( node.type === 'VariableDeclaration' ) { + node.declarators.forEach( declarator => { + extractNames( declarator.id ).forEach( name => { + this.declarations[ name ] = true; + }); + }); + } else { + this.declarations[ node.id.name ] = true; + } + } + + has ( name ) { + return name in this.declarations || this.parent && this.parent.has( name ); + } +} + +function extractNames ( param ) { + const names = []; + extractors[ param.type ]( names, param ); + return names; +} + +const extractors = { + Identifier ( names, param ) { + names.push( param.name ); + }, + + ObjectPattern ( names, param ) { + param.properties.forEach( prop => { + extractors[ prop.value.type ]( names, prop.value ); + }); + }, + + 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 ) { + extractors[ param.left.type ]( names, param.left ); + } +}; + diff --git a/test/generate.js b/test/generate.js index 0bd0e8e608..113c162dc9 100644 --- a/test/generate.js +++ b/test/generate.js @@ -58,13 +58,15 @@ describe( 'generate', () => { const { code } = compiled; // check that no ES2015+ syntax slipped in - try { - const startIndex = code.indexOf( 'function renderMainFragment' ); // may change! - const es5 = spaces( startIndex ) + code.slice( startIndex ).replace( /export default .+/, '' ); - acorn.parse( es5, { ecmaVersion: 5 }); - } catch ( err ) { - if ( !config.show ) console.log( addLineNumbers( code ) ); // eslint-disable-line no-console - throw err; + if ( !config.allowES2015 ) { + try { + const startIndex = code.indexOf( 'function renderMainFragment' ); // may change! + const es5 = spaces( startIndex ) + code.slice( startIndex ).replace( /export default .+/, '' ); + acorn.parse( es5, { ecmaVersion: 5 }); + } catch ( err ) { + if ( !config.show ) console.log( addLineNumbers( code ) ); // eslint-disable-line no-console + throw err; + } } Object.keys( require.cache ).filter( x => x.endsWith( '.html' ) ).forEach( file => { diff --git a/test/generator/function-in-expression/_config.js b/test/generator/function-in-expression/_config.js new file mode 100644 index 0000000000..14cfb8f89f --- /dev/null +++ b/test/generator/function-in-expression/_config.js @@ -0,0 +1,19 @@ +export default { + allowES2015: true, + + data: { + numbers: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] + }, + + html: '1, 3, 5, 7, 9', + + test ( assert, component, target ) { + component.set({ + numbers: [ 10, 11, 12, 13, 14, 15, 16 ] + }); + + assert.htmlEqual( target.innerHTML, `11, 13, 15` ); + + component.destroy(); + } +}; \ No newline at end of file diff --git a/test/generator/function-in-expression/main.html b/test/generator/function-in-expression/main.html new file mode 100644 index 0000000000..2c98a5124d --- /dev/null +++ b/test/generator/function-in-expression/main.html @@ -0,0 +1 @@ +{{ numbers.filter( x => x % 2 ).join( ', ' ) }} \ No newline at end of file