From 83581cfe6bf046c6d659d96cb0b25f62e0e9da00 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Tue, 10 Mar 2015 13:55:54 -0700 Subject: [PATCH] Initial import of the lib to parse javascript code, in the same vein as we parse React proptypes --- website/jsdocs/TypeExpressionParser.js | 557 +++++++++++++++++++++ website/jsdocs/findExportDefinition.js | 276 ++++++++++ website/jsdocs/generic-function-visitor.js | 534 ++++++++++++++++++++ website/jsdocs/jsdocs.js | 519 +++++++++++++++++++ website/jsdocs/meta.js | 54 ++ website/jsdocs/traverseFlat.js | 97 ++++ website/jsdocs/type.js | 79 +++ website/package.json | 5 +- website/server/extractDocs.js | 19 +- 9 files changed, 2137 insertions(+), 3 deletions(-) create mode 100644 website/jsdocs/TypeExpressionParser.js create mode 100644 website/jsdocs/findExportDefinition.js create mode 100644 website/jsdocs/generic-function-visitor.js create mode 100644 website/jsdocs/jsdocs.js create mode 100644 website/jsdocs/meta.js create mode 100644 website/jsdocs/traverseFlat.js create mode 100644 website/jsdocs/type.js diff --git a/website/jsdocs/TypeExpressionParser.js b/website/jsdocs/TypeExpressionParser.js new file mode 100644 index 000000000..7efcb747a --- /dev/null +++ b/website/jsdocs/TypeExpressionParser.js @@ -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; +}; diff --git a/website/jsdocs/findExportDefinition.js b/website/jsdocs/findExportDefinition.js new file mode 100644 index 000000000..45bf4a539 --- /dev/null +++ b/website/jsdocs/findExportDefinition.js @@ -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; diff --git a/website/jsdocs/generic-function-visitor.js b/website/jsdocs/generic-function-visitor.js new file mode 100644 index 000000000..5262d3fb8 --- /dev/null +++ b/website/jsdocs/generic-function-visitor.js @@ -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 + '\'' + : ''; + + 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; diff --git a/website/jsdocs/jsdocs.js b/website/jsdocs/jsdocs.js new file mode 100644 index 000000000..1e65fcf25 --- /dev/null +++ b/website/jsdocs/jsdocs.js @@ -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} 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} commentsForFile + * @param {array} 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} + * @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} commentsForFile + * @param {array} 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} commentsForFile + * @param {array} 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} commentsForFile + * @param {array} 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} + */ +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; diff --git a/website/jsdocs/meta.js b/website/jsdocs/meta.js new file mode 100644 index 000000000..0cc92da3e --- /dev/null +++ b/website/jsdocs/meta.js @@ -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'; diff --git a/website/jsdocs/traverseFlat.js b/website/jsdocs/traverseFlat.js new file mode 100644 index 000000000..5d2390dca --- /dev/null +++ b/website/jsdocs/traverseFlat.js @@ -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; diff --git a/website/jsdocs/type.js b/website/jsdocs/type.js new file mode 100644 index 000000000..bcfe0b531 --- /dev/null +++ b/website/jsdocs/type.js @@ -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 + '\'' + : ''; + 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'; diff --git a/website/package.json b/website/package.json index b5580871c..32642915d 100644 --- a/website/package.json +++ b/website/package.json @@ -10,6 +10,9 @@ "glob": "*", "mkdirp": "*", "request": "*", - "fs.extra": "*" + "fs.extra": "*", + "esprima": "*", + "esprima-fb": "*", + "jstransform": "*" } } diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 5a5456e43..c18b58dd4 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -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); };