565 lines
14 KiB
JavaScript
565 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
/*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) {
|
|
switch (ast.value) {
|
|
case 'DOMElement': return 'HTMLElement';
|
|
case 'FBID': return 'string';
|
|
default: 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.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.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 (name !== 'Object' && 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;
|
|
};
|