Update react-docgen and ignore pages with no header

This commit is contained in:
Felix Kling 2015-02-18 16:39:28 -08:00
parent 022696c345
commit 472c287cd3
12 changed files with 564 additions and 246 deletions

View File

@ -13,201 +13,112 @@
*/
"use strict";
/**
* How this parser works:
*
* 1. For each given file path do:
*
* a. Find component definition
* -. Find the rvalue module.exports assignment.
* Otherwise inspect assignments to exports. If there are multiple
* components that are exported, we don't continue with parsing the file.
* -. If the previous step results in a variable name, resolve it.
* -. Extract the object literal from the React.createClass call.
*
* b. Execute definition handlers (handlers working with the object
* expression).
*
* c. For each property of the definition object, execute the registered
* callbacks, if they are eligible for this property.
*
* 2. Return the aggregated results
*/
type Handler = (documentation: Documentation, path: NodePath) => void;
var Documentation = require('./Documentation');
var expressionTo = require('./utils/expressionTo');
var findExportedReactCreateClass =
require('./strategies/findExportedReactCreateClassCall');
var getPropertyName = require('./utils/getPropertyName');
var isReactModuleName = require('./utils/isReactModuleName');
var match = require('./utils/match');
var resolveToValue = require('./utils/resolveToValue');
var resolveToModule = require('./utils/resolveToModule');
var recast = require('recast');
var resolveToValue = require('./utils/resolveToValue');
var n = recast.types.namedTypes;
function ignore() {
return false;
}
/**
* Returns true if the statement is of form `foo = bar;`.
*
* @param {object} node
* @return {bool}
*/
function isAssignmentStatement(node) {
return match(node, {expression: {operator: '='}});
}
/**
* Returns true if the expression is of form `exports.foo = bar;` or
* `modules.exports = foo;`.
*
* @param {object} node
* @return {bool}
*/
function isExportsOrModuleExpression(path) {
if (!n.AssignmentExpression.check(path.node) ||
!n.MemberExpression.check(path.node.left)) {
return false;
}
var exprArr = expressionTo.Array(path.get('left'));
return (exprArr[0] === 'module' && exprArr[1] === 'exports') ||
exprArr[0] == 'exports';
}
/**
* Returns true if the expression is a function call of the form
* `React.createClass(...)`.
*
* @param {object} node
* @param {array} scopeChain
* @return {bool}
*/
function isReactCreateClassCall(path) {
if (!match(path.node, {callee: {property: {name: 'createClass'}}})) {
return false;
}
var module = resolveToModule(path.get('callee', 'object'));
return module && isReactModuleName(module);
}
/**
* Given an AST, this function tries to find the object expression that is
* passed to `React.createClass`, by resolving all references properly.
*
* @param {object} ast
* @return {?object}
*/
function findComponentDefinition(ast) {
var definition;
recast.visit(ast, {
visitFunctionDeclaration: ignore,
visitFunctionExpression: ignore,
visitIfStatement: ignore,
visitWithStatement: ignore,
visitSwitchStatement: ignore,
visitTryStatement: ignore,
visitWhileStatement: ignore,
visitDoWhileStatement: ignore,
visitForStatement: ignore,
visitForInStatement: ignore,
visitAssignmentExpression: function(path) {
// Ignore anything that is not `exports.X = ...;` or
// `module.exports = ...;`
if (!isExportsOrModuleExpression(path)) {
return false;
}
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
path = resolveToValue(path.get('right'));
if (!isReactCreateClassCall(path)) {
return false;
}
if (definition) { // If a file exports multiple components, ... complain!
throw new Error(ReactDocumentationParser.ERROR_MULTIPLE_DEFINITIONS);
}
// We found React.createClass. Lets get cracking!
definition = resolveToValue(path.get('arguments', 0));
return false;
}
});
return definition;
}
class ReactDocumentationParser {
_componentHandlers: Array<Handler>;
_propertyHandlers: Object<string, Handler>;
_apiHandlers: Object<string, Handler>;
constructor() {
this._componentHandlers = [];
this._propertyHandlers = Object.create(null);
this._apiHandlers = Object.create(null);
}
/**
* Handlers extract information from the component definition.
* Handlers to extract information from the component definition.
*
* If "property" is not provided, the handler is passed the whole component
* definition.
*
* NOTE: The component definition is currently expected to be represented as
* an ObjectExpression (an object literal). This will likely change in the
* future.
*/
addHandler(handler: Handler, property?: string): void {
if (!property) {
this._componentHandlers.push(handler);
} else {
if (!this._propertyHandlers[property]) {
this._propertyHandlers[property] = [];
if (!this._apiHandlers[property]) {
this._apiHandlers[property] = [];
}
this._propertyHandlers[property].push(handler);
this._apiHandlers[property].push(handler);
}
}
/**
* Takes JavaScript source code and returns an object with the information
* extract from it.
*
* The second argument is strategy to find the AST node(s) of the component
* definition(s) inside `source`.
* It is a function that gets passed the program AST node of
* the source as first argument, and a reference to recast as second argument.
*
* This allows you define your own strategy for finding component definitions.
* By default it will look for the exported component created by
* React.createClass. An error is thrown if multiple components are exported.
*
* NOTE: The component definition is currently expected to be represented as
* an ObjectExpression (an object literal), no matter which strategy is
* chosen. This will likely change in the future.
*/
parseSource(source: string): Object {
var documentation = new Documentation();
parseSource(
source: string,
componentDefinitionStrategy?:
(program: ASTNode, recast: Object) => (Array<NodePath>|NodePath)
): (Array<Object>|Object) {
if (!componentDefinitionStrategy) {
componentDefinitionStrategy = findExportedReactCreateClass;
}
var ast = recast.parse(source);
// Find the component definition first. The return value should be
// Find the component definitions first. The return value should be
// an ObjectExpression.
var componentDefinition = findComponentDefinition(ast.program);
if (!componentDefinition) {
var componentDefinition = componentDefinitionStrategy(ast.program, recast);
var isArray = Array.isArray(componentDefinition);
if (!componentDefinition || (isArray && componentDefinition.length === 0)) {
throw new Error(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
}
// Execute all the handlers to extract the information
this._executeHandlers(documentation, componentDefinition);
return documentation.toObject();
return isArray ?
this._executeHandlers(componentDefinition).map(
documentation => documentation.toObject()
) :
this._executeHandlers([componentDefinition])[0].toObject();
}
_executeHandlers(documentation, componentDefinition: NodePath) {
componentDefinition.get('properties').each(propertyPath => {
var name = getPropertyName(propertyPath);
if (!this._propertyHandlers[name]) {
return;
}
var propertyValuePath = propertyPath.get('value');
this._propertyHandlers[name].forEach(
handler => handler(documentation, propertyValuePath)
_executeHandlers(componentDefinitions: Array<NodePath>): Array<Documenation> {
return componentDefinitions.map(componentDefinition => {
var documentation = new Documentation();
componentDefinition.get('properties').each(propertyPath => {
var name = getPropertyName(propertyPath);
if (!this._apiHandlers[name]) {
return;
}
var propertyValuePath = propertyPath.get('value');
this._apiHandlers[name].forEach(
handler => handler(documentation, propertyValuePath)
);
});
this._componentHandlers.forEach(
handler => handler(documentation, componentDefinition)
);
return documentation;
});
this._componentHandlers.forEach(
handler => handler(documentation, componentDefinition)
);
}
}
ReactDocumentationParser.ERROR_MISSING_DEFINITION =
'No suitable component definition found.';
ReactDocumentationParser.ERROR_MULTIPLE_DEFINITIONS =
'Multiple exported component definitions found.';
module.exports = ReactDocumentationParser;

View File

@ -10,109 +10,52 @@
"use strict";
require('mock-modules').autoMockOff();
jest.autoMockOff();
describe('React documentation parser', function() {
var ReactDocumentationParser;
var parser;
var recast;
beforeEach(function() {
recast = require('recast');
ReactDocumentationParser = require('../ReactDocumentationParser');
parser = new ReactDocumentationParser();
});
it('errors if component definition is not found', function() {
var source = 'var React = require("React");';
function pathFromSource(source) {
return new recast.types.NodePath(
recast.parse(source).program.body[0].expression
);
}
describe('parseSource', function() {
it('allows custom component definition resolvers', function() {
var path = pathFromSource('({foo: "bar"})');
var resolver = jest.genMockFunction().mockReturnValue(path);
var handler = jest.genMockFunction();
parser.addHandler(handler);
parser.parseSource('', resolver);
expect(resolver).toBeCalled();
expect(handler.mock.calls[0][1]).toBe(path);
});
it('errors if component definition is not found', function() {
var handler = jest.genMockFunction();
expect(function() {
parser.parseSource('', handler);
}).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
expect(handler).toBeCalled();
handler = jest.genMockFunction().mockReturnValue([]);
expect(function() {
parser.parseSource('', handler);
}).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
expect(handler).toBeCalled();
});
expect(function() {
parser.parseSource(source);
}).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
});
it('finds React.createClass', function() {
var source = [
'var React = require("React");',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
it('finds React.createClass, independent of the var name', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
it('does not process X.createClass of other modules', function() {
var source = [
'var R = require("NoReact");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
});
it('finds assignments to exports', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'exports.foo = 42;',
'exports.Component = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
it('errors if multiple components are exported', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentA = ComponentA;',
'exports.ComponentB = ComponentB;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).toThrow(ReactDocumentationParser.ERROR_MULTIPLE_DEFINITIONS);
});
it('accepts multiple definitions if only one is exported', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentB = ComponentB;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'module.exports = ComponentB;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
});

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
describe('React documentation parser', function() {
var findAllReactCreateClassCalls;
var recast;
function parse(source) {
return findAllReactCreateClassCalls(
recast.parse(source).program,
recast
);
}
beforeEach(function() {
findAllReactCreateClassCalls = require('../findAllReactCreateClassCalls');
recast = require('recast');
});
it('finds React.createClass', function() {
var source = [
'var React = require("React");',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
var result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
expect(result[0] instanceof recast.types.NodePath).toBe(true);
expect(result[0].node.type).toBe('ObjectExpression');
});
it('finds React.createClass, independent of the var name', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
var result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
});
it('does not process X.createClass of other modules', function() {
var source = [
'var R = require("NoReact");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
var result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(0);
});
it('finds assignments to exports', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'exports.foo = 42;',
'exports.Component = Component;'
].join('\n');
var result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
});
it('accepts multiple definitions', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentB = ComponentB;'
].join('\n');
var result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(2);
source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'module.exports = ComponentB;'
].join('\n');
result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(2);
});
});

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
describe('React documentation parser', function() {
var findExportedReactCreateClass;
var recast;
function parse(source) {
return findExportedReactCreateClass(
recast.parse(source).program,
recast
);
}
beforeEach(function() {
findExportedReactCreateClass =
require('../findExportedReactCreateClassCall');
recast = require('recast');
});
it('finds React.createClass', function() {
var source = [
'var React = require("React");',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
expect(parse(source)).toBeDefined();
});
it('finds React.createClass, independent of the var name', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
expect(parse(source)).toBeDefined();
});
it('does not process X.createClass of other modules', function() {
var source = [
'var R = require("NoReact");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
expect(parse(source)).toBeUndefined();
});
it('finds assignments to exports', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'exports.foo = 42;',
'exports.Component = Component;'
].join('\n');
expect(parse(source)).toBeDefined();
});
it('errors if multiple components are exported', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentA = ComponentA;',
'exports.ComponentB = ComponentB;'
].join('\n');
expect(function() {
parse(source)
}).toThrow();
});
it('accepts multiple definitions if only one is exported', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentB = ComponentB;'
].join('\n');
expect(parse(source)).toBeDefined();
source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'module.exports = ComponentB;'
].join('\n');
expect(parse(source)).toBeDefined();
});
});

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var isReactCreateClassCall = require('../utils/isReactCreateClassCall');
var resolveToValue = require('../utils/resolveToValue');
/**
* Given an AST, this function tries to find all object expressions that are
* passed to `React.createClass` calls, by resolving all references properly.
*/
function findAllReactCreateClassCalls(
ast: ASTNode,
recast: Object
): Array<NodePath> {
var types = recast.types.namedTypes;
var definitions = [];
recast.visit(ast, {
visitCallExpression: function(path) {
if (!isReactCreateClassCall(path)) {
return false;
}
// We found React.createClass. Lets get cracking!
var resolvedPath = resolveToValue(path.get('arguments', 0));
if (types.ObjectExpression.check(resolvedPath.node)) {
definitions.push(resolvedPath);
}
return false;
}
});
return definitions;
}
module.exports = findAllReactCreateClassCalls;

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var isExportsOrModuleAssignment =
require('../utils/isExportsOrModuleAssignment');
var isReactCreateClassCall = require('../utils/isReactCreateClassCall');
var resolveToValue = require('../utils/resolveToValue');
var ERROR_MULTIPLE_DEFINITIONS =
'Multiple exported component definitions found.';
function ignore() {
return false;
}
/**
* Given an AST, this function tries to find the object expression that is
* passed to `React.createClass`, by resolving all references properly.
*/
function findExportedReactCreateClass(
ast: ASTNode,
recast: Object
): ?NodePath {
var types = recast.types.namedTypes;
var definition;
recast.visit(ast, {
visitFunctionDeclaration: ignore,
visitFunctionExpression: ignore,
visitIfStatement: ignore,
visitWithStatement: ignore,
visitSwitchStatement: ignore,
visitCatchCause: ignore,
visitWhileStatement: ignore,
visitDoWhileStatement: ignore,
visitForStatement: ignore,
visitForInStatement: ignore,
visitAssignmentExpression: function(path) {
// Ignore anything that is not `exports.X = ...;` or
// `module.exports = ...;`
if (!isExportsOrModuleAssignment(path)) {
return false;
}
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
path = resolveToValue(path.get('right'));
if (!isReactCreateClassCall(path)) {
return false;
}
if (definition) {
// If a file exports multiple components, ... complain!
throw new Error(ERROR_MULTIPLE_DEFINITIONS);
}
// We found React.createClass. Lets get cracking!
var resolvedPath = resolveToValue(path.get('arguments', 0));
if (types.ObjectExpression.check(resolvedPath.node)) {
definition = resolvedPath;
}
return false;
}
});
return definition;
}
module.exports = findExportedReactCreateClass;

View File

@ -0,0 +1,36 @@
"use strict";
jest.autoMockOff();
describe('isExportsOrModuleAssignment', function() {
var recast;
var isExportsOrModuleAssignment;
function parse(src) {
return new recast.types.NodePath(
recast.parse(src).program.body[0]
);
}
beforeEach(function() {
isExportsOrModuleAssignment = require('../isExportsOrModuleAssignment');
recast = require('recast');
});
it('detects "module.exports = ...;"', function() {
expect(isExportsOrModuleAssignment(parse('module.exports = foo;')))
.toBe(true);
});
it('detects "exports.foo = ..."', function() {
expect(isExportsOrModuleAssignment(parse('exports.foo = foo;')))
.toBe(true);
});
it('does not accept "exports = foo;"', function() {
// That doesn't actually export anything
expect(isExportsOrModuleAssignment(parse('exports = foo;')))
.toBe(false);
});
});

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var expressionTo = require('./expressionTo');
var types = require('recast').types.namedTypes;
/**
* Returns true if the expression is of form `exports.foo = ...;` or
* `modules.exports = ...;`.
*/
function isExportsOrModuleAssignment(path: NodePath): boolean {
if (types.ExpressionStatement.check(path.node)) {
path = path.get('expression');
}
if (!types.AssignmentExpression.check(path.node) ||
!types.MemberExpression.check(path.node.left)) {
return false;
}
var exprArr = expressionTo.Array(path.get('left'));
return (exprArr[0] === 'module' && exprArr[1] === 'exports') ||
exprArr[0] == 'exports';
}
module.exports = isExportsOrModuleAssignment;

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var isReactModuleName = require('./isReactModuleName');
var match = require('./match');
var resolveToModule = require('./resolveToModule');
var types = require('recast').types.namedTypes;
/**
* Returns true if the expression is a function call of the form
* `React.createClass(...)`.
*/
function isReactCreateClassCall(path: NodePath): boolean {
if (types.ExpressionStatement.check(path.node)) {
path = path.get('expression');
}
if (!match(path.node, {callee: {property: {name: 'createClass'}}})) {
return false;
}
var module = resolveToModule(path.get('callee', 'object'));
return module && isReactModuleName(module);
}
module.exports = isReactCreateClassCall;

View File

@ -25,7 +25,7 @@
"recast": "^0.9.17"
},
"devDependencies": {
"jest-cli": "^0.2.2",
"jest-cli": "^0.3.0",
"react-tools": "^0.12.2"
},
"jest": {

View File

@ -14,7 +14,8 @@ function splitHeader(content) {
}
}
return {
header: lines.slice(1, i + 1).join('\n'),
header: i < lines.length - 1 ?
lines.slice(1, i + 1).join('\n') : null,
content: lines.slice(i + 1).join('\n')
};
}
@ -47,6 +48,9 @@ function execute() {
// Extract markdown metadata header
var both = splitHeader(content);
if (!both.header) {
return;
}
var lines = both.header.split('\n');
for (var i = 0; i < lines.length - 1; ++i) {
var keyvalue = lines[i].split(':');

View File

@ -1,4 +1,10 @@
var docs = require('../react-docgen');
var findExportedReactCreateClassCall = require(
'../react-docgen/dist/strategies/findExportedReactCreateClassCall'
);
var findAllReactCreateClassCalls = require(
'../react-docgen/dist/strategies/findAllReactCreateClassCalls'
);
var fs = require('fs');
var path = require('path');
var slugify = require('../core/slugify');
@ -12,7 +18,14 @@ function getNameFromPath(filepath) {
}
function docsToMarkdown(filepath, i) {
var json = docs.parseSource(fs.readFileSync(filepath));
var json = docs.parseSource(
fs.readFileSync(filepath),
function(node, recast) {
return findExportedReactCreateClassCall(node, recast) ||
findAllReactCreateClassCalls(node, recast)[0];
}
)
var componentName = getNameFromPath(filepath);
var res = [
@ -45,7 +58,7 @@ var components = [
'../Libraries/Components/TextInput/TextInput.ios.js',
'../Libraries/Components/Touchable/TouchableHighlight.js',
'../Libraries/Components/Touchable/TouchableWithoutFeedback.js',
// '../Libraries/Components/View/View.js',
'../Libraries/Components/View/View.js',
];
module.exports = function() {