Initial import of the lib to parse javascript code, in the same vein as we parse React proptypes
This commit is contained in:
parent
3d7b5d263a
commit
83581cfe6b
|
@ -0,0 +1,557 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*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;
|
||||
};
|
|
@ -0,0 +1,276 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*jslint node: true */
|
||||
"use strict";
|
||||
|
||||
var esprima = require('esprima');
|
||||
var Syntax = esprima.Syntax;
|
||||
var traverseFlat = require('./traverseFlat');
|
||||
|
||||
|
||||
/**
|
||||
* If the expression is an identifier, it is resolved in the scope chain.
|
||||
* If it is an assignment expression, it resolves to the right hand side.
|
||||
*
|
||||
* In all other cases the expression itself is returned.
|
||||
*
|
||||
* Since the scope chain constructed by the traverse function is very simple
|
||||
* (it doesn't take into account *changes* to the variable through assignment
|
||||
* statements), this function doesn't return the correct value in every
|
||||
* situation. But it's good enough for how it is used in the parser.
|
||||
*
|
||||
* @param {object} expr
|
||||
* @param {array} scopeChain
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
function resolveToValue(expr, scopeChain) {
|
||||
switch (expr.type) {
|
||||
case Syntax.AssignmentExpression:
|
||||
if (expr.operator === '=') {
|
||||
return resolveToValue(expr.right, scopeChain);
|
||||
}
|
||||
break;
|
||||
case Syntax.Identifier:
|
||||
var value;
|
||||
scopeChain.some(function(scope, i) {
|
||||
if (hasOwnProperty.call(scope, expr.name) && scope[expr.name]) {
|
||||
value = resolveToValue(scope[expr.name], scopeChain.slice(i));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the statement is of form `foo = bar;`.
|
||||
*
|
||||
* @param {object} node
|
||||
* @return {bool}
|
||||
*/
|
||||
function isAssignmentStatement(node) {
|
||||
return node.type === Syntax.ExpressionStatement &&
|
||||
node.expression.type === Syntax.AssignmentExpression &&
|
||||
node.expression.operator === '=';
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a member or call expression into parts. E.g. foo.bar.baz becomes
|
||||
* ['foo', 'bar', 'baz']
|
||||
*
|
||||
* @param {object} expr
|
||||
* @return {array}
|
||||
*/
|
||||
function expressionToArray(expr) {
|
||||
var parts = [];
|
||||
switch(expr.type) {
|
||||
case Syntax.CallExpression:
|
||||
parts = expressionToArray(expr.callee);
|
||||
break;
|
||||
case Syntax.MemberExpression:
|
||||
parts = expressionToArray(expr.object);
|
||||
if (expr.computed) {
|
||||
parts.push('...');
|
||||
} else {
|
||||
parts.push(expr.property.name || expr.property.value);
|
||||
}
|
||||
break;
|
||||
case Syntax.Identifier:
|
||||
parts = [expr.name];
|
||||
break;
|
||||
case Syntax.Literal:
|
||||
parts = [expr.raw];
|
||||
break;
|
||||
case Syntax.ThisExpression:
|
||||
parts = ['this'];
|
||||
break;
|
||||
case Syntax.ObjectExpression:
|
||||
var properties = expr.properties.map(function(property) {
|
||||
return expressionToString(property.key) +
|
||||
': ' +
|
||||
expressionToString(property.value);
|
||||
});
|
||||
parts = ['{' + properties.join(', ') + '}'];
|
||||
break;
|
||||
case Syntax.ArrayExpression:
|
||||
parts = ['[' + expr.elements.map(expressionToString).join(', ') + ']'];
|
||||
break;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string representation of a member expression.
|
||||
*
|
||||
* @param {object} expr
|
||||
* @return {array}
|
||||
*/
|
||||
function expressionToString(expr) {
|
||||
return expressionToArray(expr).join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the expression is of form `exports.foo = bar;` or
|
||||
* `modules.exports = foo;`.
|
||||
*
|
||||
* @param {object} node
|
||||
* @return {bool}
|
||||
*/
|
||||
function isExportsOrModuleExpression(expr) {
|
||||
if (expr.left.type !== Syntax.MemberExpression) {
|
||||
return false;
|
||||
}
|
||||
var exprArr = expressionToArray(expr.left);
|
||||
return (exprArr[0] === 'module' && exprArr[1] === 'exports') ||
|
||||
exprArr[0] == 'exports';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds module.exports / exports.X statements inside an assignment expression.
|
||||
*/
|
||||
function handleAssignmentExpression(expr, scopeChain, multipleExports) {
|
||||
while (!isExportsOrModuleExpression(expr)) {
|
||||
if (expr.type === Syntax.AssignmentExpression &&
|
||||
expr.right.type === Syntax.AssignmentExpression) {
|
||||
expr = expr.right;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var definition = resolveToValue(
|
||||
expr.right,
|
||||
scopeChain
|
||||
);
|
||||
|
||||
if (!definition) {
|
||||
// handle empty var declaration, e.g. "var x; ... module.exports = x"
|
||||
if (expr.right.type === Syntax.Identifier) {
|
||||
var found = false;
|
||||
scopeChain.some(function(scope) {
|
||||
if (scope[expr.right.name] === null) {
|
||||
return found = true;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
// fake definition so we still return something at least
|
||||
return {
|
||||
definition: {
|
||||
type: Syntax.VariableDeclaration,
|
||||
loc: expr.loc,
|
||||
isSynthesized: true
|
||||
},
|
||||
scopeChain: scopeChain
|
||||
};
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var leftExpression = expr.left;
|
||||
var leftExpressions = expressionToArray(leftExpression);
|
||||
if (leftExpressions[0] === 'exports') {
|
||||
// exports.A = A
|
||||
if (leftExpressions.length === 2 && leftExpression.property) {
|
||||
// The 2nd element is the field name
|
||||
multipleExports.push({
|
||||
key: leftExpression.property,
|
||||
value: definition
|
||||
});
|
||||
}
|
||||
} else if (definition) {
|
||||
// module.exports = A
|
||||
return {
|
||||
definition: definition,
|
||||
scopeChain: scopeChain
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an AST, this function tries to find the object expression that is the
|
||||
* module's exported value.
|
||||
*
|
||||
* @param {object} ast
|
||||
* @return {?object}
|
||||
*/
|
||||
function findExportDefinition(ast) {
|
||||
var multipleExports = [];
|
||||
var singleExport;
|
||||
traverseFlat(ast, function(node, scopeChain) {
|
||||
if (singleExport) {
|
||||
return false;
|
||||
}
|
||||
if (node.type === Syntax.VariableDeclaration) {
|
||||
node.declarations.forEach(function (decl) {
|
||||
if (!singleExport && decl.init &&
|
||||
decl.init.type === Syntax.AssignmentExpression) {
|
||||
singleExport = handleAssignmentExpression(
|
||||
decl.init,
|
||||
scopeChain,
|
||||
multipleExports
|
||||
);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (!isAssignmentStatement(node)) {
|
||||
return false;
|
||||
}
|
||||
if (node.expression) {
|
||||
singleExport = handleAssignmentExpression(
|
||||
node.expression,
|
||||
scopeChain,
|
||||
multipleExports
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// NOT going to handle the f**ked up case where in the same file we have
|
||||
// module.exports = A; exports.b = b;
|
||||
if (singleExport) {
|
||||
return singleExport;
|
||||
}
|
||||
|
||||
if (multipleExports.length === 1) {
|
||||
return {
|
||||
scopeChain: [],
|
||||
definition: multipleExports[0].value
|
||||
};
|
||||
}
|
||||
|
||||
if (multipleExports.length > 0) {
|
||||
// Synthesize an ObjectExpression union all exports
|
||||
var properties = multipleExports.map(function(element) {
|
||||
var key = element.key;
|
||||
var value = element.value;
|
||||
return {
|
||||
type: Syntax.Property,
|
||||
key: key,
|
||||
value: value,
|
||||
loc: {
|
||||
start: { line: key.loc.start.line, column: key.loc.start.column },
|
||||
end: { line: value.loc.end.line, column: value.loc.end.column }
|
||||
},
|
||||
range: [ key.range[0], value.range[1] ]
|
||||
};
|
||||
});
|
||||
return {
|
||||
scopeChain: [],
|
||||
definition: {
|
||||
isSynthesized: true,
|
||||
type: Syntax.ObjectExpression,
|
||||
properties: properties,
|
||||
// Use the first export statement location
|
||||
loc: properties[0].loc
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = findExportDefinition;
|
|
@ -0,0 +1,534 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*global exports:true*/
|
||||
/*jslint node:true*/
|
||||
"use strict";
|
||||
|
||||
var util = require('util');
|
||||
|
||||
var Syntax = require('esprima-fb').Syntax;
|
||||
var utils = require('jstransform/src/utils');
|
||||
|
||||
// Transforms
|
||||
var meta = require('./meta');
|
||||
var type = require('./type');
|
||||
|
||||
var typeHintExp = /^\??[\w<>|:(),?]+$/;
|
||||
var paramRe = /\*\s+@param\s+{?([^\s*{}.]+)}?(\s+([\w\$]+))?/g;
|
||||
var returnRe = /\*\s+@return(s?)\s+{?([^\s*{}.]+)}?/;
|
||||
|
||||
var nameToTransforms = {
|
||||
'sourcemeta': meta,
|
||||
'typechecks': type,
|
||||
};
|
||||
|
||||
var excludes = [];
|
||||
|
||||
function getTypeHintsFromDocBlock(node, docBlocksByLine) {
|
||||
var comments = docBlocksByLine[node.loc.start.line - 1];
|
||||
if (!comments) {
|
||||
return {
|
||||
params: null,
|
||||
returns: null
|
||||
};
|
||||
}
|
||||
|
||||
var params = [];
|
||||
if (node.params) {
|
||||
var paramNames = node.params.reduce(function(map, param) {
|
||||
map[param.name] = true;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
var param;
|
||||
while(param = paramRe.exec(comments.value)) {
|
||||
|
||||
if (!param[1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var functionName = node.id
|
||||
? '`' + node.id.name + '\''
|
||||
: '<anonymous>';
|
||||
|
||||
if (!param[3]) {
|
||||
throw new Error(util.format('Lines: %s-%s: Your @param declaration in' +
|
||||
' function %s is missing the parameter\'s name,' +
|
||||
' i.e. "@param {string} name"',
|
||||
comments.loc.start.line, comments.loc.end.line, functionName));
|
||||
}
|
||||
|
||||
// TODO(ostrulovich) if we're really nice, we should probably check edit
|
||||
// distance and suggest the right name the user meant
|
||||
if (!(param[3] in paramNames)) {
|
||||
throw new Error(util.format('Lines: %s-%s: `%s\' is not a valid ' +
|
||||
'formal parameter of function %s. Must be one of: %s',
|
||||
comments.loc.start.line, comments.loc.end.line, param[3],
|
||||
functionName, Object.keys(paramNames).join(', ')));
|
||||
}
|
||||
|
||||
params.push([param[3], param[1]]);
|
||||
}
|
||||
}
|
||||
var returnType = returnRe.exec(comments.value);
|
||||
if (returnType && returnType[1]) {
|
||||
throw new Error(util.format('Lines: %s-%s: Your @return declaration in' +
|
||||
' function %s is incorrectly written as @returns. Remove the trailing'+
|
||||
' \'s\'.',
|
||||
comments.loc.start.line, comments.loc.end.line, functionName));
|
||||
}
|
||||
return {
|
||||
params: params.length ? params : null,
|
||||
returns: returnType ? returnType[2] : null
|
||||
};
|
||||
}
|
||||
|
||||
function getTypeHintFromInline(node, commentsByLine) {
|
||||
var key = node.loc.start.column - 1;
|
||||
var comments = commentsByLine[node.loc.start.line];
|
||||
if (!comments || !(key in comments)) {
|
||||
return null;
|
||||
}
|
||||
// annotate the node
|
||||
node.typeHint = comments[key].value;
|
||||
return node.typeHint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out comments from AST
|
||||
* and populates commentsByLine and docBlocksByLine
|
||||
*/
|
||||
function parseComments(programNode, state) {
|
||||
programNode.comments.forEach(function(c) {
|
||||
if (c.type !== 'Block') return;
|
||||
|
||||
var comments;
|
||||
if (c.loc.start.line === c.loc.end.line &&
|
||||
typeHintExp.test(c.value)) {
|
||||
// inline comments
|
||||
comments = state.commentsByLine[c.loc.start.line] ||
|
||||
(state.commentsByLine[c.loc.start.line] = {});
|
||||
comments[c.loc.end.column] = c;
|
||||
|
||||
comments = state.commentsByLine[c.loc.end.line] ||
|
||||
(state.commentsByLine[c.loc.start.line] = {});
|
||||
comments[c.loc.end.column] = c;
|
||||
} else {
|
||||
// docblocks
|
||||
state.docBlocksByLine[c.loc.end.line] = c;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTypeHintParams(node, state) {
|
||||
// First look for typehints in the docblock.
|
||||
var typeHints = getTypeHintsFromDocBlock(node, state.docBlocksByLine);
|
||||
|
||||
// If not found, look inline.
|
||||
if (!typeHints.params && node.params) {
|
||||
typeHints.params = node.params.map(function(param, index) {
|
||||
return [param.name, getTypeHintFromInline(param, state.commentsByLine)];
|
||||
}).filter(function(param) {
|
||||
return param[1];
|
||||
});
|
||||
}
|
||||
if (!typeHints.returns) {
|
||||
typeHints.returns = getTypeHintFromInline(node.body, state.commentsByLine);
|
||||
}
|
||||
|
||||
return typeHints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameters needed for the dynamic typehint checks.
|
||||
*/
|
||||
function normalizeTypeHintParams(node, state, typeHints) {
|
||||
var preCond = [];
|
||||
if (typeHints.params.length > 0) {
|
||||
typeHints.params.forEach(function(typeHint) {
|
||||
if (typeHint[1]) {
|
||||
preCond.push([
|
||||
typeHint[0],
|
||||
'\''+ type.parseAndNormalize(typeHint[1], typeHint[0], node) +'\'',
|
||||
'\''+ typeHint[0] +'\''
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var postCond = null;
|
||||
if (typeHints.returns) {
|
||||
postCond = type.parseAndNormalize(typeHints.returns, 'return', node);
|
||||
}
|
||||
|
||||
// If static-only, then we don't need to pass the type hint
|
||||
// params since we're not going to do any dynamic checking.
|
||||
var pragmas = utils.getDocblock(state);
|
||||
if ('static-only' in pragmas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var typeHintParams = {};
|
||||
if (preCond.length > 0) {
|
||||
typeHintParams.params = preCond;
|
||||
}
|
||||
if (postCond) {
|
||||
typeHintParams.returns = postCond;
|
||||
}
|
||||
return (preCond.length || postCond) ? typeHintParams : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in all the various params on the function in the docblock or inline
|
||||
* comments and converts them into the format the bodyWrapper transform is
|
||||
* expecting. If there are no params needed, returns null.
|
||||
*
|
||||
* For example, for a docblock like so
|
||||
* @param {string} x
|
||||
* @param {number} y
|
||||
* @return {number}
|
||||
* the resulting params object would contain
|
||||
* {
|
||||
* params: [ [ 'x', 'number' ], [ 'y', 'number' ] ],
|
||||
* returns: 'number'
|
||||
* }
|
||||
*
|
||||
* However the bodyWrapper transform expects input like
|
||||
* {
|
||||
* params:
|
||||
* [ [ 'x', '\'number\'', '\'x\'' ],
|
||||
* [ 'y', '\'number\'', '\'y\'' ] ],
|
||||
* returns: 'number'
|
||||
* }
|
||||
*/
|
||||
function formatBodyParams(node, state, params) /*?object*/ {
|
||||
return normalizeTypeHintParams(node, state, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in all the various params on the function in the docblock or inline
|
||||
* comments and converts them into the format the annotator transform is
|
||||
* expecting. If there are no params needed, returns null.
|
||||
*
|
||||
* For example, for a docblock like so
|
||||
* @param {string} x
|
||||
* @param {number} y
|
||||
* @return {number}
|
||||
* the resulting params object would contain
|
||||
* {
|
||||
* params: [ [ 'x', 'number' ], [ 'y', 'number' ] ],
|
||||
* returns: 'number'
|
||||
* }
|
||||
*
|
||||
* However the bodyWrapper transform expects input like
|
||||
* {
|
||||
* params: [ 'number', 'number' ],
|
||||
* returns: 'number'
|
||||
* }
|
||||
*/
|
||||
function formatAnnotatorParams(params) /*?object*/ {
|
||||
if ((!params.params || params.params.length === 0) && !params.returns) {
|
||||
return null;
|
||||
}
|
||||
var annotatorParams = {};
|
||||
if (params.params && params.params.length > 0) {
|
||||
var paramTypes = [];
|
||||
params.params.forEach(function(paramArray) {
|
||||
paramTypes.push(paramArray[1]);
|
||||
});
|
||||
annotatorParams.params = paramTypes;
|
||||
}
|
||||
|
||||
if (params.returns) {
|
||||
annotatorParams.returns = params.returns;
|
||||
}
|
||||
|
||||
return annotatorParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used for determining how the params will be inlined
|
||||
* into the function transform. We can't just use utils.format
|
||||
* with %j because the way the data is stored in params vs
|
||||
* formatted is different.
|
||||
*/
|
||||
function renderParams(/*?object*/ params) /*string*/ {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var formattedParams = [];
|
||||
if (params.params && params.params.length > 0) {
|
||||
var preCond = params.params;
|
||||
var joined = preCond.map(function(cond) {
|
||||
return '[' + cond.join(', ') + ']';
|
||||
}).join(', ');
|
||||
var paramString = '\"params\":' + '[' + joined + ']';
|
||||
formattedParams.push(paramString);
|
||||
}
|
||||
|
||||
if (params.returns) {
|
||||
var returnParam = '\"returns\":' + '\'' + params.returns + '\'';
|
||||
formattedParams.push(returnParam);
|
||||
}
|
||||
return "{" + formattedParams.join(',') + "}";
|
||||
}
|
||||
|
||||
function getModuleName(state) {
|
||||
var docblock = utils.getDocblock(state);
|
||||
return docblock.providesModule || docblock.providesLegacy;
|
||||
}
|
||||
|
||||
function getFunctionMetadata(node, state) {
|
||||
var funcMeta = {
|
||||
module: getModuleName(state),
|
||||
line: node.loc.start.line,
|
||||
column: node.loc.start.column,
|
||||
name: node.id && node.id.name
|
||||
};
|
||||
if (!funcMeta.name) {
|
||||
delete funcMeta.name;
|
||||
}
|
||||
return funcMeta;
|
||||
}
|
||||
|
||||
function getNameToTransforms() {
|
||||
var filtered = {};
|
||||
Object.keys(nameToTransforms).forEach(function(name) {
|
||||
if (excludes.indexOf(name) == -1) {
|
||||
filtered[name] = nameToTransforms[name];
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are any transforms that would want to modify the
|
||||
* current source. Usually we can rule out some transforms because the top
|
||||
* pragma may say @nosourcemeta or there isn't a @typechecks. This function is
|
||||
* used to rule out sources where no transform applies.
|
||||
*
|
||||
* @param {object} state
|
||||
* @param {object} pragmas
|
||||
* @return {bool}
|
||||
*/
|
||||
function shouldTraverseFile(state, pragmas) {
|
||||
var t = false;
|
||||
var nameToTransforms = getNameToTransforms();
|
||||
Object.keys(nameToTransforms).forEach(function(value) {
|
||||
var transform = nameToTransforms[value];
|
||||
t = t || transform.shouldTraverseFile(state, pragmas);
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all the necessary information from the docblock and inline comments
|
||||
* that may be useful to a transform. Currently only the type transform has
|
||||
* information like @param and @return or the inline comments.
|
||||
*/
|
||||
function getAllParams(node, state) {
|
||||
if (type.shouldTransformFile(state, utils.getDocblock(state))) {
|
||||
return getTypeHintParams(node, state);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of transforms that return true when shouldTransformFile is
|
||||
* called.
|
||||
*/
|
||||
function getTransformsForFile(state, pragmas) {
|
||||
var transforms = [];
|
||||
var nameToTransforms = getNameToTransforms();
|
||||
Object.keys(nameToTransforms).forEach(function(value) {
|
||||
var transform = nameToTransforms[value];
|
||||
if (transform.shouldTransformFile(state, pragmas)) {
|
||||
transforms.push(transform);
|
||||
}
|
||||
});
|
||||
return transforms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of trasnforms that return true when
|
||||
* shouldTransformFunction is called.
|
||||
*/
|
||||
function getTransformsForFunction(transformsForFile, node, state, pragmas,
|
||||
params) {
|
||||
var transforms = [];
|
||||
transformsForFile.forEach(function(transform) {
|
||||
if (transform.shouldTransformFunction(node, state, pragmas, params)) {
|
||||
transforms.push(transform);
|
||||
}
|
||||
});
|
||||
return transforms;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will perform any checks over the JS source that doesn't
|
||||
* require injecting in source code. For example the typechecks transform
|
||||
* has a mode called static-only that does not add any extra code.
|
||||
*/
|
||||
function processStaticOnly(node, state) {
|
||||
var pragmas = utils.getDocblock(state);
|
||||
if (pragmas.typechecks === 'static-only') {
|
||||
var params = getTypeHintParams(node, state);
|
||||
normalizeTypeHintParams(node, state, params);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldWrapBody(transformsForFile) {
|
||||
var t = false;
|
||||
transformsForFile.forEach(function(transform) {
|
||||
t = t || transform.wrapsBody();
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
function shouldAnnotate(transformsForFile) {
|
||||
var t = false;
|
||||
transformsForFile.forEach(function(transform) {
|
||||
t = t || transform.annotates();
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trailing arguments string that should be appended to
|
||||
* __annotator(foo,
|
||||
* and does not include a semicolon.
|
||||
*/
|
||||
function getTrailingAnnotatorArguments(funcMeta, annotatorParams) {
|
||||
if (annotatorParams === null) {
|
||||
return util.format(', %j)', funcMeta);
|
||||
}
|
||||
return util.format(', %j, %j)', funcMeta, annotatorParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main entry point into the generic function transforming.
|
||||
*/
|
||||
function genericFunctionTransformer(traverse, node, path, state) {
|
||||
// The typechecks transform has a static-only mode that doesn't actually
|
||||
// perform a transform but validates the types.
|
||||
processStaticOnly(node, state, params);
|
||||
|
||||
var params = getAllParams(node, state);
|
||||
var transformsForFile = getTransformsForFile(state, utils.getDocblock(state));
|
||||
var transformsForFunction =
|
||||
getTransformsForFunction(
|
||||
transformsForFile,
|
||||
node,
|
||||
state,
|
||||
utils.getDocblock(state),
|
||||
params
|
||||
);
|
||||
|
||||
if (transformsForFunction.length === 0) {
|
||||
traverse(node.body, path, state);
|
||||
return;
|
||||
}
|
||||
|
||||
var wrapBody = shouldWrapBody(transformsForFunction);
|
||||
var annotate = shouldAnnotate(transformsForFunction);
|
||||
|
||||
// There are two different objects containing the params for the wrapper
|
||||
// vs annotator because the type param information only makes sense inside
|
||||
// the body wrapper like [x, 'number', 'x']. During execution the body wrapper
|
||||
// will be passed the correct values whereas during the annotator the
|
||||
// arguments don't exist yet.
|
||||
var bodyParams = wrapBody ? formatBodyParams(node, state, params) : null;
|
||||
var annotatorParams = annotate ? formatAnnotatorParams(params) : null;
|
||||
var funcMeta = getFunctionMetadata(node, state);
|
||||
|
||||
// If there are no params to pass to the body, then don't wrap the
|
||||
// body function.
|
||||
wrapBody = wrapBody && bodyParams !== null;
|
||||
var renderedBodyParams = renderParams(bodyParams);
|
||||
|
||||
if (node.type === Syntax.FunctionExpression && annotate) {
|
||||
utils.append('__annotator(', state);
|
||||
}
|
||||
|
||||
// Enter function body.
|
||||
utils.catchup(node.body.range[0] + 1, state);
|
||||
|
||||
// Insert a function that wraps the function body.
|
||||
if (wrapBody) {
|
||||
utils.append(
|
||||
'return __bodyWrapper(this, arguments, function() {',
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
// Recurse down into the child.
|
||||
traverse(node.body, path, state);
|
||||
// Move the cursor to the end of the function body.
|
||||
utils.catchup(node.body.range[1] - 1, state);
|
||||
|
||||
// Close the inserted function.
|
||||
if (wrapBody) {
|
||||
utils.append(util.format('}, %s);', renderedBodyParams), state);
|
||||
}
|
||||
|
||||
// Write the closing } of the function.
|
||||
utils.catchup(node.range[1], state);
|
||||
|
||||
if (!annotate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === Syntax.FunctionExpression) {
|
||||
utils.append(
|
||||
getTrailingAnnotatorArguments(funcMeta, annotatorParams),
|
||||
state
|
||||
);
|
||||
} else if (node.type === Syntax.FunctionDeclaration) {
|
||||
utils.append(
|
||||
util.format(
|
||||
'__annotator(%s',
|
||||
node.id.name
|
||||
) + getTrailingAnnotatorArguments(funcMeta, annotatorParams) + ';',
|
||||
state
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function visitFunction(traverse, node, path, state) {
|
||||
if (node.type === Syntax.Program) {
|
||||
state.docBlocksByLine = {};
|
||||
state.commentsByLine = {};
|
||||
parseComments(node, state);
|
||||
return;
|
||||
}
|
||||
|
||||
genericFunctionTransformer(traverse, node, path, state);
|
||||
return false;
|
||||
}
|
||||
visitFunction.test = function(node, path, state) {
|
||||
var pragmas = utils.getDocblock(state);
|
||||
if (!shouldTraverseFile(state, pragmas)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case Syntax.Program:
|
||||
case Syntax.FunctionExpression:
|
||||
case Syntax.FunctionDeclaration:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function setExcludes(excl) {
|
||||
excludes = excl;
|
||||
}
|
||||
|
||||
exports.visitorList = [visitFunction];
|
||||
exports.setExcludes = setExcludes;
|
||||
exports.formatAnnotatorParams = formatAnnotatorParams;
|
||||
exports.getTrailingAnnotatorArguments = getTrailingAnnotatorArguments;
|
||||
exports.getTypeHintsFromDocBlock = getTypeHintsFromDocBlock;
|
||||
exports.getTypeHintFromInline = getTypeHintFromInline;
|
|
@ -0,0 +1,519 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*jslint node: true */
|
||||
"use strict";
|
||||
|
||||
var esprima = require('esprima');
|
||||
var fs = require('fs');
|
||||
var Syntax = esprima.Syntax;
|
||||
|
||||
var findExportDefinition = require('./findExportDefinition');
|
||||
var genericTransform = require('./generic-function-visitor');
|
||||
var genericVisitor = genericTransform.visitorList[0];
|
||||
var traverseFlat = require('./traverseFlat')
|
||||
var parseTypehint = require('./TypeExpressionParser').parse;
|
||||
|
||||
// Don't save object properties source code that is longer than this
|
||||
var MAX_PROPERTY_SOURCE_LENGTH = 1000;
|
||||
|
||||
function invariant(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the expression is an identifier, it is resolved in the scope chain.
|
||||
* If it is an assignment expression, it resolves to the right hand side.
|
||||
*
|
||||
* In all other cases the expression itself is returned.
|
||||
*
|
||||
* Since the scope chain constructed by the traverse function is very simple
|
||||
* (it doesn't take into account *changes* to the variable through assignment
|
||||
* statements), this function doesn't return the correct value in every
|
||||
* situation. But it's good enough for how it is used in the parser.
|
||||
*
|
||||
* @param {object} expr
|
||||
* @param {array} scopeChain
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
function resolveToValue(expr, scopeChain) {
|
||||
switch (expr.type) {
|
||||
case Syntax.AssignmentExpression:
|
||||
if (expr.operator === '=') {
|
||||
return resolveToValue(expr.right, scopeChain);
|
||||
}
|
||||
break;
|
||||
case Syntax.Identifier:
|
||||
var value;
|
||||
scopeChain.some(function(scope, i) {
|
||||
if (hasOwnProperty.call(scope, expr.name) && scope[expr.name]) {
|
||||
value = resolveToValue(scope[expr.name], scopeChain.slice(i));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the "static upstream" warning from the docblock.
|
||||
*
|
||||
* @param {?string} docblock
|
||||
* @return {?string}
|
||||
*/
|
||||
function stripStaticUpstreamWarning(docblock) {
|
||||
if (!docblock) {
|
||||
return docblock;
|
||||
}
|
||||
// Esprima strips out the starting and ending tokens, so add them back
|
||||
docblock = "/*" + docblock + "*/\n";
|
||||
return docblock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a typehint, but swallow any errors.
|
||||
*/
|
||||
function safeParseTypehint(typehint) {
|
||||
if (!typehint) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(parseTypehint(typehint));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the docblock for the file
|
||||
*
|
||||
* @param {array<object>} commentsForFile
|
||||
* @return {?string}
|
||||
*/
|
||||
function getFileDocBlock(commentsForFile) {
|
||||
var docblock;
|
||||
commentsForFile.some(function(comment, i) {
|
||||
if (comment.loc.start.line === 1) {
|
||||
var lines = comment.value.split("\n");
|
||||
var filteredLines = lines.filter(function(line) {
|
||||
var hasCopyright = !!line.match(/^\s*\*\s+Copyright/);
|
||||
var hasProvides = !!line.match(/^\s*\*\s+@provides/);
|
||||
return !hasCopyright && !hasProvides;
|
||||
});
|
||||
docblock = filteredLines.join("\n");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return stripStaticUpstreamWarning(docblock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the docblock for a given node.
|
||||
*
|
||||
* @param {object} Node to get docblock for
|
||||
* @param {array<object>} commentsForFile
|
||||
* @param {array<string>} linesForFile
|
||||
* @return {?string}
|
||||
*/
|
||||
function getDocBlock(node, commentsForFile, linesForFile) {
|
||||
if (node.isSynthesized === true) {
|
||||
return '';
|
||||
}
|
||||
var docblock;
|
||||
var prevLine = node.loc.start.line - 1;
|
||||
// skip blank lines
|
||||
while (linesForFile[prevLine - 1].trim() === '') {
|
||||
prevLine--;
|
||||
}
|
||||
|
||||
commentsForFile.some(function(comment, i) {
|
||||
if (comment.loc.end.line === prevLine) {
|
||||
if (comment.type === 'Line') {
|
||||
// Don't accept line comments that are separated
|
||||
if (prevLine !== node.loc.start.line - 1) {
|
||||
return true;
|
||||
}
|
||||
var line = prevLine;
|
||||
docblock = '';
|
||||
for (var ii = i; ii >= 0; ii--) {
|
||||
var lineComment = commentsForFile[ii];
|
||||
if (lineComment.loc.end.line === line) {
|
||||
docblock = '//' + lineComment.value +
|
||||
(docblock ? "\n" + docblock : "");
|
||||
line--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
docblock = stripStaticUpstreamWarning(comment.value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return docblock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the comments for a file, return the module name (by looking for
|
||||
* @providesModule).
|
||||
*
|
||||
* @param {array<object>}
|
||||
* @return {?string}
|
||||
*/
|
||||
function getModuleName(commentsForFile) {
|
||||
var moduleName;
|
||||
commentsForFile.forEach(function(comment) {
|
||||
if (comment.type === 'Block') {
|
||||
var matches = comment.value.match(/@providesModule\s+(\S*)/);
|
||||
if (matches && matches[1]) {
|
||||
moduleName = matches[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Esprima includes the leading colon (and possibly spaces) as part of the
|
||||
* typehint, so we have to strip those out.
|
||||
*/
|
||||
function sanitizeTypehint(string) {
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
if (string[i] != ' ' && string[i] != ':') {
|
||||
return string.substring(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} node
|
||||
* @param {object} state
|
||||
* @param {string} source
|
||||
* @param {array<object>} commentsForFile
|
||||
* @param {array<string>} linesForFile
|
||||
* @return {object}
|
||||
*/
|
||||
function getFunctionData(node, state, source, commentsForFile, linesForFile) {
|
||||
var params = [];
|
||||
var typechecks = commentsForFile.typechecks;
|
||||
var typehintsFromBlock = null;
|
||||
if (typechecks) {
|
||||
// esprima has trouble with some params so ignore them (e.g. $__0)
|
||||
if (!node.params.some(function(param) { return !param.name; })) {
|
||||
try {
|
||||
typehintsFromBlock = genericTransform.getTypeHintsFromDocBlock(
|
||||
node,
|
||||
state.docBlocksByLine
|
||||
);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
node.params.forEach(function(param) {
|
||||
// TODO: Handle other things like Syntax.ObjectPattern
|
||||
if (param.type === Syntax.Identifier) {
|
||||
var typehint;
|
||||
if (typehintsFromBlock && typehintsFromBlock.params) {
|
||||
typehintsFromBlock.params.some(function(paramTypehint) {
|
||||
if (paramTypehint[0] === param.name) {
|
||||
typehint = paramTypehint[1];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!typehint && typechecks) {
|
||||
try {
|
||||
typehint = genericTransform.getTypeHintFromInline(
|
||||
param,
|
||||
state.commentsByLine
|
||||
);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
params.push({
|
||||
typehint: safeParseTypehint(typehint),
|
||||
name: param.name
|
||||
});
|
||||
} else if (param.type === Syntax.TypeAnnotatedIdentifier) {
|
||||
params.push({
|
||||
typehint: sanitizeTypehint(source.substring(
|
||||
param.annotation.range[0],
|
||||
param.annotation.range[1]
|
||||
)),
|
||||
name: param.id.name
|
||||
});
|
||||
}
|
||||
});
|
||||
var returnTypehint = null;
|
||||
if (node.returnType) {
|
||||
returnTypehint = sanitizeTypehint(source.substring(
|
||||
node.returnType.range[0],
|
||||
node.returnType.range[1]
|
||||
));
|
||||
} else if (typehintsFromBlock) {
|
||||
returnTypehint = typehintsFromBlock.returns;
|
||||
}
|
||||
return {
|
||||
line: node.loc.start.line,
|
||||
source: source.substring.apply(source, node.range),
|
||||
docblock: getDocBlock(node, commentsForFile, linesForFile),
|
||||
modifiers: [],
|
||||
params: params,
|
||||
returntypehint: safeParseTypehint(returnTypehint)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} node
|
||||
* @param {object} state
|
||||
* @param {string} source
|
||||
* @param {array<object>} commentsForFile
|
||||
* @param {array<string>} linesForFile
|
||||
* @return {object}
|
||||
*/
|
||||
function getObjectData(node, state, source, scopeChain,
|
||||
commentsForFile, linesForFile) {
|
||||
var methods = [];
|
||||
var properties = [];
|
||||
var superClass = null;
|
||||
node.properties.forEach(function(property) {
|
||||
if (property.type === Syntax.SpreadProperty) {
|
||||
if (property.argument.type === Syntax.Identifier) {
|
||||
superClass = property.argument.name;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (property.value.type) {
|
||||
case Syntax.FunctionExpression:
|
||||
var methodData = getFunctionData(property.value, state, source,
|
||||
commentsForFile, linesForFile);
|
||||
methodData.name = property.key.name || property.key.value;
|
||||
methodData.source = source.substring.apply(source, property.range);
|
||||
methodData.modifiers.push('static');
|
||||
methods.push(methodData);
|
||||
break;
|
||||
case Syntax.Identifier:
|
||||
var expr = resolveToValue(
|
||||
property.value,
|
||||
scopeChain
|
||||
);
|
||||
if (expr) {
|
||||
if (expr.type === Syntax.FunctionDeclaration) {
|
||||
var functionData =
|
||||
getFunctionData(expr, state, source, commentsForFile, linesForFile);
|
||||
functionData.name = property.key.name || property.key.value;
|
||||
functionData.modifiers.push('static');
|
||||
methods.push(functionData);
|
||||
break;
|
||||
} else {
|
||||
property.value = expr;
|
||||
}
|
||||
}
|
||||
/* falls through */
|
||||
default:
|
||||
var propertySource = '';
|
||||
var valueRange = property.value.range;
|
||||
if (valueRange[1] - valueRange[0] < MAX_PROPERTY_SOURCE_LENGTH) {
|
||||
propertySource = source.substring.apply(source, valueRange);
|
||||
}
|
||||
var docBlock = getDocBlock(property, commentsForFile, linesForFile);
|
||||
/* CodexVarDef: modifiers, type, name, default, docblock */
|
||||
var propertyData = [
|
||||
['static'],
|
||||
'',
|
||||
// Cast to String because this can be a Number
|
||||
// Could also be a String literal (e.g. "key") hence the value
|
||||
String(property.key.name || property.key.value),
|
||||
propertySource,
|
||||
docBlock || '',
|
||||
property.loc.start.line
|
||||
];
|
||||
properties.push(propertyData);
|
||||
break;
|
||||
}
|
||||
});
|
||||
return {
|
||||
methods: methods,
|
||||
properties: properties,
|
||||
superClass: superClass
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} node
|
||||
* @param {object} state
|
||||
* @param {string} source
|
||||
* @param {array<object>} commentsForFile
|
||||
* @param {array<string>} linesForFile
|
||||
* @return {object}
|
||||
*/
|
||||
function getClassData(node, state, source, commentsForFile, linesForFile) {
|
||||
var methods = [];
|
||||
invariant(node.body.type === Syntax.ClassBody, 'Expected ClassBody');
|
||||
node.body.body.forEach(function(bodyItem) {
|
||||
if (bodyItem.type === Syntax.MethodDefinition) {
|
||||
if (bodyItem.value.type === Syntax.FunctionExpression) {
|
||||
var methodData =
|
||||
getFunctionData(bodyItem.value, state, source,
|
||||
commentsForFile, linesForFile);
|
||||
methodData.name = bodyItem.key.name;
|
||||
methodData.source = source.substring.apply(source, bodyItem.range);
|
||||
if (bodyItem.static) {
|
||||
methodData.modifiers.push('static');
|
||||
}
|
||||
methods.push(methodData);
|
||||
}
|
||||
}
|
||||
});
|
||||
var data = {
|
||||
methods: methods
|
||||
};
|
||||
if (node.superClass && node.superClass.type === Syntax.Identifier) {
|
||||
data.superClass = node.superClass.name;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds all the requires
|
||||
*
|
||||
* @param {object} ast
|
||||
* @return {array<object>}
|
||||
*/
|
||||
function findRequires(ast) {
|
||||
var requires = [];
|
||||
traverseFlat(ast, function(node, scopeChain) {
|
||||
var requireData = getRequireData(node);
|
||||
if (requireData) {
|
||||
requires.push(requireData);
|
||||
}
|
||||
return !requireData;
|
||||
});
|
||||
return requires;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given node is a 'require' statement, returns a list of following data
|
||||
* {
|
||||
* name: string
|
||||
* }
|
||||
*
|
||||
* @return ?object
|
||||
*/
|
||||
function getRequireData(node) {
|
||||
if (!node || node.type !== Syntax.CallExpression) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var callee = node.callee;
|
||||
if (callee.type !== Syntax.Identifier
|
||||
|| (callee.name !== 'require')) {
|
||||
return null;
|
||||
}
|
||||
var args = node['arguments'];
|
||||
if (args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var firstArgument = args[0];
|
||||
if (firstArgument.type !== Syntax.Literal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: firstArgument.value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the source of a file, this returns the data about the module's exported
|
||||
* value.
|
||||
*
|
||||
* @param {string} source
|
||||
* @return {?object} data
|
||||
*/
|
||||
function parseSource(source) {
|
||||
var lines = source.split("\n");
|
||||
var ast = esprima.parse(source, {
|
||||
loc: true,
|
||||
comment: true,
|
||||
range: true,
|
||||
sourceType: 'nonStrictModule',
|
||||
});
|
||||
|
||||
/**
|
||||
* This sets up genericTransform so that it can be queried above.
|
||||
*/
|
||||
var _state = {
|
||||
g: {
|
||||
source: source
|
||||
}
|
||||
};
|
||||
if (genericVisitor.test(ast, [], _state)) {
|
||||
// HACK: Mark that this file has typechecks on the comments object.
|
||||
ast.comments.typechecks = true;
|
||||
// This fills out the data for genericTransform.
|
||||
genericVisitor(function() {}, ast, [], _state);
|
||||
}
|
||||
var result = findExportDefinition(ast.body);
|
||||
if (result) {
|
||||
var definition = result.definition;
|
||||
var scopeChain = result.scopeChain;
|
||||
var data;
|
||||
var moduleName = getModuleName(ast.comments);
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
if (definition.type === Syntax.NewExpression &&
|
||||
definition.callee.type === Syntax.Identifier) {
|
||||
var name = definition.callee.name;
|
||||
// If the class is defined in the scopeChain, export that instead.
|
||||
scopeChain.some(function(scope) {
|
||||
if (hasOwnProperty.call(scope, name) &&
|
||||
scope[name].type === Syntax.ClassDeclaration) {
|
||||
definition = scope[name];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(definition.type);
|
||||
switch (definition.type) {
|
||||
case Syntax.ClassDeclaration:
|
||||
data = getClassData(definition, _state, source, ast.comments, lines);
|
||||
data.type = 'class';
|
||||
break;
|
||||
case Syntax.ObjectExpression:
|
||||
data = getObjectData(definition, _state, source, scopeChain,
|
||||
ast.comments, lines);
|
||||
data.type = 'object';
|
||||
break;
|
||||
case Syntax.FunctionDeclaration:
|
||||
case Syntax.FunctionExpression:
|
||||
data = getFunctionData(definition, _state, source, ast.comments, lines);
|
||||
data.type = 'function';
|
||||
break;
|
||||
default:
|
||||
data = {type: 'module'};
|
||||
break;
|
||||
}
|
||||
if (data) {
|
||||
data.line = definition.loc.start.line;
|
||||
data.name = moduleName;
|
||||
data.docblock =
|
||||
getDocBlock(definition, ast.comments, lines) ||
|
||||
getFileDocBlock(ast.comments);
|
||||
data.requires = findRequires(ast.body);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
module.exports = parseSource;
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*global exports:true*/
|
||||
/*jslint node:true*/
|
||||
"use strict";
|
||||
|
||||
var util = require('util');
|
||||
|
||||
var Syntax = require('esprima-fb').Syntax;
|
||||
var utils = require('jstransform/src/utils');
|
||||
|
||||
// Top level file pragmas that must not exist for the meta transform to
|
||||
// be applied.
|
||||
var mustNotHave = [
|
||||
'nosourcemeta',
|
||||
];
|
||||
|
||||
function shouldTraverseFile(state, pragmas) {
|
||||
if (state.g.sourcemeta === undefined) {
|
||||
var notHaves = true;
|
||||
mustNotHave.forEach(function (value) {
|
||||
notHaves = notHaves && !(value in pragmas);
|
||||
});
|
||||
state.g.sourcemeta = notHaves;
|
||||
}
|
||||
return state.g.sourcemeta;
|
||||
}
|
||||
|
||||
var shouldTransformFile = shouldTraverseFile;
|
||||
|
||||
function shouldTransformFunction(node, state, pragmas, params) /*bool*/ {
|
||||
if (!shouldTransformFile(state, pragmas)) {
|
||||
throw new Error(
|
||||
'shouldTransformFunction should not be called if shouldTransformFile ' +
|
||||
'fails'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function wrapsBody() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function annotates() {
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.shouldTransformFile = shouldTransformFile;
|
||||
exports.shouldTraverseFile = shouldTraverseFile;
|
||||
exports.shouldTransformFunction = shouldTransformFunction;
|
||||
exports.wrapsBody = wrapsBody;
|
||||
exports.annotates = annotates;
|
||||
exports.name = 'sourcemeta';
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*global exports:true*/
|
||||
/*jslint node:true*/
|
||||
"use strict";
|
||||
|
||||
var Syntax = require('esprima-fb').Syntax;
|
||||
|
||||
/**
|
||||
* Executes visitor on the object and its children (recursively).
|
||||
* While traversing the tree, a scope chain is built and passed to the visitor.
|
||||
*
|
||||
* If the visitor returns false, the object's children are not traversed.
|
||||
*
|
||||
* @param {object} object
|
||||
* @param {function} visitor
|
||||
* @param {?array} scopeChain
|
||||
*/
|
||||
function traverse(object, visitor, scopeChain) {
|
||||
scopeChain = scopeChain || [{}];
|
||||
|
||||
var scope = scopeChain[0];
|
||||
|
||||
switch (object.type) {
|
||||
case Syntax.VariableDeclaration:
|
||||
object.declarations.forEach(function(decl) {
|
||||
scope[decl.id.name] = decl.init;
|
||||
});
|
||||
break;
|
||||
case Syntax.ClassDeclaration:
|
||||
scope[object.id.name] = object;
|
||||
break;
|
||||
case Syntax.FunctionDeclaration:
|
||||
// A function declaration creates a symbol in the current scope
|
||||
scope[object.id.name] = object;
|
||||
/* falls through */
|
||||
case Syntax.FunctionExpression:
|
||||
case Syntax.Program:
|
||||
scopeChain = [{}].concat(scopeChain);
|
||||
break;
|
||||
}
|
||||
|
||||
if (object.type === Syntax.FunctionExpression ||
|
||||
object.type === Syntax.FunctionDeclaration) {
|
||||
// add parameters to the new scope
|
||||
object.params.forEach(function(param) {
|
||||
// since the value of the parameters are unknown during parsing time
|
||||
// we set the value to `undefined`.
|
||||
scopeChain[0][param.name] = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (object.type) {
|
||||
if (visitor.call(null, object, scopeChain) === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in object) {
|
||||
if (object.hasOwnProperty(key)) {
|
||||
var child = object[key];
|
||||
if (typeof child === 'object' && child !== null) {
|
||||
traverse(child, visitor, scopeChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes visitor on the object and its children, but only traverses into
|
||||
* children which can be statically analyzed and don't depend on runtime
|
||||
* information.
|
||||
*
|
||||
* @param {object} object
|
||||
* @param {function} visitor
|
||||
* @param {?array} scopeChain
|
||||
*/
|
||||
function traverseFlat(object, visitor, scopeChain) {
|
||||
traverse(object, function(node, scopeChain) {
|
||||
switch (node.type) {
|
||||
case Syntax.FunctionDeclaration:
|
||||
case Syntax.FunctionExpression:
|
||||
case Syntax.IfStatement:
|
||||
case Syntax.WithStatement:
|
||||
case Syntax.SwitchStatement:
|
||||
case Syntax.TryStatement:
|
||||
case Syntax.WhileStatement:
|
||||
case Syntax.DoWhileStatement:
|
||||
case Syntax.ForStatement:
|
||||
case Syntax.ForInStatement:
|
||||
return false;
|
||||
}
|
||||
return visitor(node, scopeChain);
|
||||
}, scopeChain);
|
||||
}
|
||||
|
||||
module.exports = traverseFlat;
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
/*global exports:true*/
|
||||
"use strict";
|
||||
|
||||
var util = require('util');
|
||||
|
||||
var Syntax = require('esprima-fb').Syntax;
|
||||
var utils = require('jstransform/src/utils');
|
||||
|
||||
var parse = require('./TypeExpressionParser').parse;
|
||||
var compile = require('./TypeExpressionParser').compile;
|
||||
var normalize = require('./TypeExpressionParser').normalize;
|
||||
|
||||
function parseAndNormalize(source, name, object) {
|
||||
if (/\?$/.test(source)) {
|
||||
source = '?' + source.substring(0, source.length - 1);
|
||||
}
|
||||
try {
|
||||
var ast = parse(source);
|
||||
return compile(normalize(ast));
|
||||
} catch (e) {
|
||||
var functionName = object.id
|
||||
? '`' + object.id.name + '\''
|
||||
: '<anonymous>';
|
||||
throw new Error(util.format('The type `%s\' specified for %s for ' +
|
||||
'the function %s, on line %s, could not be parsed. The error given was: %s',
|
||||
source, name, functionName, object.loc.start.line, e.message
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
function initializeSettings(state, pragmas) {
|
||||
state.g.typechecks = 'typechecks' in pragmas;
|
||||
state.g.staticOnly = pragmas.typechecks === 'static-only';
|
||||
}
|
||||
|
||||
function shouldTraverseFile(state, pragmas) {
|
||||
if (state.g.typechecks === undefined) {
|
||||
initializeSettings(state, pragmas);
|
||||
}
|
||||
return state.g.typechecks;
|
||||
}
|
||||
|
||||
function shouldTransformFile(state, pragmas) {
|
||||
if (state.g.typechecks === undefined) {
|
||||
initializeSettings(state, pragmas);
|
||||
}
|
||||
return !state.g.staticOnly && state.g.typechecks;
|
||||
}
|
||||
|
||||
function shouldTransformFunction(node, state, pragmas, params) {
|
||||
if (!shouldTransformFile(state, pragmas)) {
|
||||
throw new Error(
|
||||
'shouldTransformFunction should not be called if shouldTransformFile ' +
|
||||
'fails'
|
||||
);
|
||||
}
|
||||
|
||||
return (params.params && params.params.length > 0) ||
|
||||
params.returns ||
|
||||
(node.id && /^[A-Z]/.test(node.id.name));
|
||||
}
|
||||
|
||||
function wrapsBody() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function annotates() {
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.parseAndNormalize = parseAndNormalize;
|
||||
exports.shouldTransformFile = shouldTransformFile;
|
||||
exports.shouldTraverseFile = shouldTraverseFile;
|
||||
exports.shouldTransformFunction = shouldTransformFunction;
|
||||
exports.wrapsBody = wrapsBody;
|
||||
exports.annotates = annotates;
|
||||
exports.name = 'typechecks';
|
|
@ -10,6 +10,9 @@
|
|||
"glob": "*",
|
||||
"mkdirp": "*",
|
||||
"request": "*",
|
||||
"fs.extra": "*"
|
||||
"fs.extra": "*",
|
||||
"esprima": "*",
|
||||
"esprima-fb": "*",
|
||||
"jstransform": "*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ var docs = require('../react-docgen');
|
|||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var slugify = require('../core/slugify');
|
||||
var jsDocs = require('../jsdocs/jsdocs.js')
|
||||
|
||||
function getNameFromPath(filepath) {
|
||||
var ext = null;
|
||||
|
@ -11,7 +12,7 @@ function getNameFromPath(filepath) {
|
|||
return filepath;
|
||||
}
|
||||
|
||||
function docsToMarkdown(filepath, i) {
|
||||
function componentsToMarkdown(filepath, i) {
|
||||
var json = docs.parse(
|
||||
fs.readFileSync(filepath),
|
||||
function(node, recast) {
|
||||
|
@ -42,11 +43,15 @@ function docsToMarkdown(filepath, i) {
|
|||
|
||||
var components = [
|
||||
'../Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js',
|
||||
'../Libraries/Components/DatePicker/DatePickerIOS.ios.js',
|
||||
'../Libraries/Text/ExpandingText.js',
|
||||
'../Libraries/Image/Image.ios.js',
|
||||
'../Libraries/Components/ListView/ListView.js',
|
||||
'../Libraries/Components/Navigation/NavigatorIOS.ios.js',
|
||||
'../Libraries/Components/ScrollView/ScrollView.js',
|
||||
'../Libraries/Components/Slider/Slider.js',
|
||||
'../Libraries/Components/SwitchIOS/SwitchIOS.ios.js',
|
||||
'../Libraries/Components/TabBarIOS/TabBarIOS.ios.js',
|
||||
'../Libraries/Text/Text.js',
|
||||
'../Libraries/Components/TextInput/TextInput.ios.js',
|
||||
'../Libraries/Components/Touchable/TouchableHighlight.js',
|
||||
|
@ -55,6 +60,16 @@ var components = [
|
|||
'../Libraries/Components/View/View.js',
|
||||
];
|
||||
|
||||
|
||||
function apisToMarkdown(filepath, i) {
|
||||
var json = jsDocs(fs.readFileSync(filepath).toString());
|
||||
console.log(JSON.stringify(json, null, 2));
|
||||
}
|
||||
|
||||
var apis = [
|
||||
'../Libraries/AppRegistry/AppRegistry.js',
|
||||
];
|
||||
|
||||
module.exports = function() {
|
||||
return components.map(docsToMarkdown);
|
||||
return components.map(componentsToMarkdown);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue