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; +}