Skip to content

Commit 55bbefe

Browse files
authored
feat(resolve): resolve Object.keys() in PropType.oneOf() (#211)
* feat(resolve): resolve Object.keys() in PropType.oneOf() Creates a new util to extract keys from ObjectExpressions. * Use same logic as JS itself and do not ignore setters This does not ignore setters, but deduplicate the resolved entries and makes them unique.
1 parent 48fe56e commit 55bbefe

File tree

4 files changed

+269
-7
lines changed

4 files changed

+269
-7
lines changed

src/utils/__tests__/getPropType-test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,10 @@ describe('getPropType', () => {
236236

237237
expect(getPropType(propTypeExpression)).toEqual({
238238
name: 'enum',
239-
value: 'Object.keys(TYPES)',
240-
computed: true,
239+
value: [
240+
{value: '"FOO"', computed: false},
241+
{value: '"BAR"', computed: false},
242+
],
241243
});
242244
});
243245

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
*/
10+
11+
/* eslint-env jest */
12+
13+
import recast from 'recast';
14+
15+
const builders = recast.types.builders;
16+
import resolveObjectKeysToArray from '../resolveObjectKeysToArray';
17+
import * as utils from '../../../tests/utils';
18+
19+
describe('resolveObjectKeysToArray', () => {
20+
21+
function parse(src) {
22+
var root = utils.parse(src);
23+
return root.get('body', root.node.body.length - 1, 'expression');
24+
}
25+
26+
it('resolves Object.keys with identifiers', () => {
27+
var path = parse([
28+
'var foo = { bar: 1, foo: 2 };',
29+
'Object.keys(foo);',
30+
].join('\n'));
31+
32+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
33+
builders.arrayExpression(
34+
[builders.literal('bar'), builders.literal('foo')]
35+
)
36+
);
37+
});
38+
39+
it('resolves Object.keys with literals', () => {
40+
var path = parse([
41+
'var foo = { "bar": 1, 5: 2 };',
42+
'Object.keys(foo);',
43+
].join('\n'));
44+
45+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
46+
builders.arrayExpression(
47+
[builders.literal('bar'), builders.literal('5')]
48+
)
49+
);
50+
});
51+
52+
it('resolves Object.keys with literals as computed key', () => {
53+
var path = parse([
54+
'var foo = { ["bar"]: 1, [5]: 2};',
55+
'Object.keys(foo);',
56+
].join('\n'));
57+
58+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
59+
builders.arrayExpression(
60+
[builders.literal('bar'), builders.literal('5')]
61+
)
62+
);
63+
});
64+
65+
it('resolves Object.keys when using resolvable spread', () => {
66+
var path = parse([
67+
'var bar = { doo: 4 }',
68+
'var foo = { boo: 1, foo: 2, ...bar };',
69+
'Object.keys(foo);',
70+
].join('\n'));
71+
72+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
73+
builders.arrayExpression(
74+
[builders.literal('boo'), builders.literal('foo'), builders.literal('doo')]
75+
)
76+
);
77+
});
78+
79+
it('resolves Object.keys when using getters', () => {
80+
var path = parse([
81+
'var foo = { boo: 1, foo: 2, get bar() {} };',
82+
'Object.keys(foo);',
83+
].join('\n'));
84+
85+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
86+
builders.arrayExpression(
87+
[builders.literal('boo'), builders.literal('foo'), builders.literal('bar')]
88+
)
89+
);
90+
});
91+
92+
it('resolves Object.keys when using setters', () => {
93+
var path = parse([
94+
'var foo = { boo: 1, foo: 2, set bar(e) {} };',
95+
'Object.keys(foo);',
96+
].join('\n'));
97+
98+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
99+
builders.arrayExpression(
100+
[builders.literal('boo'), builders.literal('foo'), builders.literal('bar')]
101+
)
102+
);
103+
});
104+
105+
it('resolves Object.keys but ignores duplicates', () => {
106+
var path = parse([
107+
'var bar = { doo: 4, doo: 5 }',
108+
'var foo = { boo: 1, foo: 2, doo: 1, ...bar };',
109+
'Object.keys(foo);',
110+
].join('\n'));
111+
112+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
113+
builders.arrayExpression(
114+
[builders.literal('boo'), builders.literal('foo'), builders.literal('doo')]
115+
)
116+
);
117+
});
118+
119+
it('resolves Object.keys but ignores duplicates with getter and setter', () => {
120+
var path = parse([
121+
'var foo = { get x() {}, set x(a) {} };',
122+
'Object.keys(foo);',
123+
].join('\n'));
124+
125+
expect(resolveObjectKeysToArray(path).node).toEqualASTNode(
126+
builders.arrayExpression(
127+
[builders.literal('x')]
128+
)
129+
);
130+
});
131+
132+
it('does not resolve Object.keys when using unresolvable spread', () => {
133+
var path = parse([
134+
'var foo = { bar: 1, foo: 2, ...bar };',
135+
'Object.keys(foo);',
136+
].join('\n'));
137+
138+
expect(resolveObjectKeysToArray(path)).toBeNull();
139+
});
140+
141+
it('does not resolve Object.keys when using computed keys', () => {
142+
var path = parse([
143+
'var foo = { [bar]: 1, foo: 2 };',
144+
'Object.keys(foo);',
145+
].join('\n'));
146+
147+
expect(resolveObjectKeysToArray(path)).toBeNull();
148+
});
149+
});

src/utils/getPropType.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import isRequiredPropType from '../utils/isRequiredPropType';
2020
import printValue from './printValue';
2121
import recast from 'recast';
2222
import resolveToValue from './resolveToValue';
23+
import resolveObjectKeysToArray from './resolveObjectKeysToArray';
2324

2425
var {types: {namedTypes: types}} = recast;
2526

@@ -54,12 +55,17 @@ function getEnumValues(path) {
5455
}
5556

5657
function getPropTypeOneOf(argumentPath) {
57-
var type: PropTypeDescriptor = {name: 'enum'};
58-
const value = resolveToValue(argumentPath);
58+
const type: PropTypeDescriptor = {name: 'enum'};
59+
let value = resolveToValue(argumentPath);
5960
if (!types.ArrayExpression.check(value.node)) {
60-
// could not easily resolve to an Array, let's print the original value
61-
type.computed = true;
62-
type.value = printValue(argumentPath);
61+
value = resolveObjectKeysToArray(value);
62+
if (value) {
63+
type.value = getEnumValues(value);
64+
} else {
65+
// could not easily resolve to an Array, let's print the original value
66+
type.computed = true;
67+
type.value = printValue(argumentPath);
68+
}
6369
} else {
6470
type.value = getEnumValues(value);
6571
}

src/utils/resolveObjectKeysToArray.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2017, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow
10+
*
11+
*/
12+
13+
import recast from 'recast';
14+
import resolveToValue from './resolveToValue';
15+
16+
var {
17+
types: {
18+
ASTNode,
19+
NodePath,
20+
builders,
21+
namedTypes: types,
22+
},
23+
} = recast;
24+
25+
function isObjectKeysCall(node: ASTNode): bool {
26+
return types.CallExpression.check(node) &&
27+
node.arguments.length === 1 &&
28+
types.MemberExpression.check(node.callee) &&
29+
types.Identifier.check(node.callee.object) &&
30+
node.callee.object.name === 'Object' &&
31+
types.Identifier.check(node.callee.property) &&
32+
node.callee.property.name === 'keys';
33+
}
34+
35+
function resolveObjectExpressionToNameArray(objectExpression: NodePath): ?Array<string> {
36+
if (
37+
types.ObjectExpression.check(objectExpression.value) &&
38+
objectExpression.value.properties.every(
39+
prop =>
40+
types.Property.check(prop) &&
41+
(
42+
(types.Identifier.check(prop.key) && !prop.computed) ||
43+
types.Literal.check(prop.key)
44+
) ||
45+
types.SpreadProperty.check(prop)
46+
)
47+
) {
48+
let values = [];
49+
let error = false;
50+
objectExpression.get('properties').each(propPath => {
51+
if (error) return;
52+
const prop = propPath.value;
53+
54+
if (types.Property.check(prop)) {
55+
// Key is either Identifier or Literal
56+
const name = prop.key.name || prop.key.value;
57+
58+
values.push(name);
59+
} else if (types.SpreadProperty.check(prop)) {
60+
const spreadObject = resolveToValue(propPath.get('argument'));
61+
const spreadValues = resolveObjectExpressionToNameArray(spreadObject);
62+
if (!spreadValues) {
63+
error = true;
64+
return;
65+
}
66+
values = [...values, ...spreadValues];
67+
}
68+
69+
});
70+
71+
if (!error) {
72+
return values;
73+
}
74+
}
75+
76+
return null;
77+
}
78+
79+
/**
80+
* Returns an ArrayExpression which contains all the keys resolved from an object
81+
*
82+
* Ignores setters in objects
83+
*
84+
* Returns null in case of
85+
* unresolvable spreads
86+
* computed identifier keys
87+
*/
88+
export default function resolveObjectKeysToArray(path: NodePath): ?NodePath {
89+
var node = path.node;
90+
91+
if (isObjectKeysCall(node)) {
92+
const objectExpression = resolveToValue(path.get('arguments').get(0));
93+
const values = resolveObjectExpressionToNameArray(objectExpression);
94+
95+
if (values) {
96+
const nodes = values
97+
.filter((value, index, array) => array.indexOf(value) === index)
98+
.map(value => builders.literal(value))
99+
100+
return new NodePath(builders.arrayExpression(nodes));
101+
}
102+
}
103+
104+
return null;
105+
}

0 commit comments

Comments
 (0)