Update react-docgen

This commit is contained in:
Felix Kling
2015-03-04 13:37:05 -08:00
parent 7b0cd86759
commit 74824cb96e
18 changed files with 228 additions and 697 deletions

View File

@@ -1,124 +0,0 @@
/*
* 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";
type Handler = (documentation: Documentation, path: NodePath) => void;
var Documentation = require('./Documentation');
var findExportedReactCreateClass =
require('./strategies/findExportedReactCreateClassCall');
var getPropertyName = require('./utils/getPropertyName');
var recast = require('recast');
var resolveToValue = require('./utils/resolveToValue');
var n = recast.types.namedTypes;
class ReactDocumentationParser {
_componentHandlers: Array<Handler>;
_apiHandlers: Object<string, Handler>;
constructor() {
this._componentHandlers = [];
this._apiHandlers = Object.create(null);
}
/**
* 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._apiHandlers[property]) {
this._apiHandlers[property] = [];
}
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,
componentDefinitionStrategy?:
(program: ASTNode, recast: Object) => (Array<NodePath>|NodePath)
): (Array<Object>|Object) {
if (!componentDefinitionStrategy) {
componentDefinitionStrategy = findExportedReactCreateClass;
}
var ast = recast.parse(source);
// Find the component definitions first. The return value should be
// an ObjectExpression.
var componentDefinition = componentDefinitionStrategy(ast.program, recast);
var isArray = Array.isArray(componentDefinition);
if (!componentDefinition || (isArray && componentDefinition.length === 0)) {
throw new Error(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
}
return isArray ?
this._executeHandlers(componentDefinition).map(
documentation => documentation.toObject()
) :
this._executeHandlers([componentDefinition])[0].toObject();
}
_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;
});
}
}
ReactDocumentationParser.ERROR_MISSING_DEFINITION =
'No suitable component definition found.';
module.exports = ReactDocumentationParser;

View File

@@ -1,61 +0,0 @@
/*
* 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 ReactDocumentationParser;
var parser;
var recast;
beforeEach(function() {
recast = require('recast');
ReactDocumentationParser = require('../ReactDocumentationParser');
parser = new ReactDocumentationParser();
});
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();
});
});
});

View File

@@ -1,81 +0,0 @@
/*
* 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();
var module_template = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var Component = React.createClass(%s);',
'module.exports = Component;'
].join('\n');
function getSource(definition) {
return module_template.replace('%s', definition);
}
describe('React documentation parser', function() {
var parser;
beforeEach(function() {
parser = new (require('../../ReactDocumentationParser'));
parser.addHandler(require('../defaultValueHandler'), 'getDefaultProps');
});
it ('should find prop default values that are literals', function() {
var source = getSource([
'{',
' getDefaultProps: function() {',
' return {',
' foo: "bar",',
' bar: 42,',
' baz: ["foo", "bar"],',
' abc: {xyz: abc.def, 123: 42}',
' };',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
defaultValue: {
value: '"bar"',
computed: false
}
},
bar: {
defaultValue: {
value: '42',
computed: false
}
},
baz: {
defaultValue: {
value: '["foo", "bar"]',
computed: false
}
},
abc: {
defaultValue: {
value: '{xyz: abc.def, 123: 42}',
computed: false
}
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
});

View File

@@ -25,12 +25,17 @@ describe('propDocblockHandler', function() {
});
function parse(definition) {
return utils.parse('(' + definition + ')').get('body', 0, 'expression');
var programPath = utils.parse(definition);
return programPath.get(
'body',
programPath.node.body.length - 1,
'expression'
);
}
it('finds docblocks for prop types', function() {
var definition = parse([
'{',
'({',
' propTypes: {',
' /**',
' * Foo comment',
@@ -42,7 +47,7 @@ describe('propDocblockHandler', function() {
' */',
' bar: Prop.bool,',
' }',
'}'
'})'
].join('\n'));
propDocblockHandler(documentation, definition);
@@ -58,7 +63,7 @@ describe('propDocblockHandler', function() {
it('can handle multline comments', function() {
var definition = parse([
'{',
'({',
' propTypes: {',
' /**',
' * Foo comment with',
@@ -68,7 +73,7 @@ describe('propDocblockHandler', function() {
' */',
' foo: Prop.bool',
' }',
'}'
'})'
].join('\n'));
propDocblockHandler(documentation, definition);
@@ -82,7 +87,7 @@ describe('propDocblockHandler', function() {
it('ignores non-docblock comments', function() {
var definition = parse([
'{',
'({',
' propTypes: {',
' /**',
' * Foo comment',
@@ -96,7 +101,7 @@ describe('propDocblockHandler', function() {
' /* This is not a doc comment */',
' bar: Prop.bool,',
' }',
'}'
'})'
].join('\n'));
propDocblockHandler(documentation, definition);
@@ -112,7 +117,7 @@ describe('propDocblockHandler', function() {
it('only considers the comment with the property below it', function() {
var definition = parse([
'{',
'({',
' propTypes: {',
' /**',
' * Foo comment',
@@ -120,7 +125,7 @@ describe('propDocblockHandler', function() {
' foo: Prop.bool,',
' bar: Prop.bool,',
' }',
'}'
'})'
].join('\n'));
propDocblockHandler(documentation, definition);
@@ -136,7 +141,7 @@ describe('propDocblockHandler', function() {
it('understands and ignores the spread operator', function() {
var definition = parse([
'{',
'({',
' propTypes: {',
' ...Foo.propTypes,',
' /**',
@@ -144,7 +149,28 @@ describe('propDocblockHandler', function() {
' */',
' foo: Prop.bool,',
' }',
'}'
'})'
].join('\n'));
propDocblockHandler(documentation, definition);
expect(documentation.descriptors).toEqual({
foo: {
description: 'Foo comment'
}
});
});
it('resolves variables', function() {
var definition = parse([
'var Props = {',
' /**',
' * Foo comment',
' */',
' foo: Prop.bool,',
'};',
'({',
' propTypes: Props',
'})'
].join('\n'));
propDocblockHandler(documentation, definition);

View File

@@ -171,4 +171,21 @@ describe('propTypeHandler', function() {
},
});
});
it('resolves variables', function() {
var definition = parse([
'var props = {bar: PropTypes.bool};',
'({',
' propTypes: props',
'})',
].join('\n'));
propTypeHandler(documentation, definition);
expect(documentation.descriptors).toEqual({
bar: {
type: {},
required: false
},
});
});
});

View File

@@ -1,77 +0,0 @@
/*
* 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 Documentation = require('../Documentation');
var expressionTo = require('../utils/expressionTo');
var getPropertyName = require('../utils/getPropertyName');
var recast = require('recast');
var resolveToValue = require('../utils/resolveToValue');
var types = recast.types.namedTypes;
var visit = recast.types.visit;
function getDefaultValue(path) {
var node = path.node;
var defaultValue;
if (types.Literal.check(node)) {
defaultValue = node.raw;
} else {
path = resolveToValue(path);
node = path.node;
defaultValue = recast.print(path).code;
}
if (typeof defaultValue !== 'undefined') {
return {
value: defaultValue,
computed: types.CallExpression.check(node) ||
types.MemberExpression.check(node) ||
types.Identifier.check(node)
};
}
}
function defaultValueHandler(documentation: Documentation, path: NodePath) {
if (!types.FunctionExpression.check(path.node)) {
return;
}
// Find the value that is returned from the function and process it if it is
// an object literal.
var objectExpressionPath;
visit(path.get('body'), {
visitFunction: () => false,
visitReturnStatement: function(path) {
var resolvedPath = resolveToValue(path.get('argument'));
if (types.ObjectExpression.check(resolvedPath.node)) {
objectExpressionPath = resolvedPath;
}
return false;
}
});
if (objectExpressionPath) {
objectExpressionPath.get('properties').each(function(propertyPath) {
var propDescriptor = documentation.getPropDescriptor(
getPropertyName(propertyPath)
);
var defaultValue = getDefaultValue(propertyPath.get('value'));
if (defaultValue) {
propDescriptor.defaultValue = defaultValue;
}
});
}
}
module.exports = defaultValueHandler;

View File

@@ -15,13 +15,14 @@
var Documentation = require('../Documentation');
var types = require('recast').types.namedTypes;
var getDocblock = require('../utils/docblock').getDocblock;
var getPropertyName = require('../utils/getPropertyName');
var getPropertyValuePath = require('../utils/getPropertyValuePath');
var types = require('recast').types.namedTypes;
var resolveToValue = require('../utils/resolveToValue');
function propDocBlockHandler(documentation: Documentation, path: NodePath) {
var propTypesPath = getPropertyValuePath(path, 'propTypes');
var propTypesPath = resolveToValue(getPropertyValuePath(path, 'propTypes'));
if (!propTypesPath || !types.ObjectExpression.check(propTypesPath.node)) {
return;
}

View File

@@ -100,7 +100,7 @@ function amendPropTypes(documentation, path) {
}
function propTypeHandler(documentation: Documentation, path: NodePath) {
var propTypesPath = getPropertyValuePath(resolveToValue(path), 'propTypes');
var propTypesPath = resolveToValue(getPropertyValuePath(path, 'propTypes'));
if (!propTypesPath || !types.ObjectExpression.check(propTypesPath.node)) {
return;
}

View File

@@ -1,106 +0,0 @@
/*
* 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

@@ -1,106 +0,0 @@
/*
* 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

@@ -1,47 +0,0 @@
/*
* 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

@@ -1,78 +0,0 @@
/*
* 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;