Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support destructuring and aliased imports in react builtin call detection #385

Merged
merged 2 commits into from Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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');
}