Skip to content

Commit

Permalink
Support destructuring and aliased imports in react builtin call detec…
Browse files Browse the repository at this point in the history
…tion (#385)

* fix: #380

* feat: Support destructuring and aliased imports in react builtin call detection
  • Loading branch information
tongbin authored and danez committed Oct 24, 2019
1 parent 387b331 commit 4305f9a
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 64 deletions.
83 changes: 83 additions & 0 deletions 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);
});
});
});
32 changes: 32 additions & 0 deletions src/utils/__tests__/isReactCreateClassCall-test.js
Expand Up @@ -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', () => {
Expand Down
89 changes: 89 additions & 0 deletions 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);
});
});
});
89 changes: 89 additions & 0 deletions 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);
});
});
});
56 changes: 56 additions & 0 deletions 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;
}
19 changes: 3 additions & 16 deletions src/utils/isReactCloneElementCall.js
Expand Up @@ -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');
}

0 comments on commit 4305f9a

Please sign in to comment.