From 4305f9a1c87066d4de9921e4b253e46bad27ed6b Mon Sep 17 00:00:00 2001 From: tongbin Date: Fri, 25 Oct 2019 05:35:01 +0800 Subject: [PATCH] Support destructuring and aliased imports in react builtin call detection (#385) * fix: #380 * feat: Support destructuring and aliased imports in react builtin call detection --- .../__tests__/isReactCloneElementCall-test.js | 83 +++++++++++++++++ .../__tests__/isReactCreateClassCall-test.js | 32 +++++++ .../isReactCreateElementCall-test.js | 89 +++++++++++++++++++ .../__tests__/isReactForwardRefCall-test.js | 89 +++++++++++++++++++ src/utils/isReactBuiltinCall.js | 56 ++++++++++++ src/utils/isReactCloneElementCall.js | 19 +--- src/utils/isReactCreateClassCall.js | 21 +---- src/utils/isReactCreateElementCall.js | 17 +--- src/utils/isReactForwardRefCall.js | 17 +--- 9 files changed, 359 insertions(+), 64 deletions(-) create mode 100644 src/utils/__tests__/isReactCloneElementCall-test.js create mode 100644 src/utils/__tests__/isReactCreateElementCall-test.js create mode 100644 src/utils/__tests__/isReactForwardRefCall-test.js create mode 100644 src/utils/isReactBuiltinCall.js diff --git a/src/utils/__tests__/isReactCloneElementCall-test.js b/src/utils/__tests__/isReactCloneElementCall-test.js new file mode 100644 index 00000000000..d2596453cc8 --- /dev/null +++ b/src/utils/__tests__/isReactCloneElementCall-test.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { parse } from '../../../tests/utils'; +import isReactCloneElementCall from '../isReactCloneElementCall'; + +describe('isReactCloneElementCall', () => { + function parsePath(src) { + const root = parse(src); + return root.get('body', root.node.body.length - 1, 'expression'); + } + + describe('built in React.createClass', () => { + it('accepts cloneElement called on React', () => { + const def = parsePath(` + var React = require("React"); + React.cloneElement({}); + `); + expect(isReactCloneElementCall(def)).toBe(true); + }); + + it('accepts cloneElement called on aliased React', () => { + const def = parsePath(` + var other = require("React"); + other.cloneElement({}); + `); + expect(isReactCloneElementCall(def)).toBe(true); + }); + + it('ignores other React calls', () => { + const def = parsePath(` + var React = require("React"); + React.isValidElement({}); + `); + expect(isReactCloneElementCall(def)).toBe(false); + }); + + it('ignores non React calls to cloneElement', () => { + const def = parsePath(` + var React = require("bob"); + React.cloneElement({}); + `); + expect(isReactCloneElementCall(def)).toBe(false); + }); + + it('accepts cloneElement called on destructed value', () => { + const def = parsePath(` + var { cloneElement } = require("react"); + cloneElement({}); + `); + expect(isReactCloneElementCall(def)).toBe(true); + }); + + it('accepts cloneElement called on destructed aliased value', () => { + const def = parsePath(` + var { cloneElement: foo } = require("react"); + foo({}); + `); + expect(isReactCloneElementCall(def)).toBe(true); + }); + + it('accepts cloneElement called on imported value', () => { + const def = parsePath(` + import { cloneElement } from "react"; + cloneElement({}); + `); + expect(isReactCloneElementCall(def)).toBe(true); + }); + + it('accepts cloneElement called on imported aliased value', () => { + const def = parsePath(` + import { cloneElement as foo } from "react"; + foo({}); + `); + expect(isReactCloneElementCall(def)).toBe(true); + }); + }); +}); diff --git a/src/utils/__tests__/isReactCreateClassCall-test.js b/src/utils/__tests__/isReactCreateClassCall-test.js index edf30b40235..c7ea0c6abd0 100644 --- a/src/utils/__tests__/isReactCreateClassCall-test.js +++ b/src/utils/__tests__/isReactCreateClassCall-test.js @@ -53,6 +53,38 @@ describe('isReactCreateClassCall', () => { `); expect(isReactCreateClassCall(def)).toBe(false); }); + + it('accepts createClass called on destructed value', () => { + const def = parsePath(` + var { createClass } = require("react"); + createClass({}); + `); + expect(isReactCreateClassCall(def)).toBe(true); + }); + + it('accepts createClass called on destructed aliased value', () => { + const def = parsePath(` + var { createClass: foo } = require("react"); + foo({}); + `); + expect(isReactCreateClassCall(def)).toBe(true); + }); + + it('accepts createClass called on imported value', () => { + const def = parsePath(` + import { createClass } from "react"; + createClass({}); + `); + expect(isReactCreateClassCall(def)).toBe(true); + }); + + it('accepts createClass called on imported aliased value', () => { + const def = parsePath(` + import { createClass as foo } from "react"; + foo({}); + `); + expect(isReactCreateClassCall(def)).toBe(true); + }); }); describe('modular in create-react-class', () => { diff --git a/src/utils/__tests__/isReactCreateElementCall-test.js b/src/utils/__tests__/isReactCreateElementCall-test.js new file mode 100644 index 00000000000..67662b391b7 --- /dev/null +++ b/src/utils/__tests__/isReactCreateElementCall-test.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { parse } from '../../../tests/utils'; +import isReactCreateElementCall from '../isReactCreateElementCall'; + +describe('isReactCreateElementCall', () => { + function parsePath(src) { + const root = parse(src); + return root.get('body', root.node.body.length - 1, 'expression'); + } + + describe('built in React.createElement', () => { + it('accepts createElement called on React', () => { + const def = parsePath(` + var React = require("React"); + React.createElement({ + render() {} + }); + `); + expect(isReactCreateElementCall(def)).toBe(true); + }); + + it('accepts createElement called on aliased React', () => { + const def = parsePath(` + var other = require("React"); + other.createElement({ + render() {} + }); + `); + expect(isReactCreateElementCall(def)).toBe(true); + }); + + it('ignores other React calls', () => { + const def = parsePath(` + var React = require("React"); + React.isValidElement({}); + `); + expect(isReactCreateElementCall(def)).toBe(false); + }); + + it('ignores non React calls to createElement', () => { + const def = parsePath(` + var React = require("bob"); + React.createElement({ + render() {} + }); + `); + expect(isReactCreateElementCall(def)).toBe(false); + }); + + it('accepts createElement called on destructed value', () => { + const def = parsePath(` + var { createElement } = require("react"); + createElement({}); + `); + expect(isReactCreateElementCall(def)).toBe(true); + }); + + it('accepts createElement called on destructed aliased value', () => { + const def = parsePath(` + var { createElement: foo } = require("react"); + foo({}); + `); + expect(isReactCreateElementCall(def)).toBe(true); + }); + + it('accepts createElement called on imported value', () => { + const def = parsePath(` + import { createElement } from "react"; + createElement({}); + `); + expect(isReactCreateElementCall(def)).toBe(true); + }); + + it('accepts createElement called on imported aliased value', () => { + const def = parsePath(` + import { createElement as foo } from "react"; + foo({}); + `); + expect(isReactCreateElementCall(def)).toBe(true); + }); + }); +}); diff --git a/src/utils/__tests__/isReactForwardRefCall-test.js b/src/utils/__tests__/isReactForwardRefCall-test.js new file mode 100644 index 00000000000..2395f7cba9d --- /dev/null +++ b/src/utils/__tests__/isReactForwardRefCall-test.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { parse } from '../../../tests/utils'; +import isReactForwardRefCall from '../isReactForwardRefCall'; + +describe('isReactForwardRefCall', () => { + function parsePath(src) { + const root = parse(src); + return root.get('body', root.node.body.length - 1, 'expression'); + } + + describe('built in React.forwardRef', () => { + it('accepts forwardRef called on React', () => { + const def = parsePath(` + var React = require("React"); + React.forwardRef({ + render() {} + }); + `); + expect(isReactForwardRefCall(def)).toBe(true); + }); + + it('accepts forwardRef called on aliased React', () => { + const def = parsePath(` + var other = require("React"); + other.forwardRef({ + render() {} + }); + `); + expect(isReactForwardRefCall(def)).toBe(true); + }); + + it('ignores other React calls', () => { + const def = parsePath(` + var React = require("React"); + React.isValidElement({}); + `); + expect(isReactForwardRefCall(def)).toBe(false); + }); + + it('ignores non React calls to forwardRef', () => { + const def = parsePath(` + var React = require("bob"); + React.forwardRef({ + render() {} + }); + `); + expect(isReactForwardRefCall(def)).toBe(false); + }); + + it('accepts forwardRef called on destructed value', () => { + const def = parsePath(` + var { forwardRef } = require("react"); + forwardRef({}); + `); + expect(isReactForwardRefCall(def)).toBe(true); + }); + + it('accepts forwardRef called on destructed aliased value', () => { + const def = parsePath(` + var { forwardRef: foo } = require("react"); + foo({}); + `); + expect(isReactForwardRefCall(def)).toBe(true); + }); + + it('accepts forwardRef called on imported value', () => { + const def = parsePath(` + import { forwardRef } from "react"; + forwardRef({}); + `); + expect(isReactForwardRefCall(def)).toBe(true); + }); + + it('accepts forwardRef called on imported aliased value', () => { + const def = parsePath(` + import { forwardRef as foo } from "react"; + foo({}); + `); + expect(isReactForwardRefCall(def)).toBe(true); + }); + }); +}); diff --git a/src/utils/isReactBuiltinCall.js b/src/utils/isReactBuiltinCall.js new file mode 100644 index 00000000000..8b474dbcb29 --- /dev/null +++ b/src/utils/isReactBuiltinCall.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import types from 'ast-types'; +import isReactModuleName from './isReactModuleName'; +import match from './match'; +import resolveToModule from './resolveToModule'; +import resolveToValue from './resolveToValue'; + +const { namedTypes: t } = types; + +/** + * Returns true if the expression is a function call of the form + * `React.foo(...)`. + */ +export default function isReactBuiltinCall( + path: NodePath, + name: string, +): boolean { + if (t.ExpressionStatement.check(path.node)) { + path = path.get('expression'); + } + + if (match(path.node, { callee: { property: { name } } })) { + const module = resolveToModule(path.get('callee', 'object')); + return Boolean(module && isReactModuleName(module)); + } + + if (t.CallExpression.check(path.node)) { + const value = resolveToValue(path.get('callee')); + if (value === path.get('callee')) return false; + + if ( + // `require('react').createElement` + (t.MemberExpression.check(value.node) && + t.Identifier.check(value.get('property').node) && + value.get('property').node.name === name) || + // `import { createElement } from 'react'` + (t.ImportDeclaration.check(value.node) && + value.node.specifiers.some( + specifier => specifier.imported && specifier.imported.name === name, + )) + ) { + const module = resolveToModule(value); + return Boolean(module && isReactModuleName(module)); + } + } + + return false; +} diff --git a/src/utils/isReactCloneElementCall.js b/src/utils/isReactCloneElementCall.js index 3b2094c89ae..eb8e06aae2a 100644 --- a/src/utils/isReactCloneElementCall.js +++ b/src/utils/isReactCloneElementCall.js @@ -7,25 +7,12 @@ * @flow */ -import types from 'ast-types'; -import isReactModuleName from './isReactModuleName'; -import match from './match'; -import resolveToModule from './resolveToModule'; - -const { namedTypes: t } = types; +import isReactBuiltinCall from './isReactBuiltinCall'; /** * Returns true if the expression is a function call of the form - * `React.createElement(...)`. + * `React.cloneElement(...)`. */ export default function isReactCloneElementCall(path: NodePath): boolean { - if (t.ExpressionStatement.check(path.node)) { - path = path.get('expression'); - } - - if (!match(path.node, { callee: { property: { name: 'cloneElement' } } })) { - return false; - } - const module = resolveToModule(path.get('callee', 'object')); - return Boolean(module && isReactModuleName(module)); + return isReactBuiltinCall(path, 'cloneElement'); } diff --git a/src/utils/isReactCreateClassCall.js b/src/utils/isReactCreateClassCall.js index 484af2f30a8..3a914316e37 100644 --- a/src/utils/isReactCreateClassCall.js +++ b/src/utils/isReactCreateClassCall.js @@ -8,28 +8,12 @@ */ import types from 'ast-types'; -import isReactModuleName from './isReactModuleName'; import match from './match'; import resolveToModule from './resolveToModule'; +import isReactBuiltinCall from './isReactBuiltinCall'; const { namedTypes: t } = types; -/** - * Returns true if the expression is a function call of the form - * `React.createClass(...)`. - */ -function isReactCreateClassCallBuiltIn(path: NodePath): boolean { - if (t.ExpressionStatement.check(path.node)) { - path = path.get('expression'); - } - - if (!match(path.node, { callee: { property: { name: 'createClass' } } })) { - return false; - } - const module = resolveToModule(path.get('callee', 'object')); - return Boolean(module && isReactModuleName(module)); -} - /** * Returns true if the expression is a function call of the form * ``` @@ -59,6 +43,7 @@ function isReactCreateClassCallModular(path: NodePath): boolean { */ export default function isReactCreateClassCall(path: NodePath): boolean { return ( - isReactCreateClassCallBuiltIn(path) || isReactCreateClassCallModular(path) + isReactBuiltinCall(path, 'createClass') || + isReactCreateClassCallModular(path) ); } diff --git a/src/utils/isReactCreateElementCall.js b/src/utils/isReactCreateElementCall.js index 0bb81cb5dcb..2b1ff9079a8 100644 --- a/src/utils/isReactCreateElementCall.js +++ b/src/utils/isReactCreateElementCall.js @@ -7,25 +7,12 @@ * @flow */ -import types from 'ast-types'; -import isReactModuleName from './isReactModuleName'; -import match from './match'; -import resolveToModule from './resolveToModule'; - -const { namedTypes: t } = types; +import isReactBuiltinCall from './isReactBuiltinCall'; /** * Returns true if the expression is a function call of the form * `React.createElement(...)`. */ export default function isReactCreateElementCall(path: NodePath): boolean { - if (t.ExpressionStatement.check(path.node)) { - path = path.get('expression'); - } - - if (!match(path.node, { callee: { property: { name: 'createElement' } } })) { - return false; - } - const module = resolveToModule(path.get('callee', 'object')); - return Boolean(module && isReactModuleName(module)); + return isReactBuiltinCall(path, 'createElement'); } diff --git a/src/utils/isReactForwardRefCall.js b/src/utils/isReactForwardRefCall.js index 56d1d936e10..90d25eb78c5 100644 --- a/src/utils/isReactForwardRefCall.js +++ b/src/utils/isReactForwardRefCall.js @@ -6,25 +6,12 @@ * * @flow */ -import types from 'ast-types'; -import isReactModuleName from './isReactModuleName'; -import match from './match'; -import resolveToModule from './resolveToModule'; - -const { namedTypes: t } = types; +import isReactBuiltinCall from './isReactBuiltinCall'; /** * Returns true if the expression is a function call of the form * `React.forwardRef(...)`. */ export default function isReactForwardRefCall(path: NodePath): boolean { - if (t.ExpressionStatement.check(path.node)) { - path = path.get('expression'); - } - - if (!match(path.node, { callee: { property: { name: 'forwardRef' } } })) { - return false; - } - const module = resolveToModule(path.get('callee', 'object')); - return Boolean(module && isReactModuleName(module)); + return isReactBuiltinCall(path, 'forwardRef'); }