diff --git a/src/__tests__/main-test.js b/src/__tests__/main-test.js
index f8cc18a1c11..ae1e4e808ac 100644
--- a/src/__tests__/main-test.js
+++ b/src/__tests__/main-test.js
@@ -13,10 +13,11 @@
jest.autoMockOff();
describe('main', () => {
- var docgen;
+ var docgen, ERROR_MISSING_DEFINITION;
beforeEach(() => {
docgen = require('../main');
+ ({ERROR_MISSING_DEFINITION} = require('../parse'));
});
function test(source) {
@@ -99,4 +100,114 @@ describe('main', () => {
`);
});
+ describe('Stateless Component definition: ArrowFunctionExpression', () => {
+ test(`
+ import React, {PropTypes} from "React";
+
+ /**
+ * Example component description
+ */
+ let Component = props =>
;
+ Component.displayName = 'ABC';
+ Component.defaultProps = {
+ foo: true
+ };
+
+ Component.propTypes = {
+ /**
+ * Example prop description
+ */
+ foo: PropTypes.bool
+ };
+
+ export default Component;
+ `);
+ });
+
+ describe('Stateless Component definition: FunctionDeclaration', () => {
+ test(`
+ import React, {PropTypes} from "React";
+
+ /**
+ * Example component description
+ */
+ function Component (props) {
+ return ;
+ }
+
+ Component.displayName = 'ABC';
+ Component.defaultProps = {
+ foo: true
+ };
+
+ Component.propTypes = {
+ /**
+ * Example prop description
+ */
+ foo: PropTypes.bool
+ };
+
+ export default Component;
+ `);
+ });
+
+ describe('Stateless Component definition: FunctionExpression', () => {
+ test(`
+ import React, {PropTypes} from "React";
+
+ /**
+ * Example component description
+ */
+ let Component = function(props) {
+ return React.createElement('div', null);
+ }
+
+ Component.displayName = 'ABC';
+ Component.defaultProps = {
+ foo: true
+ };
+
+ Component.propTypes = {
+ /**
+ * Example prop description
+ */
+ foo: PropTypes.bool
+ };
+
+ export default Component;
+ `);
+ });
+
+ describe('Stateless Component definition', () => {
+ it('is not so greedy', () => {
+ const source = `
+ import React, {PropTypes} from "React";
+
+ /**
+ * Example component description
+ */
+ let NotAComponent = function(props) {
+ let HiddenComponent = () => React.createElement('div', null);
+
+ return 7;
+ }
+
+ NotAComponent.displayName = 'ABC';
+ NotAComponent.defaultProps = {
+ foo: true
+ };
+
+ NotAComponent.propTypes = {
+ /**
+ * Example prop description
+ */
+ foo: PropTypes.bool
+ };
+
+ export default NotAComponent;
+ `;
+
+ expect(() => docgen.parse(source)).toThrow(ERROR_MISSING_DEFINITION);
+ });
+ });
});
diff --git a/src/handlers/__tests__/propTypeHandler-test.js b/src/handlers/__tests__/propTypeHandler-test.js
index afcb6b30fa8..43bda1281c1 100644
--- a/src/handlers/__tests__/propTypeHandler-test.js
+++ b/src/handlers/__tests__/propTypeHandler-test.js
@@ -184,6 +184,16 @@ describe('propTypeHandler', () => {
);
});
+ describe('stateless component', () => {
+ test(
+ propTypesSrc => template(`
+ var Component = (props) => ;
+ Component.propTypes = ${propTypesSrc};
+ `),
+ src => statement(src)
+ );
+ });
+
it('does not error if propTypes cannot be found', () => {
var definition = expression('{fooBar: 42}');
expect(() => propTypeHandler(documentation, definition))
diff --git a/src/resolver/__tests__/findAllComponentDefinitions-test.js b/src/resolver/__tests__/findAllComponentDefinitions-test.js
index 3e2b44f5ee9..0061f7fdaf0 100644
--- a/src/resolver/__tests__/findAllComponentDefinitions-test.js
+++ b/src/resolver/__tests__/findAllComponentDefinitions-test.js
@@ -149,4 +149,60 @@ describe('findAllComponentDefinitions', () => {
});
});
+ describe('stateless components', () => {
+
+ it('finds stateless components', () => {
+ var source = `
+ import React from 'React';
+ let ComponentA = () => ;
+ function ComponentB () { return React.createElement('div', null); }
+ const ComponentC = function(props) { return {props.children}
; };
+ var Obj = {
+ component() { if (true) { return ; } }
+ };
+ const ComponentD = function(props) {
+ var result = {props.children}
;
+ return result;
+ };
+ const ComponentE = function(props) {
+ var result = () => {props.children}
;
+ return result();
+ };
+ const ComponentF = function(props) {
+ var helpers = {
+ comp() { return {props.children}
; }
+ };
+ return helpers.comp();
+ };
+ `;
+
+ var result = parse(source);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(7);
+ });
+
+ it('finds React.createElement, independent of the var name', () => {
+ var source = `
+ import AlphaBetters from 'react';
+ function ComponentA () { return AlphaBetters.createElement('div', null); }
+ function ComponentB () { return 7; }
+ `;
+
+ var result = parse(source);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(1);
+ });
+
+ it('does not process X.createClass of other modules', () => {
+ var source = `
+ import R from 'FakeReact';
+ const ComponentA = () => R.createElement('div', null);
+ `;
+
+ var result = parse(source);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(0);
+ });
+ });
+
});
diff --git a/src/resolver/__tests__/findExportedComponentDefinition-test.js b/src/resolver/__tests__/findExportedComponentDefinition-test.js
index e45c5b01a15..5a79bbbe045 100644
--- a/src/resolver/__tests__/findExportedComponentDefinition-test.js
+++ b/src/resolver/__tests__/findExportedComponentDefinition-test.js
@@ -106,6 +106,40 @@ describe('findExportedComponentDefinition', () => {
});
});
+ describe('stateless components', () => {
+
+ it('finds stateless component with JSX', () => {
+ var source = `
+ var React = require("React");
+ var Component = () => ;
+ module.exports = Component;
+ `;
+
+ expect(parse(source)).toBeDefined();
+ });
+
+ it('finds stateless components with React.createElement, independent of the var name', () => {
+ var source = `
+ var R = require("React");
+ var Component = () => R.createElement('div', {});
+ module.exports = Component;
+ `;
+
+ expect(parse(source)).toBeDefined();
+ });
+
+ it('does not process X.createElement of other modules', () => {
+ var source = `
+ var R = require("NoReact");
+ var Component = () => R.createElement({});
+ module.exports = Component;
+ `;
+
+ expect(parse(source)).toBeUndefined();
+ });
+
+ });
+
describe('module.exports = ; / exports.foo = ;', () => {
describe('React.createClass', () => {
@@ -449,6 +483,75 @@ describe('findExportedComponentDefinition', () => {
});
});
+ describe.only('stateless components', () => {
+
+ it('finds named exports', () => {
+ var source = `
+ import React from 'React';
+ export var somethingElse = 42,
+ Component = () => ;
+ `;
+ var result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ArrowFunctionExpression');
+
+ source = `
+ import React from 'React';
+ export let Component = () => ,
+ somethingElse = 42;
+ `;
+ result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ArrowFunctionExpression');
+
+ source = `
+ import React from 'React';
+ export const something = 21,
+ Component = () => ,
+ somethingElse = 42;
+ `;
+ result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ArrowFunctionExpression');
+
+ source = `
+ import React from 'React';
+ export var somethingElse = function() {};
+ export let Component = () =>
+ `;
+ result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ArrowFunctionExpression');
+ });
+
+ it('errors if multiple components are exported', () => {
+ var source = `
+ import React from 'React';
+ export var ComponentA = () =>
+ export var ComponentB = () =>
+ `;
+ expect(() => parse(source)).toThrow();
+
+ source = `
+ import React from 'React';
+ export var ComponentA = () =>
+ var ComponentB = () =>
+ export {ComponentB};
+ `;
+ expect(() => parse(source)).toThrow();
+ });
+
+ it('accepts multiple definitions if only one is exported', () => {
+ var source = `
+ import React from 'React';
+ var ComponentA = class extends React.Component {}
+ export var ComponentB = function() { return ; };
+ `;
+ var result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('FunctionExpression');
+ });
+ });
});
describe('export {};', () => {
@@ -566,6 +669,65 @@ describe('findExportedComponentDefinition', () => {
});
+ describe('stateless components', () => {
+
+ it('finds exported specifiers', () => {
+ var source = `
+ import React from 'React';
+ var foo = 42;
+ function Component = () { return ; }
+ export {foo, Component};
+ `;
+ var result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ClassExpression');
+
+ source = `
+ import React from 'React';
+ var foo = 42;
+ var Component = () => ;
+ export {Component, foo};
+ `;
+ result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ClassExpression');
+
+ source = `
+ import React from 'React';
+ var foo = 42;
+ var baz = 21;
+ var Component = function () { return ; }
+ export {foo, Component as bar, baz};
+ `;
+ result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ClassExpression');
+ });
+
+ it('errors if multiple components are exported', () => {
+ var source = `
+ import React from 'React';
+ var ComponentA = () => ;
+ function ComponentB() { return ; }
+ export {ComponentA as foo, ComponentB};
+ `;
+
+ expect(() => parse(source)).toThrow();
+ });
+
+ it('accepts multiple definitions if only one is exported', () => {
+ var source = `
+ import React from 'React';
+ var ComponentA = () => ;
+ var ComponentB = () => ;
+ export {ComponentA};
+ `;
+ var result = parse(source);
+ expect(result).toBeDefined();
+ expect(result.node.type).toBe('ArrowFunctionExpression');
+ });
+
+ });
});
// Only applies to classes
diff --git a/src/resolver/findAllComponentDefinitions.js b/src/resolver/findAllComponentDefinitions.js
index de82adf7abd..1633088728a 100644
--- a/src/resolver/findAllComponentDefinitions.js
+++ b/src/resolver/findAllComponentDefinitions.js
@@ -12,6 +12,7 @@
import isReactComponentClass from '../utils/isReactComponentClass';
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
+import isStatelessComponent from '../utils/isStatelessComponent';
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
import resolveToValue from '../utils/resolveToValue';
@@ -34,7 +35,17 @@ export default function findAllReactCreateClassCalls(
return false;
}
+ function statelessVisitor(path) {
+ if (isStatelessComponent(path)) {
+ definitions.push(path);
+ }
+ return false;
+ }
+
recast.visit(ast, {
+ visitFunctionDeclaration: statelessVisitor,
+ visitFunctionExpression: statelessVisitor,
+ visitArrowFunctionExpression: statelessVisitor,
visitClassExpression: classVisitor,
visitClassDeclaration: classVisitor,
visitCallExpression: function(path) {
diff --git a/src/resolver/findExportedComponentDefinition.js b/src/resolver/findExportedComponentDefinition.js
index 024b6861f81..785e88450fb 100644
--- a/src/resolver/findExportedComponentDefinition.js
+++ b/src/resolver/findExportedComponentDefinition.js
@@ -12,6 +12,7 @@
import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment';
import isReactComponentClass from '../utils/isReactComponentClass';
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
+import isStatelessComponent from '../utils/isStatelessComponent';
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
import resolveToValue from '../utils/resolveToValue';
@@ -24,7 +25,7 @@ function ignore() {
}
function isComponentDefinition(path) {
- return isReactCreateClassCall(path) || isReactComponentClass(path);
+ return isReactCreateClassCall(path) || isReactComponentClass(path) || isStatelessComponent(path);
}
function resolveDefinition(definition, types) {
@@ -37,6 +38,8 @@ function resolveDefinition(definition, types) {
} else if(isReactComponentClass(definition)) {
normalizeClassDefinition(definition);
return definition;
+ } else if (isStatelessComponent(definition)) {
+ return definition;
}
return null;
}
@@ -51,7 +54,7 @@ function resolveDefinition(definition, types) {
* If a definition is part of the following statements, it is considered to be
* exported:
*
- * modules.exports = Defintion;
+ * modules.exports = Definition;
* exports.foo = Definition;
* export default Definition;
* export var Definition = ...;
diff --git a/src/utils/__tests__/getMemberExpressionValuePath-test.js b/src/utils/__tests__/getMemberExpressionValuePath-test.js
new file mode 100644
index 00000000000..d7a75de584c
--- /dev/null
+++ b/src/utils/__tests__/getMemberExpressionValuePath-test.js
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ *
+ */
+
+/*global jest, describe, beforeEach, it, expect*/
+
+jest.autoMockOff();
+
+describe('getMemberExpressionValuePath', () => {
+ var getMemberExpressionValuePath;
+ var statement;
+
+ beforeEach(() => {
+ getMemberExpressionValuePath = require('../getMemberExpressionValuePath');
+ ({statement} = require('../../../tests/utils'));
+ });
+
+ describe('MethodExpression', () => {
+ it('finds "normal" property definitions', () => {
+ var def = statement(`
+ var Foo = () => {};
+ Foo.propTypes = {};
+ `);
+
+ expect(getMemberExpressionValuePath(def, 'propTypes'))
+ .toBe(def.parent.get('body', 1, 'expression', 'right'));
+ });
+
+ it('finds computed property definitions with literal keys', () => {
+ var def = statement(`
+ function Foo () {}
+ Foo['render'] = () => {};
+ `);
+
+ expect(getMemberExpressionValuePath(def, 'render'))
+ .toBe(def.parent.get('body', 1, 'expression', 'right'));
+ });
+
+ it('ignores computed property definitions with expression', () => {
+ var def = statement(`
+ var Foo = function Bar() {};
+ Foo[imComputed] = () => {};
+ `);
+
+ expect(getMemberExpressionValuePath(def, 'imComputed')).not.toBeDefined();
+ });
+ });
+});
diff --git a/src/utils/__tests__/isStatelessComponent-test.js b/src/utils/__tests__/isStatelessComponent-test.js
new file mode 100644
index 00000000000..f9753e12eaf
--- /dev/null
+++ b/src/utils/__tests__/isStatelessComponent-test.js
@@ -0,0 +1,253 @@
+/*
+ * 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.
+ *
+ */
+
+/*global jest, describe, beforeEach, it, expect*/
+
+
+jest.autoMockOff();
+
+describe('isStatelessComponent', () => {
+ var isStatelessComponent;
+ var statement, parse;
+
+ beforeEach(() => {
+ isStatelessComponent = require('../isStatelessComponent');
+ ({statement, parse} = require('../../../tests/utils'));
+ });
+
+ describe('Stateless Function Components with JSX', () => {
+ it('accepts simple arrow function components', () => {
+ var def = parse(
+ 'var Foo = () => '
+ ).get('body', 0).get('declarations', [0]).get('init');
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+
+ it('accepts simple function expressions components', () => {
+ var def = parse(
+ 'let Foo = function() { return ; };'
+ ).get('body', 0).get('declarations', [0]).get('init');
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+
+ it('accepts simple function declaration components', () => {
+ var def = parse('function Foo () { return }').get('body', 0);
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+ });
+
+ describe('Stateless Function Components with React.createElement', () => {
+ it('accepts simple arrow function components', () => {
+ var def = parse(`
+ var AlphaBetters = require('react');
+ var Foo = () => AlphaBetters.createElement("div", null);
+ `).get('body', 1).get('declarations', [0]).get('init');
+
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+
+ it('accepts simple function expressions components', () => {
+ var def = parse(`
+ var React = require('react');
+ let Foo = function() { return React.createElement("div", null); };
+ `).get('body', 1).get('declarations', [0]).get('init');
+
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+
+ it('accepts simple function declaration components', () => {
+ var def = parse(`
+ var React = require('react');
+ function Foo () { return React.createElement("div", null); }
+ `).get('body', 1);
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+ });
+
+ describe('Stateless Function Components inside module pattern', () => {
+ it('', () => {
+ var def = parse(`
+ var React = require('react');
+ var Foo = {
+ Bar() { return ; },
+ Baz: function() { return React.createElement('div'); },
+ ['hello']: function() { return React.createElement('div'); },
+ render() { return 7; }
+ }
+ `).get('body', 1).get('declarations', 0).get('init');
+
+ var bar = def.get('properties', 0);
+ var baz = def.get('properties', 1);
+ var hello = def.get('properties', 2);
+ var render = def.get('properties', 3);
+
+ expect(isStatelessComponent(bar)).toBe(true);
+ expect(isStatelessComponent(baz)).toBe(true);
+ expect(isStatelessComponent(hello)).toBe(true);
+ expect(isStatelessComponent(render)).toBe(false);
+ });
+ });
+
+ describe('is not overzealous', () => {
+ it('does not accept declarations with a render method', () => {
+ var def = statement(`
+ class Foo {
+ render() {
+ return ;
+ }
+ }
+ `);
+ expect(isStatelessComponent(def)).toBe(false);
+ });
+
+ it('does not accept React.Component classes', () => {
+ var def = parse(`
+ var React = require('react');
+ class Foo extends React.Component {
+ render() {
+ return ;
+ }
+ }
+ `).get('body', 1);
+
+ expect(isStatelessComponent(def)).toBe(false);
+ });
+
+ it('does not accept React.createClass calls', () => {
+ var def = statement(`
+ React.createClass({
+ render() {
+ return ;
+ }
+ });
+ `);
+
+ expect(isStatelessComponent(def)).toBe(false);
+ });
+
+ it('does not mark containing functions as StatelessComponents', () => {
+ var def = parse(`
+ var React = require('react');
+ function Foo (props) {
+ function Bar() {
+ return React.createElement("div", props);
+ }
+
+ return {Bar}
+ }
+ `).get('body', 1);
+
+ expect(isStatelessComponent(def)).toBe(false);
+ });
+ });
+
+ describe('resolving return values', () => {
+ function test(desc, code) {
+ it(desc, () => {
+ var def = parse(code).get('body', 1);
+
+ expect(isStatelessComponent(def)).toBe(true);
+ });
+ }
+
+ test('handles simple resolves', `
+ var React = require('react');
+ function Foo (props) {
+ function bar() {
+ return React.createElement("div", props);
+ }
+
+ return bar();
+ }
+ `);
+
+ test('handles reference resolves', `
+ var React = require('react');
+ function Foo (props) {
+ var result = bar();
+
+ return result;
+
+ function bar() {
+ return ;
+ }
+ }
+ `);
+
+ test('handles shallow member call expression resolves', `
+ var React = require('react');
+ function Foo (props) {
+ var shallow = {
+ shallowMember() {
+ return ;
+ }
+ };
+
+ return shallow.shallowMember();
+ }
+ `);
+
+ test('handles deep member call expression resolves', `
+ var React = require('react');
+ function Foo (props) {
+ var obj = {
+ deep: {
+ member() {
+ return ;
+ }
+ }
+ };
+
+ return obj.deep.member();
+ }
+ `);
+
+ test('handles external reference member call expression resolves', `
+ var React = require('react');
+ function Foo (props) {
+ var member = () => ;
+ var obj = {
+ deep: {
+ member: member
+ }
+ };
+
+ return obj.deep.member();
+ }
+ `);
+
+ test('handles external reference member call expression resolves', `
+ var React = require('react');
+ function Foo (props) {
+ var member = () => ;
+ var obj = {
+ deep: {
+ member: member
+ }
+ };
+
+ return obj.deep.member();
+ }
+ `);
+
+ test('handles all sorts of JavaScript things', `
+ var React = require('react');
+ function Foo (props) {
+ var external = {
+ member: () =>
+ };
+ var obj = {external};
+
+ return obj.external.member();
+ }
+ `);
+ });
+});
+
diff --git a/src/utils/getMemberExpressionValuePath.js b/src/utils/getMemberExpressionValuePath.js
new file mode 100644
index 00000000000..c774e3cfbdb
--- /dev/null
+++ b/src/utils/getMemberExpressionValuePath.js
@@ -0,0 +1,96 @@
+/*
+ * 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
+ *
+ */
+
+import getNameOrValue from './getNameOrValue';
+import recast from 'recast';
+
+var {types: {namedTypes: types}} = recast;
+
+function resolveName(path) {
+ if (types.VariableDeclaration.check(path.node)) {
+ var declarations = path.get('declarations');
+ if (declarations.value.length && declarations.value.length !== 1) {
+ throw new TypeError(
+ 'Got unsupported VariableDeclaration. VariableDeclaration must only ' +
+ 'have a single VariableDeclarator. Got ' + declarations.value.length +
+ ' declarations.'
+ );
+ }
+ var value = declarations.get(0, 'id', 'name').value;
+ return value;
+ }
+
+ if (types.FunctionDeclaration.check(path.node)) {
+ return path.get('id', 'name').value;
+ }
+
+ if (
+ types.FunctionExpression.check(path.node) ||
+ types.ArrowFunctionExpression.check(path.node)
+ ) {
+ if (!types.VariableDeclarator.check(path.parent.node)) {
+ return; // eslint-disable-line consistent-return
+ }
+
+ return path.parent.get('id', 'name').value;
+ }
+
+ throw new TypeError(
+ 'Attempted to resolveName for an unsupported path. resolveName accepts a ' +
+ 'VariableDeclaration, FunctionDeclaration, or FunctionExpression. Got "' +
+ path.node.type + '".'
+ );
+}
+
+function getRoot(node) {
+ var root = node.parent;
+ while (root.parent) {
+ root = root.parent;
+ }
+ return root;
+}
+
+export default function getMemberExpressionValuePath(
+ variableDefinition: NodePath,
+ memberName: string
+): ?NodePath {
+ var localName = resolveName(variableDefinition);
+ var program = getRoot(variableDefinition);
+
+ if (!localName) {
+ // likely an immediately exported and therefore nameless/anonymous node
+ // passed in
+ return;
+ }
+
+ var result;
+ recast.visit(program, {
+ visitAssignmentExpression(path) {
+ var memberPath = path.get('left');
+ if (!types.MemberExpression.check(memberPath.node)) {
+ return this.traverse(path);
+ }
+
+ if (
+ (!memberPath.node.computed || types.Literal.check(memberPath.node.property)) &&
+ getNameOrValue(memberPath.get('property')) === memberName
+ ) {
+ result = path.get('right');
+ return false;
+ }
+
+ this.traverse(memberPath);
+ },
+ });
+
+ return result; // eslint-disable-line consistent-return
+}
diff --git a/src/utils/getMemberValuePath.js b/src/utils/getMemberValuePath.js
index 92dddffd608..a074c57f346 100644
--- a/src/utils/getMemberValuePath.js
+++ b/src/utils/getMemberValuePath.js
@@ -10,6 +10,7 @@
*
*/
+import getMemberExpressionValuePath from './getMemberExpressionValuePath';
import getClassMemberValuePath from './getClassMemberValuePath';
import getPropertyValuePath from './getPropertyValuePath';
import recast from 'recast';
@@ -22,6 +23,10 @@ var SYNONYMS = {
};
var LOOKUP_METHOD = {
+ [types.ArrowFunctionExpression.name]: getMemberExpressionValuePath,
+ [types.FunctionExpression.name]: getMemberExpressionValuePath,
+ [types.FunctionDeclaration.name]: getMemberExpressionValuePath,
+ [types.VariableDeclaration.name]: getMemberExpressionValuePath,
[types.ObjectExpression.name]: getPropertyValuePath,
[types.ClassDeclaration.name]: getClassMemberValuePath,
[types.ClassExpression.name]: getClassMemberValuePath,
@@ -30,7 +35,13 @@ var LOOKUP_METHOD = {
function isSupportedDefinitionType({node}) {
return types.ObjectExpression.check(node) ||
types.ClassDeclaration.check(node) ||
- types.ClassExpression.check(node);
+ types.ClassExpression.check(node) ||
+
+ // potential stateless function component
+ types.VariableDeclaration.check(node) ||
+ types.ArrowFunctionExpression.check(node) ||
+ types.FunctionDeclaration.check(node) ||
+ types.FunctionExpression.check(node);
}
/**
@@ -52,9 +63,11 @@ export default function getMemberValuePath(
): ?NodePath {
if (!isSupportedDefinitionType(componentDefinition)) {
throw new TypeError(
- 'Got unsupported definition type. Definition must either be an ' +
- 'ObjectExpression, ClassDeclaration or ClassExpression. Got "' +
- componentDefinition.node.type + '" instead.'
+ 'Got unsupported definition type. Definition must be one of ' +
+ 'ObjectExpression, ClassDeclaration, ClassExpression,' +
+ 'VariableDeclaration, ArrowFunctionExpression, FunctionExpression, or ' +
+ 'FunctionDeclaration. Got "' + componentDefinition.node.type + '"' +
+ 'instead.'
);
}
diff --git a/src/utils/isReactCreateElementCall.js b/src/utils/isReactCreateElementCall.js
new file mode 100644
index 00000000000..20836a908c4
--- /dev/null
+++ b/src/utils/isReactCreateElementCall.js
@@ -0,0 +1,34 @@
+/*
+ * 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
+ *
+ */
+
+import isReactModuleName from './isReactModuleName';
+import match from './match';
+import recast from 'recast';
+import resolveToModule from './resolveToModule';
+
+var {types: {namedTypes: types}} = recast;
+
+/**
+ * Returns true if the expression is a function call of the form
+ * `React.createElement(...)`.
+ */
+export default function isReactCreateElementCall(path: NodePath): boolean {
+ if (types.ExpressionStatement.check(path.node)) {
+ path = path.get('expression');
+ }
+
+ if (!match(path.node, {callee: {property: {name: 'createElement'}}})) {
+ return false;
+ }
+ var module = resolveToModule(path.get('callee', 'object'));
+ return Boolean(module && isReactModuleName(module));
+}
diff --git a/src/utils/isStatelessComponent.js b/src/utils/isStatelessComponent.js
new file mode 100644
index 00000000000..6336e890d44
--- /dev/null
+++ b/src/utils/isStatelessComponent.js
@@ -0,0 +1,151 @@
+/*
+ * 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
+ */
+
+import getPropertyValuePath from './getPropertyValuePath';
+import isReactComponentClass from './isReactComponentClass';
+import isReactCreateClassCall from './isReactCreateClassCall';
+import isReactCreateElementCall from './isReactCreateElementCall';
+import recast from 'recast';
+import resolveToValue from './resolveToValue';
+
+var {types: {namedTypes: types}} = recast;
+
+const reNonLexicalBlocks = /^If|^Else|^Switch/;
+
+const validPossibleStatelessComponentTypes = [
+ 'Property',
+ 'FunctionDeclaration',
+ 'FunctionExpression',
+ 'ArrowFunctionExpression',
+];
+
+function isJSXElementOrReactCreateElement(path) {
+ return (
+ path.node.type === 'JSXElement' ||
+ (path.node.type === 'CallExpression' && isReactCreateElementCall(path))
+ );
+}
+
+function returnsJSXElementOrReactCreateElementCall(path) {
+ let visited = false;
+
+ // early exit for ArrowFunctionExpressions
+ if (isJSXElementOrReactCreateElement(path.get('body'))) {
+ return true;
+ }
+
+ function isSameBlockScope(p) {
+ let block = p;
+ do {
+ block = block.parent;
+ // jump over non-lexical blocks
+ if (reNonLexicalBlocks.test(block.parent.node.type)) {
+ block = block.parent;
+ }
+ } while (
+ !types.BlockStatement.check(block.node) &&
+ /Function|Property/.test(block.parent.parent.node.type) &&
+ !reNonLexicalBlocks.test(block.parent.node.type)
+ );
+
+ // special case properties
+ if (types.Property.check(path.node)) {
+ return block.node === path.get('value', 'body').node;
+ }
+
+ return block.node === path.get('body').node;
+ }
+
+ recast.visit(path, {
+ visitReturnStatement(returnPath) {
+ const resolvedPath = resolveToValue(returnPath.get('argument'));
+ if (
+ isJSXElementOrReactCreateElement(resolvedPath) &&
+ isSameBlockScope(returnPath)
+ ) {
+ visited = true;
+ return false;
+ }
+
+ if (resolvedPath.node.type === 'CallExpression') {
+ let calleeValue = resolveToValue(resolvedPath.get('callee'));
+
+ if (returnsJSXElementOrReactCreateElementCall(calleeValue)) {
+ visited = true;
+ return false;
+ }
+
+ let resolvedValue;
+
+ let namesToResolve = [calleeValue.get('property')];
+
+ if (calleeValue.node.type === 'MemberExpression') {
+ if (calleeValue.get('object').node.type === 'Identifier') {
+ resolvedValue = resolveToValue(calleeValue.get('object'));
+ }
+ else {
+ while (calleeValue.get('object').node.type !== 'Identifier') {
+ calleeValue = calleeValue.get('object');
+ namesToResolve.unshift(calleeValue.get('property'));
+ }
+
+ resolvedValue = resolveToValue(calleeValue.get('object'));
+ }
+ }
+
+ if (types.ObjectExpression.check(resolvedValue.node)) {
+ var resolvedMemberExpression = namesToResolve
+ .reduce((result, path) => { // eslint-disable-line no-shadow
+ result = getPropertyValuePath(result, path.node.name);
+ if (types.Identifier.check(result.node)) {
+ return resolveToValue(result);
+ }
+ return result;
+ }, resolvedValue);
+
+ if (returnsJSXElementOrReactCreateElementCall(resolvedMemberExpression)) {
+ visited = true;
+ return false;
+ }
+ }
+ }
+
+ this.traverse(returnPath);
+ },
+ });
+
+ return visited;
+}
+
+/**
+ * Returns `true` if the path represents a function which returns a JSXElment
+ */
+export default function isStatelessComponent(
+ path: NodePath
+): bool {
+ var node = path.node;
+
+ if (validPossibleStatelessComponentTypes.indexOf(node.type) === -1) {
+ return false;
+ }
+
+ if (node.type === 'Property') {
+ if (isReactCreateClassCall(path.parent) || isReactComponentClass(path.parent)) {
+ return false;
+ }
+ }
+
+ if (returnsJSXElementOrReactCreateElementCall(path)) {
+ return true;
+ }
+
+ return false;
+}