Skip to content

Commit 4305f9a

Browse files
tongbindanez
authored andcommitted
Support destructuring and aliased imports in react builtin call detection (#385)
* fix: #380 * feat: Support destructuring and aliased imports in react builtin call detection
1 parent 387b331 commit 4305f9a

9 files changed

+359
-64
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import { parse } from '../../../tests/utils';
10+
import isReactCloneElementCall from '../isReactCloneElementCall';
11+
12+
describe('isReactCloneElementCall', () => {
13+
function parsePath(src) {
14+
const root = parse(src);
15+
return root.get('body', root.node.body.length - 1, 'expression');
16+
}
17+
18+
describe('built in React.createClass', () => {
19+
it('accepts cloneElement called on React', () => {
20+
const def = parsePath(`
21+
var React = require("React");
22+
React.cloneElement({});
23+
`);
24+
expect(isReactCloneElementCall(def)).toBe(true);
25+
});
26+
27+
it('accepts cloneElement called on aliased React', () => {
28+
const def = parsePath(`
29+
var other = require("React");
30+
other.cloneElement({});
31+
`);
32+
expect(isReactCloneElementCall(def)).toBe(true);
33+
});
34+
35+
it('ignores other React calls', () => {
36+
const def = parsePath(`
37+
var React = require("React");
38+
React.isValidElement({});
39+
`);
40+
expect(isReactCloneElementCall(def)).toBe(false);
41+
});
42+
43+
it('ignores non React calls to cloneElement', () => {
44+
const def = parsePath(`
45+
var React = require("bob");
46+
React.cloneElement({});
47+
`);
48+
expect(isReactCloneElementCall(def)).toBe(false);
49+
});
50+
51+
it('accepts cloneElement called on destructed value', () => {
52+
const def = parsePath(`
53+
var { cloneElement } = require("react");
54+
cloneElement({});
55+
`);
56+
expect(isReactCloneElementCall(def)).toBe(true);
57+
});
58+
59+
it('accepts cloneElement called on destructed aliased value', () => {
60+
const def = parsePath(`
61+
var { cloneElement: foo } = require("react");
62+
foo({});
63+
`);
64+
expect(isReactCloneElementCall(def)).toBe(true);
65+
});
66+
67+
it('accepts cloneElement called on imported value', () => {
68+
const def = parsePath(`
69+
import { cloneElement } from "react";
70+
cloneElement({});
71+
`);
72+
expect(isReactCloneElementCall(def)).toBe(true);
73+
});
74+
75+
it('accepts cloneElement called on imported aliased value', () => {
76+
const def = parsePath(`
77+
import { cloneElement as foo } from "react";
78+
foo({});
79+
`);
80+
expect(isReactCloneElementCall(def)).toBe(true);
81+
});
82+
});
83+
});

src/utils/__tests__/isReactCreateClassCall-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,38 @@ describe('isReactCreateClassCall', () => {
5353
`);
5454
expect(isReactCreateClassCall(def)).toBe(false);
5555
});
56+
57+
it('accepts createClass called on destructed value', () => {
58+
const def = parsePath(`
59+
var { createClass } = require("react");
60+
createClass({});
61+
`);
62+
expect(isReactCreateClassCall(def)).toBe(true);
63+
});
64+
65+
it('accepts createClass called on destructed aliased value', () => {
66+
const def = parsePath(`
67+
var { createClass: foo } = require("react");
68+
foo({});
69+
`);
70+
expect(isReactCreateClassCall(def)).toBe(true);
71+
});
72+
73+
it('accepts createClass called on imported value', () => {
74+
const def = parsePath(`
75+
import { createClass } from "react";
76+
createClass({});
77+
`);
78+
expect(isReactCreateClassCall(def)).toBe(true);
79+
});
80+
81+
it('accepts createClass called on imported aliased value', () => {
82+
const def = parsePath(`
83+
import { createClass as foo } from "react";
84+
foo({});
85+
`);
86+
expect(isReactCreateClassCall(def)).toBe(true);
87+
});
5688
});
5789

5890
describe('modular in create-react-class', () => {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import { parse } from '../../../tests/utils';
10+
import isReactCreateElementCall from '../isReactCreateElementCall';
11+
12+
describe('isReactCreateElementCall', () => {
13+
function parsePath(src) {
14+
const root = parse(src);
15+
return root.get('body', root.node.body.length - 1, 'expression');
16+
}
17+
18+
describe('built in React.createElement', () => {
19+
it('accepts createElement called on React', () => {
20+
const def = parsePath(`
21+
var React = require("React");
22+
React.createElement({
23+
render() {}
24+
});
25+
`);
26+
expect(isReactCreateElementCall(def)).toBe(true);
27+
});
28+
29+
it('accepts createElement called on aliased React', () => {
30+
const def = parsePath(`
31+
var other = require("React");
32+
other.createElement({
33+
render() {}
34+
});
35+
`);
36+
expect(isReactCreateElementCall(def)).toBe(true);
37+
});
38+
39+
it('ignores other React calls', () => {
40+
const def = parsePath(`
41+
var React = require("React");
42+
React.isValidElement({});
43+
`);
44+
expect(isReactCreateElementCall(def)).toBe(false);
45+
});
46+
47+
it('ignores non React calls to createElement', () => {
48+
const def = parsePath(`
49+
var React = require("bob");
50+
React.createElement({
51+
render() {}
52+
});
53+
`);
54+
expect(isReactCreateElementCall(def)).toBe(false);
55+
});
56+
57+
it('accepts createElement called on destructed value', () => {
58+
const def = parsePath(`
59+
var { createElement } = require("react");
60+
createElement({});
61+
`);
62+
expect(isReactCreateElementCall(def)).toBe(true);
63+
});
64+
65+
it('accepts createElement called on destructed aliased value', () => {
66+
const def = parsePath(`
67+
var { createElement: foo } = require("react");
68+
foo({});
69+
`);
70+
expect(isReactCreateElementCall(def)).toBe(true);
71+
});
72+
73+
it('accepts createElement called on imported value', () => {
74+
const def = parsePath(`
75+
import { createElement } from "react";
76+
createElement({});
77+
`);
78+
expect(isReactCreateElementCall(def)).toBe(true);
79+
});
80+
81+
it('accepts createElement called on imported aliased value', () => {
82+
const def = parsePath(`
83+
import { createElement as foo } from "react";
84+
foo({});
85+
`);
86+
expect(isReactCreateElementCall(def)).toBe(true);
87+
});
88+
});
89+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import { parse } from '../../../tests/utils';
10+
import isReactForwardRefCall from '../isReactForwardRefCall';
11+
12+
describe('isReactForwardRefCall', () => {
13+
function parsePath(src) {
14+
const root = parse(src);
15+
return root.get('body', root.node.body.length - 1, 'expression');
16+
}
17+
18+
describe('built in React.forwardRef', () => {
19+
it('accepts forwardRef called on React', () => {
20+
const def = parsePath(`
21+
var React = require("React");
22+
React.forwardRef({
23+
render() {}
24+
});
25+
`);
26+
expect(isReactForwardRefCall(def)).toBe(true);
27+
});
28+
29+
it('accepts forwardRef called on aliased React', () => {
30+
const def = parsePath(`
31+
var other = require("React");
32+
other.forwardRef({
33+
render() {}
34+
});
35+
`);
36+
expect(isReactForwardRefCall(def)).toBe(true);
37+
});
38+
39+
it('ignores other React calls', () => {
40+
const def = parsePath(`
41+
var React = require("React");
42+
React.isValidElement({});
43+
`);
44+
expect(isReactForwardRefCall(def)).toBe(false);
45+
});
46+
47+
it('ignores non React calls to forwardRef', () => {
48+
const def = parsePath(`
49+
var React = require("bob");
50+
React.forwardRef({
51+
render() {}
52+
});
53+
`);
54+
expect(isReactForwardRefCall(def)).toBe(false);
55+
});
56+
57+
it('accepts forwardRef called on destructed value', () => {
58+
const def = parsePath(`
59+
var { forwardRef } = require("react");
60+
forwardRef({});
61+
`);
62+
expect(isReactForwardRefCall(def)).toBe(true);
63+
});
64+
65+
it('accepts forwardRef called on destructed aliased value', () => {
66+
const def = parsePath(`
67+
var { forwardRef: foo } = require("react");
68+
foo({});
69+
`);
70+
expect(isReactForwardRefCall(def)).toBe(true);
71+
});
72+
73+
it('accepts forwardRef called on imported value', () => {
74+
const def = parsePath(`
75+
import { forwardRef } from "react";
76+
forwardRef({});
77+
`);
78+
expect(isReactForwardRefCall(def)).toBe(true);
79+
});
80+
81+
it('accepts forwardRef called on imported aliased value', () => {
82+
const def = parsePath(`
83+
import { forwardRef as foo } from "react";
84+
foo({});
85+
`);
86+
expect(isReactForwardRefCall(def)).toBe(true);
87+
});
88+
});
89+
});

src/utils/isReactBuiltinCall.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import types from 'ast-types';
11+
import isReactModuleName from './isReactModuleName';
12+
import match from './match';
13+
import resolveToModule from './resolveToModule';
14+
import resolveToValue from './resolveToValue';
15+
16+
const { namedTypes: t } = types;
17+
18+
/**
19+
* Returns true if the expression is a function call of the form
20+
* `React.foo(...)`.
21+
*/
22+
export default function isReactBuiltinCall(
23+
path: NodePath,
24+
name: string,
25+
): boolean {
26+
if (t.ExpressionStatement.check(path.node)) {
27+
path = path.get('expression');
28+
}
29+
30+
if (match(path.node, { callee: { property: { name } } })) {
31+
const module = resolveToModule(path.get('callee', 'object'));
32+
return Boolean(module && isReactModuleName(module));
33+
}
34+
35+
if (t.CallExpression.check(path.node)) {
36+
const value = resolveToValue(path.get('callee'));
37+
if (value === path.get('callee')) return false;
38+
39+
if (
40+
// `require('react').createElement`
41+
(t.MemberExpression.check(value.node) &&
42+
t.Identifier.check(value.get('property').node) &&
43+
value.get('property').node.name === name) ||
44+
// `import { createElement } from 'react'`
45+
(t.ImportDeclaration.check(value.node) &&
46+
value.node.specifiers.some(
47+
specifier => specifier.imported && specifier.imported.name === name,
48+
))
49+
) {
50+
const module = resolveToModule(value);
51+
return Boolean(module && isReactModuleName(module));
52+
}
53+
}
54+
55+
return false;
56+
}

src/utils/isReactCloneElementCall.js

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,12 @@
77
* @flow
88
*/
99

10-
import types from 'ast-types';
11-
import isReactModuleName from './isReactModuleName';
12-
import match from './match';
13-
import resolveToModule from './resolveToModule';
14-
15-
const { namedTypes: t } = types;
10+
import isReactBuiltinCall from './isReactBuiltinCall';
1611

1712
/**
1813
* Returns true if the expression is a function call of the form
19-
* `React.createElement(...)`.
14+
* `React.cloneElement(...)`.
2015
*/
2116
export default function isReactCloneElementCall(path: NodePath): boolean {
22-
if (t.ExpressionStatement.check(path.node)) {
23-
path = path.get('expression');
24-
}
25-
26-
if (!match(path.node, { callee: { property: { name: 'cloneElement' } } })) {
27-
return false;
28-
}
29-
const module = resolveToModule(path.get('callee', 'object'));
30-
return Boolean(module && isReactModuleName(module));
17+
return isReactBuiltinCall(path, 'cloneElement');
3118
}

0 commit comments

Comments
 (0)