diff --git a/visitors/TypeExpressionParser.js b/visitors/TypeExpressionParser.js new file mode 100644 index 0000000..8d42cf9 --- /dev/null +++ b/visitors/TypeExpressionParser.js @@ -0,0 +1,572 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*global exports:true*/ +"use strict"; + +var Syntax = require('esprima-fb').Syntax; + +function toObject(/*array*/ array) /*object*/ { + var object = {}; + for (var i = 0; i < array.length; i++) { + var value = array[i]; + object[value] = value; + } + return object; +} + +function reverseObject(/*object*/ object) /*object*/ { + var reversed = {}; + for (var key in object) { + if (object.hasOwnProperty(key)) { + reversed[object[key]] = key; + } + } + return reversed; +} + +function getTagName(string) { + if (string === 'A') { + return 'Anchor'; + } + if (string === 'IMG') { + return 'Image'; + } + return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); +} + +var TOKENS = { + STRING: 'string', + OPENGENERIC: '<', + CLOSEGENERIC: '>', + COMMA: ',', + OPENPAREN: '(', + CLOSEPAREN: ')', + COLON: ':', + BAR: '|', + NULLABLE: '?', + EOL: 'eol', + OPENSEGMENT: '{', + CLOSESEGMENT: '}' +}; +var TOKENMAP = reverseObject(TOKENS); + +var SYMBOLS = { + SIMPLE: 'simple', + UNION: 'union', + GENERIC: 'generic', + FUNCTION: 'function', + SEGMENT: 'segment' +}; + +var PARSERS = { + SIMPLE: 1, + UNION: 2, + GENERIC: 4, + FUNCTION: 8, + SEGMENT: 16 +}; + +/*----- tokenizer-----*/ + +function createTokenStream(source) { + var stream = [], string, pos = 0; + + do { + var character = source.charAt(pos); + if (character && /\w/.test(character)) { + string = string ? string + character : character; + } else { + if (string) { + stream.push({ type: TOKENS.STRING, value: string }); + string = null; + } + + if (character) { + if (character in TOKENMAP) { + stream.push({ type: character }); + } else { + throwError('Invalid character: ' + character + ' at pos: ' + pos); + } + } else { + stream.push({ type: TOKENS.EOL }); + break; + } + } + } while (++pos); + + return stream; +} + +/*----- parser-----*/ + +var SIMPLETYPES = toObject([ + 'string', + 'number', + 'regexp', + 'boolean', + 'object', + 'function', + 'array', + 'date', + 'blob', + 'file', + 'int8array', + 'uint8array', + 'int16array', + 'uint16array', + 'int32array', + 'uint32array', + 'float32array', + 'float64array', + 'filelist', + 'promise', + 'map', + 'set' +]); + +// types typically used in legacy docblock +var BLACKLISTED = toObject([ + 'Object', + 'Boolean', + 'bool', + 'Number', + 'String', + 'int', + 'Node', + 'Element', +]); + +function createAst(type, value, length) { + return { type: type, value: value, length: length }; +} + +function nullable(fn) { + return function(stream, pos) { + var nullable = stream[pos].type == '?' && ++pos; + var ast = fn(stream, pos); + if (ast && nullable) { + ast.nullable = true; + ast.length++; + } + return ast; + }; +} + +var parseSimpleType = nullable(function(stream, pos) { + if (stream[pos].type == TOKENS.STRING) { + var value = stream[pos].value; + if ((/^[a-z]/.test(value) && !(value in SIMPLETYPES)) + || value in BLACKLISTED) { + throwError('Invalid type ' + value + ' at pos: ' + pos); + } + return createAst(SYMBOLS.SIMPLE, stream[pos].value, 1); + } +}); + +var parseUnionType = nullable(function(stream, pos) { + var parsers = + PARSERS.SIMPLE | PARSERS.GENERIC | PARSERS.FUNCTION | PARSERS.SEGMENT; + var list = parseList(stream, pos, TOKENS.BAR, parsers); + + if (list.value.length > 1) { + return createAst(SYMBOLS.UNION, list.value, list.length); + } +}); + +var parseGenericType = nullable(function(stream, pos, ast) { + var genericAst, typeAst; + if ((genericAst = parseSimpleType(stream, pos)) && + stream[pos + genericAst.length].type == TOKENS.OPENGENERIC && + (typeAst = parseAnyType(stream, pos += genericAst.length + 1))) { + + if (stream[pos + typeAst.length].type != TOKENS.CLOSEGENERIC) { + throwError('Missing ' + TOKENS.CLOSEGENERIC + + ' at pos: ' + pos + typeAst.length); + } + + return createAst(SYMBOLS.GENERIC, [genericAst, typeAst], + genericAst.length + typeAst.length + 2); + } +}); + +var parseFunctionType = nullable(function(stream, pos) { + if (stream[pos].type == TOKENS.STRING && + stream[pos].value == 'function' && + stream[++pos].type == TOKENS.OPENPAREN) { + + var list = stream[pos + 1].type != TOKENS.CLOSEPAREN + ? parseList(stream, pos + 1, TOKENS.COMMA) + : {value: [], length: 0}; + + pos += list.length + 1; + + if (stream[pos].type == TOKENS.CLOSEPAREN) { + var length = list.length + 3, returnAst; + + if (stream[++pos].type == TOKENS.COLON) { + returnAst = parseAnyType(stream, ++pos); + if (!returnAst) { + throwError('Could not parse return type at pos: ' + pos); + } + length += returnAst.length + 1; + } + return createAst(SYMBOLS.FUNCTION, [list.value, returnAst || null], + length); + } + } +}); + +function parseSegmentType(stream, pos) { + var segmentAst; + if (stream[pos].type == TOKENS.OPENSEGMENT && + (segmentAst = parseAnyType(stream, ++pos))) { + pos += segmentAst.length; + if (stream[pos].type == TOKENS.CLOSESEGMENT) { + return createAst(SYMBOLS.SEGMENT, segmentAst, segmentAst.length + 2); + } + } +} + +function parseAnyType(stream, pos, parsers) { + if (!parsers) { + parsers = + PARSERS.SEGMENT | PARSERS.SIMPLE | PARSERS.UNION | PARSERS.GENERIC + | PARSERS.FUNCTION; + } + + var ast = + (parsers & PARSERS.UNION && parseUnionType(stream, pos)) || + (parsers & PARSERS.SEGMENT && parseSegmentType(stream, pos)) || + (parsers & PARSERS.GENERIC && parseGenericType(stream, pos)) || + (parsers & PARSERS.FUNCTION && parseFunctionType(stream, pos)) || + (parsers & PARSERS.SIMPLE && parseSimpleType(stream, pos)); + if (!ast) { + throwError('Could not parse ' + stream[pos].type); + } + return ast; +} + +function parseList(stream, pos, separator, parsers) { + var symbols = [], childAst, length = 0, separators = 0; + while (true) { + if (childAst = parseAnyType(stream, pos, parsers)) { + symbols.push(childAst); + length += childAst.length; + pos += childAst.length; + + if (stream[pos].type == separator) { + length++; + pos++; + separators++; + continue; + } + } + break; + } + + if (symbols.length && symbols.length != separators + 1) { + throwError('Malformed list expression'); + } + + return { + value: symbols, + length: length + }; +} + +var _source; +function throwError(msg) { + throw new Error(msg + '\nSource: ' + _source); +} + + +function parse(source) { + _source = source; + var stream = createTokenStream(source); + var ast = parseAnyType(stream, 0); + if (ast) { + if (ast.length + 1 != stream.length) { + console.log(ast); + throwError('Could not parse ' + stream[ast.length].type + + ' at token pos:' + ast.length); + } + return ast; + } else { + throwError('Failed to parse the source'); + } +} + +exports.createTokenStream = createTokenStream; +exports.parse = parse; +exports.parseList = parseList; + +/*----- compiler -----*/ + +var compilers = {}; + +compilers[SYMBOLS.SIMPLE] = function(ast) { + if (ast.value === 'DOMElement') { + return 'HTMLElement'; + } + return ast.value; +}; + +compilers[SYMBOLS.UNION] = function(ast) { + return ast.value.map(function(symbol) { + return compile(symbol); + }).join(TOKENS.BAR); +}; + +compilers[SYMBOLS.GENERIC] = function(ast) { + var type = compile(ast.value[0]); + var parametricType = compile(ast.value[1]); + if (type === 'HTMLElement') { + return 'HTML' + getTagName(parametricType) + 'Element'; + } + return type + '<' + parametricType + '>'; +}; + +compilers[SYMBOLS.FUNCTION] = function(ast) { + return 'function(' + ast.value[0].map(function(symbol) { + return compile(symbol); + }).join(TOKENS.COMMA) + ')' + + (ast.value[1] ? ':' + compile(ast.value[1]) : ''); +}; + +function compile(ast) { + return (ast.nullable ? '?' : '') + compilers[ast.type](ast); +} + +exports.compile = compile; + +/*----- normalizer -----*/ + +function normalize(ast) { + if (ast.type === SYMBOLS.UNION) { + return ast.value.map(normalize).reduce(function(list, nodes) { + return list ? list.concat(nodes) : nodes; + }); + } + + var valueNodes = ast.type === SYMBOLS.GENERIC + ? normalize(ast.value[1]) + : [ast.value]; + + return valueNodes.map(function(valueNode) { + return createAst( + ast.type, + ast.type === SYMBOLS.GENERIC + ? [ast.value[0], valueNode] + : valueNode, + ast.length); + }); +} + +exports.normalize = function(ast) { + var normalized = normalize(ast); + normalized = normalized.length === 1 + ? normalized[0] + : createAst(SYMBOLS.UNION, normalized, normalized.length); + if (ast.nullable) { + normalized.nullable = true; + } + return normalized; +}; + +/*----- Tracking TypeAliases -----*/ + +function initTypeAliasTracking(state) { + state.g.typeAliasScopes = []; +} + +function pushTypeAliases(state, typeAliases) { + state.g.typeAliasScopes.unshift(typeAliases); +} + +function popTypeAliases(state) { + state.g.typeAliasScopes.shift(); +} + +function getTypeAlias(id, state) { + var typeAliasScopes = state.g.typeAliasScopes; + for (var ii = 0; ii < typeAliasScopes.length; ii++) { + var typeAliasAnnotation = typeAliasScopes[ii][id.name]; + if (typeAliasAnnotation) { + return typeAliasAnnotation; + } + } + return null; +} + +exports.initTypeAliasTracking = initTypeAliasTracking; +exports.pushTypeAliases = pushTypeAliases; +exports.popTypeAliases = popTypeAliases; + +/*----- Tracking which TypeVariables are in scope -----*/ +// Counts how many scopes deep each type variable is + +function initTypeVariableScopeTracking(state) { + state.g.typeVariableScopeDepth = {}; +} + +function pushTypeVariables(node, state) { + var parameterDeclaration = node.typeParameters, scopeHistory; + + if (parameterDeclaration !== null && parameterDeclaration !== undefined + && parameterDeclaration.type === Syntax.TypeParameterDeclaration) { + parameterDeclaration.params.forEach(function (id) { + scopeHistory = state.g.typeVariableScopeDepth[id.name] || 0; + state.g.typeVariableScopeDepth[id.name] = scopeHistory + 1; + }); + } +} + +function popTypeVariables(node, state) { + var parameterDeclaration = node.typeParameters, scopeHistory; + + if (parameterDeclaration !== null && parameterDeclaration !== undefined + && parameterDeclaration.type === Syntax.TypeParameterDeclaration) { + parameterDeclaration.params.forEach(function (id) { + scopeHistory = state.g.typeVariableScopeDepth[id.name]; + state.g.typeVariableScopeDepth[id.name] = scopeHistory - 1; + }); + } +} + +function isTypeVariableInScope(id, state) { + return state.g.typeVariableScopeDepth[id.name] > 0; +} + +exports.initTypeVariableScopeTracking = initTypeVariableScopeTracking; +exports.pushTypeVariables = pushTypeVariables; +exports.popTypeVariables = popTypeVariables; + +/*----- FromFlowToTypechecks -----*/ + +function fromFlowAnnotation(/*object*/ annotation, state) /*?object*/ { + var ast; + switch (annotation.type) { + case "NumberTypeAnnotation": + return createAst(SYMBOLS.SIMPLE, "number", 0); + case "StringTypeAnnotation": + return createAst(SYMBOLS.SIMPLE, "string", 0); + case "BooleanTypeAnnotation": + return createAst(SYMBOLS.SIMPLE, "boolean", 0); + case "AnyTypeAnnotation": // fallthrough + case "VoidTypeAnnotation": + return null; + case "NullableTypeAnnotation": + ast = fromFlowAnnotation(annotation.typeAnnotation, state); + if (ast) { + ast.nullable = true; + } + return ast; + case 'ObjectTypeAnnotation': + // ObjectTypeAnnotation is always converted to a simple object type, as we + // don't support records + return createAst(SYMBOLS.SIMPLE, 'object', 0); + case 'FunctionTypeAnnotation': + var params = annotation.params + .map(function(param) { + return fromFlowAnnotation(param.typeAnnotation, state); + }) + .filter(function(ast) { + return !!ast; + }); + + var returnType = fromFlowAnnotation(annotation.returnType, state); + + // If any of the params have a type that cannot be expressed, then we have + // to render a simple function instead of a detailed one + if ((params.length || returnType) + && params.length === annotation.params.length) { + return createAst(SYMBOLS.FUNCTION, [params, returnType], 0); + } + return createAst(SYMBOLS.SIMPLE, 'function', 0); + case "GenericTypeAnnotation": + var alias = getTypeAlias(annotation.id, state); + if (alias) { + return fromFlowAnnotation(alias, state); + } + + // Qualified type identifiers are not handled by runtime typechecker, + // so simply omit the annotation for now. + if (annotation.id.type === "QualifiedTypeIdentifier") { + return null; + } + + if (isTypeVariableInScope(annotation.id, state)) { + return null; + } + + var name = annotation.id.name; + var nameLowerCased = name.toLowerCase(); + if (BLACKLISTED.hasOwnProperty(name)) { + return null; + } + if (SIMPLETYPES.hasOwnProperty(nameLowerCased)) { + name = nameLowerCased; + } + + var id = createAst( + SYMBOLS.SIMPLE, + name, + 0 + ); + + switch (name) { + case "mixed": // fallthrough + case "$Enum": + // Not supported + return null; + case "array": // fallthrough + case "promise": + if (annotation.typeParameters) { + var parametricAst = fromFlowAnnotation( + annotation.typeParameters.params[0], + state + ); + if (parametricAst) { + return createAst( + SYMBOLS.GENERIC, + [id, parametricAst], + 0 + ); + } + } + break; + case '$Either': + if (annotation.typeParameters) { + return createAst( + SYMBOLS.UNION, + annotation.typeParameters.params.map( + function (node) { return fromFlowAnnotation(node, state); } + ), + 0 + ); + } + return null; + } + return id; + } + return null; +} + +exports.fromFlow = function(/*object*/ annotation, state) /*?string*/ { + var ast = fromFlowAnnotation(annotation, state); + return ast ? compile(ast) : null; +}; diff --git a/visitors/__tests__/es6-template-visitors-test.js b/visitors/__tests__/es6-template-visitors-test.js index 97ea846..b914b5c 100644 --- a/visitors/__tests__/es6-template-visitors-test.js +++ b/visitors/__tests__/es6-template-visitors-test.js @@ -154,7 +154,6 @@ describe('ES6 Template Visitor', function() { } ); - /*global args*/ expectEvalTag( "tag`a\nb\n${c}\nd`", (elements, ...args) => { diff --git a/visitors/__tests__/gen/generate-type-syntax-test.js b/visitors/__tests__/gen/generate-type-syntax-test.js index 8cc0943..21e2eb5 100644 --- a/visitors/__tests__/gen/generate-type-syntax-test.js +++ b/visitors/__tests__/gen/generate-type-syntax-test.js @@ -1,3 +1,19 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /*jshint evil:true*/ var tests = require("./../type-syntax-test"); diff --git a/visitors/__tests__/gen/type-syntax-test.rec.js b/visitors/__tests__/gen/type-syntax-test.rec.js index 22eb360..4b3f2f0 100644 --- a/visitors/__tests__/gen/type-syntax-test.rec.js +++ b/visitors/__tests__/gen/type-syntax-test.rec.js @@ -1,6 +1,22 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /* * WARNING: This file is autogenerated by visitors/__tests__/gen/generate-type-syntax-test.js -* Do NOT modify this file directly! Instead, add your tests to +* Do NOT modify this file directly! Instead, add your tests to * visitors/__tests__/type-syntax-test.js and run visitors/__tests__/gen/generate-type-syntax-test.js */ @@ -8,97 +24,97 @@ module.exports = { 'TypeAnnotations': { 'function foo(numVal: any){}': { raworiginal: 'function foo(numVal: any){}', - transformed: 'function foo(numVal ){}', + transformed: 'function foo(numVal){}', eval: 'No error', }, 'function foo(numVal: number){}': { raworiginal: 'function foo(numVal: number){}', - transformed: 'function foo(numVal ){}', + transformed: 'function foo(/*number*/ numVal){}', eval: 'No error', }, 'function foo(numVal: number, strVal: string){}': { raworiginal: 'function foo(numVal: number, strVal: string){}', - transformed: 'function foo(numVal , strVal ){}', + transformed: 'function foo(/*number*/ numVal, /*string*/ strVal){}', eval: 'No error', }, 'function foo(numVal: number, untypedVal){}': { raworiginal: 'function foo(numVal: number, untypedVal){}', - transformed: 'function foo(numVal , untypedVal){}', + transformed: 'function foo(/*number*/ numVal, untypedVal){}', eval: 'No error', }, 'function foo(untypedVal, numVal: number){}': { raworiginal: 'function foo(untypedVal, numVal: number){}', - transformed: 'function foo(untypedVal, numVal ){}', + transformed: 'function foo(untypedVal, /*number*/ numVal){}', eval: 'No error', }, 'function foo(nullableNum: ?number){}': { raworiginal: 'function foo(nullableNum: ?number){}', - transformed: 'function foo(nullableNum ){}', + transformed: 'function foo(/*?number*/ nullableNum){}', eval: 'No error', }, 'function foo(callback: () => void){}': { raworiginal: 'function foo(callback: () => void){}', - transformed: 'function foo(callback ){}', + transformed: 'function foo(/*function*/ callback){}', eval: 'No error', }, 'function foo(callback: () => number){}': { raworiginal: 'function foo(callback: () => number){}', - transformed: 'function foo(callback ){}', + transformed: 'function foo(/*function():number*/ callback){}', eval: 'No error', }, 'function foo(callback: (_:bool) => number){}': { raworiginal: 'function foo(callback: (_:bool) => number){}', - transformed: 'function foo(callback ){}', + transformed: 'function foo(/*function(boolean):number*/ callback){}', eval: 'No error', }, 'function foo(callback: (_1:bool, _2:string) => number){}': { raworiginal: 'function foo(callback: (_1:bool, _2:string) => number){}', - transformed: 'function foo(callback ){}', + transformed: 'function foo(/*function(boolean,string):number*/ callback){}', eval: 'No error', }, 'function foo(callback: (_1:bool, ...foo:Array) => number){}': { raworiginal: 'function foo(callback: (_1:bool, ...foo:Array) => number){}', - transformed: 'function foo(callback ){}', + transformed: 'function foo(/*function(boolean):number*/ callback){}', eval: 'No error', }, 'function foo():number{}': { raworiginal: 'function foo():number{}', - transformed: 'function foo() {}', + transformed: 'function foo() /*number*/{}', eval: 'No error', }, 'function foo():() => void{}': { raworiginal: 'function foo():() => void{}', - transformed: 'function foo() {}', + transformed: 'function foo() /*function*/{}', eval: 'No error', }, 'function foo():(_:bool) => number{}': { raworiginal: 'function foo():(_:bool) => number{}', - transformed: 'function foo() {}', + transformed: 'function foo() /*function(boolean):number*/{}', eval: 'No error', }, 'function foo():(_?:bool) => number{}': { raworiginal: 'function foo():(_?:bool) => number{}', - transformed: 'function foo() {}', + transformed: 'function foo() /*function(boolean):number*/{}', eval: 'No error', }, 'function foo(): {} {}': { raworiginal: 'function foo(): {} {}', - transformed: 'function foo() {}', + transformed: 'function foo() /*object*/ {}', eval: 'No error', }, @@ -122,121 +138,121 @@ module.exports = { }, 'a={set fooProp(value:number){}}': { raworiginal: 'a={set fooProp(value:number){}}', - transformed: 'a={set fooProp(value ){}}', + transformed: 'a={set fooProp(/*number*/ value){}}', eval: 'No error', }, 'a={set fooProp(value:number): void{}}': { raworiginal: 'a={set fooProp(value:number): void{}}', - transformed: 'a={set fooProp(value ) {}}', + transformed: 'a={set fooProp(/*number*/ value){}}', eval: 'No error', }, 'a={get fooProp(): number {}}': { raworiginal: 'a={get fooProp(): number {}}', - transformed: 'a={get fooProp() {}}', + transformed: 'a={get fooProp() /*number*/ {}}', eval: 'No error', }, 'class Foo {set fooProp(value:number){}}': { raworiginal: 'class Foo {set fooProp(value:number){}}', - transformed: 'class Foo {set fooProp(value ){}}', + transformed: 'class Foo {set fooProp(/*number*/ value){}}', eval: 'Unexpected reserved word', }, 'class Foo {set fooProp(value:number): void{}}': { raworiginal: 'class Foo {set fooProp(value:number): void{}}', - transformed: 'class Foo {set fooProp(value ) {}}', + transformed: 'class Foo {set fooProp(/*number*/ value){}}', eval: 'Unexpected reserved word', }, 'class Foo {get fooProp(): number{}}': { raworiginal: 'class Foo {get fooProp(): number{}}', - transformed: 'class Foo {get fooProp() {}}', + transformed: 'class Foo {get fooProp() /*number*/{}}', eval: 'Unexpected reserved word', }, 'var numVal:number;': { raworiginal: 'var numVal:number;', - transformed: 'var numVal ;', + transformed: 'var /*number*/ numVal;', eval: 'No error', }, 'var numVal:number = otherNumVal;': { raworiginal: 'var numVal:number = otherNumVal;', - transformed: 'var numVal = otherNumVal;', + transformed: 'var /*number*/ numVal = otherNumVal;', eval: 'otherNumVal is not defined', }, 'var a: {numVal: number};': { raworiginal: 'var a: {numVal: number};', - transformed: 'var a ;', + transformed: 'var /*object*/ a;', eval: 'No error', }, 'var a: {numVal: number;};': { raworiginal: 'var a: {numVal: number;};', - transformed: 'var a ;', + transformed: 'var /*object*/ a;', eval: 'No error', }, 'var a: {numVal: number; [indexer: string]: number};': { raworiginal: 'var a: {numVal: number; [indexer: string]: number};', - transformed: 'var a ;', + transformed: 'var /*object*/ a;', eval: 'No error', }, 'var a: ?{numVal: number};': { raworiginal: 'var a: ?{numVal: number};', - transformed: 'var a ;', + transformed: 'var /*?object*/ a;', eval: 'No error', }, 'var a: {numVal: number; strVal: string}': { raworiginal: 'var a: {numVal: number; strVal: string}', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a: {subObj: {strVal: string}}': { raworiginal: 'var a: {subObj: {strVal: string}}', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a: {subObj: ?{strVal: string}}': { raworiginal: 'var a: {subObj: ?{strVal: string}}', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a: {param1: number; param2: string}': { raworiginal: 'var a: {param1: number; param2: string}', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a: {param1: number; param2?: string}': { raworiginal: 'var a: {param1: number; param2?: string}', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a: {add(x:number, ...y:Array): void}': { raworiginal: 'var a: {add(x:number, ...y:Array): void}', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a: { id(x: T): T; }': { raworiginal: 'var a: { id(x: T): T; }', - transformed: 'var a ', + transformed: 'var /*object*/ a', eval: 'No error', }, 'var a:Array = [1, 2, 3]': { raworiginal: 'var a:Array = [1, 2, 3]', - transformed: 'var a = [1, 2, 3]', + transformed: 'var /*array*/ a = [1, 2, 3]', eval: 'No error', }, @@ -272,7 +288,7 @@ module.exports = { }, 'class Foo { bar():number { return 42; }}': { raworiginal: 'class Foo { bar():number { return 42; }}', - transformed: 'class Foo { bar() { return 42; }}', + transformed: 'class Foo { bar() /*number*/ { return 42; }}', eval: 'Unexpected reserved word', }, @@ -296,31 +312,31 @@ module.exports = { }, 'var x : number | string = 4;': { raworiginal: 'var x : number | string = 4;', - transformed: 'var x = 4;', + transformed: 'var x = 4;', eval: 'No error', }, 'class Array { concat(items:number | string) {}; }': { raworiginal: 'class Array { concat(items:number | string) {}; }', - transformed: 'class Array { concat(items ) {}; }', + transformed: 'class Array { concat(items) {}; }', eval: 'Unexpected reserved word', }, 'var x : () => number | () => string = fn;': { raworiginal: 'var x : () => number | () => string = fn;', - transformed: 'var x = fn;', + transformed: 'var /*function*/ x = fn;', eval: 'fn is not defined', }, 'var x: typeof Y = Y;': { raworiginal: 'var x: typeof Y = Y;', - transformed: 'var x = Y;', + transformed: 'var x = Y;', eval: 'Y is not defined', }, 'var x: typeof Y | number = Y;': { raworiginal: 'var x: typeof Y | number = Y;', - transformed: 'var x = Y;', + transformed: 'var x = Y;', eval: 'Y is not defined', }, @@ -356,55 +372,55 @@ module.exports = { }, 'function foo(...rest: Array) {}': { raworiginal: 'function foo(...rest: Array) {}', - transformed: 'function foo(...rest ) {}', + transformed: 'function foo(.../*array*/ rest) {}', eval: 'Unexpected token .', }, '(function (...rest: Array) {})': { raworiginal: '(function (...rest: Array) {})', - transformed: '(function (...rest ) {})', + transformed: '(function (.../*array*/ rest) {})', eval: 'Unexpected token .', }, '((...rest: Array) => rest)': { raworiginal: '((...rest: Array) => rest)', - transformed: '((...rest ) => rest)', + transformed: '((.../*array*/ rest) => rest)', eval: 'Unexpected token .', }, 'var a: Map >': { raworiginal: 'var a: Map >', - transformed: 'var a ', + transformed: 'var /*map*/ a', eval: 'No error', }, 'var a: Map>': { raworiginal: 'var a: Map>', - transformed: 'var a ', + transformed: 'var /*map*/ a', eval: 'No error', }, 'var a: number[]': { raworiginal: 'var a: number[]', - transformed: 'var a ', + transformed: 'var a', eval: 'No error', }, 'var a: ?string[]': { raworiginal: 'var a: ?string[]', - transformed: 'var a ', + transformed: 'var a', eval: 'No error', }, 'var a: Promise[]': { raworiginal: 'var a: Promise[]', - transformed: 'var a ', + transformed: 'var a', eval: 'No error', }, 'var a:(...rest:Array) => number': { raworiginal: 'var a:(...rest:Array) => number', - transformed: 'var a ', + transformed: 'var /*function():number*/ a', eval: 'No error', }, @@ -482,25 +498,25 @@ module.exports = { 'Type Grouping': { 'var a: (number)': { raworiginal: 'var a: (number)', - transformed: 'var a ', + transformed: 'var /*number*/ a', eval: 'No error', }, 'var a: (() => number) | () => string': { raworiginal: 'var a: (() => number) | () => string', - transformed: 'var a ', + transformed: 'var a', eval: 'No error', }, 'var a: number & (string | bool)': { raworiginal: 'var a: number & (string | bool)', - transformed: 'var a ', + transformed: 'var a', eval: 'No error', }, 'var a: (typeof A)': { raworiginal: 'var a: (typeof A)', - transformed: 'var a ', + transformed: 'var a', eval: 'No error', }, @@ -630,25 +646,25 @@ module.exports = { 'Call Properties': { 'var a : { (): number }': { raworiginal: 'var a : { (): number }', - transformed: 'var a ', + transformed: 'var /*object*/ a ', eval: 'No error', }, 'var a : { (): number; }': { raworiginal: 'var a : { (): number; }', - transformed: 'var a ', + transformed: 'var /*object*/ a ', eval: 'No error', }, 'var a : { (): number; y: string; (x: string): string }': { raworiginal: 'var a : { (): number; y: string; (x: string): string }', - transformed: 'var a ', + transformed: 'var /*object*/ a ', eval: 'No error', }, 'var a : { (x: T): number; }': { raworiginal: 'var a : { (x: T): number; }', - transformed: 'var a ', + transformed: 'var /*object*/ a ', eval: 'No error', }, @@ -662,13 +678,13 @@ module.exports = { 'String Literal Types': { 'function createElement(tagName: "div"): HTMLDivElement {}': { raworiginal: 'function createElement(tagName: "div"): HTMLDivElement {}', - transformed: 'function createElement(tagName ) {}', + transformed: 'function createElement(tagName) /*HTMLDivElement*/ {}', eval: 'No error', }, 'function createElement(tagName: \'div\'): HTMLDivElement {}': { raworiginal: 'function createElement(tagName: \'div\'): HTMLDivElement {}', - transformed: 'function createElement(tagName ) {}', + transformed: 'function createElement(tagName) /*HTMLDivElement*/ {}', eval: 'No error', }, @@ -676,25 +692,25 @@ module.exports = { 'Qualified Generic Type': { 'var a : A.B': { raworiginal: 'var a : A.B', - transformed: 'var a ', + transformed: 'var a ', eval: 'No error', }, 'var a : A.B.C': { raworiginal: 'var a : A.B.C', - transformed: 'var a ', + transformed: 'var a ', eval: 'No error', }, 'var a : A.B': { raworiginal: 'var a : A.B', - transformed: 'var a ', + transformed: 'var a ', eval: 'No error', }, 'var a : typeof A.B': { raworiginal: 'var a : typeof A.B', - transformed: 'var a ', + transformed: 'var a ', eval: 'No error', }, diff --git a/visitors/__tests__/type-alias-syntax-test.js b/visitors/__tests__/type-alias-syntax-test.js index 3e7966a..1d84263 100644 --- a/visitors/__tests__/type-alias-syntax-test.js +++ b/visitors/__tests__/type-alias-syntax-test.js @@ -15,6 +15,7 @@ */ /*jshint evil:true*/ +/*jshint -W117*/ require('mock-modules').autoMockOff(); @@ -47,7 +48,6 @@ describe('static type syntax syntax', function() { describe('type alias', () => { it('strips type aliases', () => { - /*global type*/ var code = transform([ 'var type = 42;', 'type FBID = number;', @@ -57,5 +57,16 @@ describe('static type syntax syntax', function() { eval(code); expect(type).toBe(84); }); + + it('catches up correctly', () => { + var code = transform([ + "var X = require('X');", + 'type FBID = number;', + ]); + expect(code).toBe([ + "var X = require('X');", + ' ' + ].join('\n')); + }); }); }); diff --git a/visitors/__tests__/type-class-syntax-test.js b/visitors/__tests__/type-class-syntax-test.js index de8a6d4..7dbe511 100644 --- a/visitors/__tests__/type-class-syntax-test.js +++ b/visitors/__tests__/type-class-syntax-test.js @@ -29,7 +29,7 @@ describe('static type class syntax', function() { require('mock-modules').dumpCache(); classSyntaxVisitors = - require('../es6-class-visitors').visitorList; + require('jstransform/visitors/es6-class-visitors').visitorList; flowSyntaxVisitors = require('../type-syntax.js').visitorList; jstransform = require('jstransform'); @@ -133,7 +133,7 @@ describe('static type class syntax', function() { it('strips annotated params before a rest parameter', () => { var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'class Foo {', diff --git a/visitors/__tests__/type-function-syntax-test.js b/visitors/__tests__/type-function-syntax-test.js index ed465a3..de79465 100644 --- a/visitors/__tests__/type-function-syntax-test.js +++ b/visitors/__tests__/type-function-syntax-test.js @@ -115,7 +115,7 @@ describe('static type function syntax', function() { it('strips annotated params before a rest parameter', () => { var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'function foo(param1: number, ...args) {', @@ -133,7 +133,7 @@ describe('static type function syntax', function() { it('strips annotated rest parameter', () => { var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'function foo(param1: number, ...args: Array) {', @@ -221,7 +221,7 @@ describe('static type function syntax', function() { ' param1();', '}' ], - require('../es6-rest-param-visitors').visitorList + require('jstransform/visitors/es6-rest-param-visitors').visitorList ); eval(code); @@ -272,7 +272,7 @@ describe('static type function syntax', function() { it('strips multi-parameter type annotations', () => { var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'function foo(param1) {', @@ -290,9 +290,9 @@ describe('static type function syntax', function() { }); describe('arrow functions', () => { - // TODO: We don't currently support arrow functions, but we should - // soon! The only reason we don't now is because we don't - // need it at this very moment and I'm in a rush to get the - // basics in. + // TODO(#3241230): We don't currently support arrow functions, but we should + // soon! The only reason we don't now is because we don't + // need it at this very moment and I'm in a rush to get the + // basics in. }); }); diff --git a/visitors/__tests__/type-interface-syntax-test.js b/visitors/__tests__/type-interface-syntax-test.js index eb63d07..589c925 100644 --- a/visitors/__tests__/type-interface-syntax-test.js +++ b/visitors/__tests__/type-interface-syntax-test.js @@ -15,6 +15,7 @@ */ /*jshint evil:true*/ +/*jshint -W117*/ require('mock-modules').autoMockOff(); @@ -46,7 +47,6 @@ describe('static type interface syntax', function() { describe('Interface Declaration', () => { it('strips interface declarations', () => { - /*global interface*/ var code = transform([ 'var interface = 42;', 'interface A { foo: () => number; }', @@ -57,5 +57,16 @@ describe('static type interface syntax', function() { eval(code); expect(interface).toBe(126); }); + + it('catches up correctly', () => { + var code = transform([ + "var X = require('X');", + 'interface A { foo: () => number; }', + ]); + expect(code).toBe([ + "var X = require('X');", + ' ' + ].join('\n')); + }); }); }); diff --git a/visitors/__tests__/type-object-method-syntax-test.js b/visitors/__tests__/type-object-method-syntax-test.js index 27947d0..a338145 100644 --- a/visitors/__tests__/type-object-method-syntax-test.js +++ b/visitors/__tests__/type-object-method-syntax-test.js @@ -30,7 +30,7 @@ describe('static type object-method syntax', function() { flowSyntaxVisitors = require('../type-syntax.js').visitorList; jstransform = require('jstransform'); objMethodVisitors = - require('../es6-object-concise-method-visitors'); + require('jstransform/visitors/es6-object-concise-method-visitors'); visitorList = objMethodVisitors.visitorList; }); @@ -107,7 +107,7 @@ describe('static type object-method syntax', function() { it('strips annotated params before a rest parameter', () => { var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'var foo = {', @@ -122,7 +122,7 @@ describe('static type object-method syntax', function() { it('strips annotated rest parameter', () => { var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'var foo = {', @@ -169,7 +169,7 @@ describe('static type object-method syntax', function() { it('strips multi-parameter type annotations', () => { // TODO: Doesnt parse var restParamVisitors = - require('../es6-rest-param-visitors').visitorList; + require('jstransform/visitors/es6-rest-param-visitors').visitorList; var code = transform([ 'var foo = {', diff --git a/visitors/__tests__/type-pattern-syntax-test.js b/visitors/__tests__/type-pattern-syntax-test.js index 4460dca..26bf99e 100644 --- a/visitors/__tests__/type-pattern-syntax-test.js +++ b/visitors/__tests__/type-pattern-syntax-test.js @@ -29,7 +29,7 @@ describe('static type pattern syntax', function() { flowSyntaxVisitors = require('../type-syntax.js').visitorList; jstransform = require('jstransform'); destructuringVisitors = - require('../es6-destructuring-visitors'); + require('jstransform/visitors/es6-destructuring-visitors'); visitorList = destructuringVisitors.visitorList; }); diff --git a/visitors/__tests__/type-typechecks-test.js b/visitors/__tests__/type-typechecks-test.js new file mode 100644 index 0000000..d1e3218 --- /dev/null +++ b/visitors/__tests__/type-typechecks-test.js @@ -0,0 +1,345 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*jshint evil:true*/ +/*jshint -W117*/ + +require('mock-modules').autoMockOff(); + +describe('static type function syntax', function() { + var flowSyntaxVisitors; + var jstransform; + + beforeEach(function() { + require('mock-modules').dumpCache(); + + flowSyntaxVisitors = require('../type-syntax.js').visitorList; + jstransform = require('jstransform'); + }); + + function transform(code, visitors) { + code = code.join('\n'); + + // We run the flow transform first + code = jstransform.transform( + flowSyntaxVisitors, + code + ).code; + + if (visitors) { + code = jstransform.transform( + visitors, + code + ).code; + } + return code; + } + describe('@typechecks annotations', () => { + it('emits the proper annotations', () => { + expect(transform([ + 'function test(\n' + + ' arr: Array,\n' + + ' arrstr: Array,\n' + + ' arrarrstr: Array>,\n' + + ' mapstrstr: Map,\n' + + ' str: string,\n' + + ' obj: object,\n' + + ' Bool: Boolean,\n' + + ' bool: bool,\n' + + ' funvoid: ()=> void,\n' + + ' funsimple: (s:any, b:?bool)=> ?Regexp,\n' + + ' fun: (s:string, b:?bool)=> ?Regexp,\n' + + ' fungen: (s:Array, b:?bool)=> ?Regexp,\n' + + ' fununion: (s:$Either, b:?bool)=> ?Regexp,\n' + + ' prom: Promise,\n' + + ' prombool: Promise,\n' + + ' either: $Either,\n' + + ' eitherarraypromise: $Either, Promise>,\n' + + ' foobar: FooBar,\n' + + ' whoknows: mixed,\n' + + ' objlit: {},\n' + + ' qualified: A.B\n' + + '): Promise { }' + ])).toBe( + 'function test(\n' + + ' /*array*/ arr,\n' + + ' /*array*/ arrstr,\n' + + ' /*array>*/ arrarrstr,\n' + + ' /*map*/ mapstrstr,\n' + + ' /*string*/ str,\n' + + ' /*object*/ obj,\n' + + ' Bool,\n' + + ' /*boolean*/ bool,\n' + + ' /*function*/ funvoid,\n' + + ' /*function*/ funsimple,\n' + + ' /*function(string,?boolean):?regexp*/ fun,\n' + + ' /*function(array,?boolean):?regexp*/ fungen,\n' + + ' /*function(string|number,?boolean):?regexp*/ fununion,\n' + + ' /*promise*/ prom,\n' + + ' /*promise*/ prombool,\n' + + ' /*string|number*/ either,\n' + + ' /*array|promise*/ eitherarraypromise,\n' + + ' /*FooBar*/ foobar,\n' + + ' whoknows,\n' + + ' /*object*/ objlit,\n' + + ' qualified\n' + + ') /*promise*/ { }' + ); + }); + }); + + describe('resolving type variables in types', () => { + it('works on simple primitive aliases', () => { + expect(transform([ + 'type CustomNumber = number;\n' + + '\n' + + 'function test(\n' + + ' fooBar: CustomNumber\n' + + '): Promise { }' + ])).toBe( + ' \n' + + '\n' + + 'function test(\n' + + ' /*number*/ fooBar\n' + + ') /*promise*/ { }' + ); + }); + + it('works on simple generic aliases', () => { + expect(transform([ + 'type CustomArray = Array;\n' + + '\n' + + 'function test(\n' + + ' fooBar: CustomArray\n' + + '): Promise { }' + ])).toBe( + ' \n' + + '\n' + + 'function test(\n' + + ' /*array*/ fooBar\n' + + ') /*promise*/ { }' + ); + }); + + it('works on nested type aliases', () => { + expect(transform([ + 'type CustomArray = Array;\n' + + 'type MoreCustomArray = CustomArray;\n' + + '\n' + + 'function test(\n' + + ' fooBar: MoreCustomArray\n' + + '): Promise { }' + ])).toBe( + ' \n' + + ' \n' + + '\n' + + 'function test(\n' + + ' /*array*/ fooBar\n' + + ') /*promise*/ { }' + ); + }); + + it('hoists type aliases', () => { + expect(transform([ + 'function test(\n' + + ' fooBar: CustomNumber\n' + + '): Promise { }\n' + + '\n'+ + 'type CustomNumber = number;' + ])).toBe( + 'function test(\n' + + ' /*number*/ fooBar\n' + + ') /*promise*/ { }\n' + + '\n' + + ' ' + ); + }); + + it('hoists type aliases from the same closure', () => { + expect(transform([ + 'function main() {\n' + + ' function test(\n' + + ' fooBar: CustomNumber\n' + + ' ): Promise { }\n' + + ' type CustomNumber = number;\n' + + '}' + ])).toBe( + 'function main() {\n' + + ' function test(\n' + + ' /*number*/ fooBar\n' + + ' ) /*promise*/ { }\n' + + ' \n' + + '}' + ); + }); + + it('ignores type aliases from the nested closures', () => { + expect(transform([ + 'function main() {\n' + + ' type CustomNumber = number;\n' + + '}\n' + + 'function test(\n' + + ' fooBar: CustomNumber\n' + + '): Promise { }' + ])).toBe( + 'function main() {\n' + + ' \n' + + '}\n' + + 'function test(\n' + + ' /*CustomNumber*/ fooBar\n' + + ') /*promise*/ { }' + ); + }); + }); + + describe('ignoring type variables in types', () => { + it('works on function declarations', () => { + expect(transform([ + 'function test(): T {}' + ])).toBe( + 'function test () /*T*/ {}' + ); + }); + + it('works on function expressions', () => { + expect(transform([ + 'var a = function test(): T {};' + ])).toBe( + 'var a = function test () /*T*/ {};' + ); + expect(transform([ + 'var a = function(): T {};' + ])).toBe( + 'var a = function () /*T*/ {};' + ); + }); + + it('works on class declarations', () => { + classSyntaxVisitors = + require('jstransform/visitors/es6-class-visitors').visitorList; + expect(transform([ + 'class A {', + ' foo() { var a: T;}', + '}' + ], classSyntaxVisitors)).toBe([ + 'function A(){"use strict";}', + ' A.prototype.foo=function() {"use strict"; var /*T*/ a;};', + '' + ].join("\n")); + }); + + it('works on class expressions', () => { + classSyntaxVisitors = + require('jstransform/visitors/es6-class-visitors').visitorList; + expect(transform([ + 'var B = class A {', + ' foo() { var a: T;}', + '}' + ], classSyntaxVisitors)).toBe([ + 'var B = (function(){function A(){"use strict";}', + ' A.prototype.foo=function() {"use strict"; var /*T*/ a;};', + 'return A;})()' + ].join("\n")); + }); + + it('works in class methods', () => { + classSyntaxVisitors = + require('jstransform/visitors/es6-class-visitors').visitorList; + + expect(transform([ + 'class A {', + ' foo() {', + ' var a : T;', + ' }', + '};' + ], classSyntaxVisitors)).toBe([ + 'function A(){"use strict";}', + ' A.prototype.foo=function() {"use strict";', + ' var a ;', + ' };', + ';' + ].join("\n")); + }); + + it('works in object methods', () => { + // TODO (glevi): There's a bug in esprima that makes type variable + // declarations not work in object methods + }); + + it('works on multiple levels', () => { + expect(transform([ + 'function a() {', + ' function b(): T {', + ' var a: T;', + ' var b: S;', + ' }', + ' var a: T;', + ' var b: S;', + '}', + 'var a: T;', + 'var b: S;', + ])).toBe([ + 'function a () {', + ' function b () /*T*/ {', + ' var /*T*/ a;', + ' var /*S*/ b;', + ' }', + ' var /*T*/ a;', + ' var /*S*/ b;', + '}', + 'var /*T*/ a;', + 'var /*S*/ b;' + ].join("\n")); + }); + + it('works on complex types', () => { + expect(transform([ + 'function foo() {', + ' var a: ?T;', + ' var b: (x: T) => number;', + ' var c: (x: number) => T;', + ' var d: Foo;', + '}' + ])).toBe([ + 'function foo () {', + ' var /*?T*/ a;', + ' var /*function(T):number*/ b;', + ' var /*function(number):T*/ c;', + ' var /*Foo*/ d;', + '}' + ].join("\n")); + }); + + it('works on this multiline example', () => { + // I had this bug where I wasn't properly + expect(transform([ + 'var a = {', + ' pushState: function(', + ' data:object,', + ' title:string,', + ' uri:T) {}', + '};' + ])).toBe([ + 'var a = {', + ' pushState: function (', + ' /*object*/ data,', + ' /*string*/ title,', + ' /*T*/ uri) {}', + '};' + ].join("\n")); + }); + }); +}); diff --git a/visitors/type-syntax.js b/visitors/type-syntax.js index 15089cc..a3a9703 100644 --- a/visitors/type-syntax.js +++ b/visitors/type-syntax.js @@ -1,14 +1,147 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + var esprima = require('esprima-fb'); var utils = require('../src/utils'); +var TypeExpressionParser = require('./TypeExpressionParser'); var Syntax = esprima.Syntax; +function createTypehint(/*object*/ node, state) /*string*/ { + var typehint = TypeExpressionParser.fromFlow(node, state); + return typehint ? '/*' + typehint + '*/' : ''; +} + +/** + * Traverses each child of a given node in order as we would do normally, + * catching up as we go. + */ +function traverseOrderedChildren(traverse, node, path, state) { + if (node.type) { + path.unshift(node); + } + utils.getOrderedChildren(node).forEach(function(child) { + child.range && utils.catchup(child.range[0], state); + traverse(child, path, state); + child.range && utils.catchup(child.range[1], state); + }); + if (node.type) { + path.shift(); + } +} + +/** + * Traverse the AST to find and hoist all Flow type aliases within the current + * "type alias closure". (As of this writing, type alias closures are the same + * as function closures.) + */ +function hoistTypeAliases(node, state, callback) { + var typeAliases; + function collectTypeAliases(child) { + // Functions create a new type alias closure. + if (_isFunctionNode(child)) { + return; + } + if (child.type === Syntax.TypeAlias) { + typeAliases = typeAliases || {}; + typeAliases[child.id.name] = child.right; + } else { + utils.getOrderedChildren(child).forEach(function(child) { + collectTypeAliases(child, typeAliases); + }); + } + } + utils.getOrderedChildren(node).forEach(function(child) { + collectTypeAliases(child, typeAliases); + }); + if (typeAliases) { + TypeExpressionParser.pushTypeAliases(state, typeAliases); + callback(); + TypeExpressionParser.popTypeAliases(state); + } else { + callback(); + } +} + +function ignoreTypeVariables(node, state, callback) { + TypeExpressionParser.pushTypeVariables(node, state); + callback(); + TypeExpressionParser.popTypeVariables(node, state); +} + +function visitProgram(traverse, node, path, state) { + TypeExpressionParser.initTypeAliasTracking(state); + TypeExpressionParser.initTypeVariableScopeTracking(state); + hoistTypeAliases(node, state, function() { + traverseOrderedChildren(traverse, node, path, state); + }); + return false; +} +visitProgram.test = function(node, path, state) { + return node.type === Syntax.Program; +}; + +function visitFunctionOrTypeVariableDeclarator(traverse, node, path, state) { + var declaresTypeVariables = _declaresTypeVariables(node); + var declaresTypeAliasScope = _isFunctionNode(node); + + if (declaresTypeVariables && declaresTypeAliasScope) { + ignoreTypeVariables(node, state, function() { + hoistTypeAliases(node, state, function() { + traverseOrderedChildren(traverse, node, path, state); + }); + }); + } else if (declaresTypeVariables) { + ignoreTypeVariables(node, state, function() { + traverseOrderedChildren(traverse, node, path, state); + }); + } else { // declaresTypeAliasScope + hoistTypeAliases(node, state, function() { + traverseOrderedChildren(traverse, node, path, state); + }); + } + utils.catchup(node.range[1], state); + return false; +} +visitFunctionOrTypeVariableDeclarator.test = function(node, path, state) { + return _isFunctionNode(node) || _declaresTypeVariables(node); +}; + function _isFunctionNode(node) { return node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression || node.type === Syntax.ArrowFunctionExpression; } +function _declaresTypeVariables(node) { + switch (node.type) { + case Syntax.FunctionDeclaration: + case Syntax.FunctionExpression: + case Syntax.ClassDeclaration: + case Syntax.ClassExpression: + return node.typeParameters !== null && node.typeParameter !== undefined; + // Not handled: + // Interfaces - stripped out so not relevant + // Type aliases - stripped out so not relevant + // Methods - Handled below + default: + return false; + } +} + function visitClassProperty(traverse, node, path, state) { utils.catchup(node.range[0], state); utils.catchupWhiteOut(node.range[1], state); @@ -40,11 +173,10 @@ function visitDeclare(traverse, node, path, state) { } visitDeclare.test = function(node, path, state) { switch (node.type) { - case Syntax.DeclareVariable: - case Syntax.DeclareFunction: - case Syntax.DeclareClass: - case Syntax.DeclareModule: - return true; + case Syntax.DeclareVariable: + case Syntax.DeclareFunction: + case Syntax.DeclareClass: + case Syntax.DeclareModule: return true; } return false; }; @@ -63,7 +195,11 @@ visitFunctionParametricAnnotation.test = function(node, path, state) { function visitFunctionReturnAnnotation(traverse, node, path, state) { utils.catchup(node.range[0], state); - utils.catchupWhiteOut(node.range[1], state); + var typehint = createTypehint(node.typeAnnotation, state); + if (typehint) { + utils.append(' ' + typehint, state); + } + utils.move(node.range[1], state); return false; } visitFunctionReturnAnnotation.test = function(node, path, state) { @@ -83,8 +219,13 @@ visitOptionalFunctionParameterAnnotation.test = function(node, path, state) { }; function visitTypeAnnotatedIdentifier(traverse, node, path, state) { + utils.catchup(node.range[0], state); + var typehint = createTypehint(node.typeAnnotation.typeAnnotation, state); + if (typehint) { + utils.append(typehint + ' ', state); + } utils.catchup(node.typeAnnotation.range[0], state); - utils.catchupWhiteOut(node.typeAnnotation.range[1], state); + utils.move(node.typeAnnotation.range[1], state); return false; } visitTypeAnnotatedIdentifier.test = function(node, path, state) { @@ -117,6 +258,7 @@ function visitMethod(traverse, node, path, state) { path.unshift(node); traverse(node.key, path, state); + TypeExpressionParser.pushTypeVariables(node.value, state); path.unshift(node.value); traverse(node.value.params, path, state); node.value.rest && traverse(node.value.rest, path, state); @@ -124,6 +266,7 @@ function visitMethod(traverse, node, path, state) { traverse(node.value.body, path, state); path.shift(); + TypeExpressionParser.popTypeVariables(node.value, state); path.shift(); return false; @@ -142,6 +285,8 @@ exports.visitorList = [ visitFunctionReturnAnnotation, visitMethod, visitOptionalFunctionParameterAnnotation, + visitProgram, + visitFunctionOrTypeVariableDeclarator, visitTypeAlias, visitTypeAnnotatedIdentifier, visitTypeAnnotatedObjectOrArrayPattern