Skip to content

Commit c791667

Browse files
gatsbimanticodanez
authored andcommitted
feat(resolve): resolve Object.values() in PropType.oneOf() (#318)
1 parent 52ae1f4 commit c791667

File tree

5 files changed

+338
-6
lines changed

5 files changed

+338
-6
lines changed

src/utils/__tests__/__snapshots__/getPropType-test.js.snap

+20-4
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,15 @@ Object {
134134
}
135135
`;
136136

137-
exports[`getPropType resolve identifier to their values does not resolve computed values 1`] = `
137+
exports[`getPropType resolve identifier to their values does not resolve external values 1`] = `
138+
Object {
139+
"computed": true,
140+
"name": "enum",
141+
"value": "TYPES",
142+
}
143+
`;
144+
145+
exports[`getPropType resolve identifier to their values does resolve object keys values 1`] = `
138146
Object {
139147
"name": "enum",
140148
"value": Array [
@@ -150,11 +158,19 @@ Object {
150158
}
151159
`;
152160

153-
exports[`getPropType resolve identifier to their values does not resolve external values 1`] = `
161+
exports[`getPropType resolve identifier to their values does resolve object values 1`] = `
154162
Object {
155-
"computed": true,
156163
"name": "enum",
157-
"value": "TYPES",
164+
"value": Array [
165+
Object {
166+
"computed": false,
167+
"value": "\\"bar\\"",
168+
},
169+
Object {
170+
"computed": false,
171+
"value": "\\"foo\\"",
172+
},
173+
],
158174
}
159175
`;
160176

src/utils/__tests__/getPropType-test.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ describe('getPropType', () => {
222222
expect(getPropType(propTypeExpression)).toMatchSnapshot();
223223
});
224224

225-
it('does not resolve computed values', () => {
225+
it('does resolve object keys values', () => {
226226
const propTypeExpression = statement(`
227227
PropTypes.oneOf(Object.keys(TYPES));
228228
var TYPES = { FOO: "foo", BAR: "bar" };
@@ -231,6 +231,15 @@ describe('getPropType', () => {
231231
expect(getPropType(propTypeExpression)).toMatchSnapshot();
232232
});
233233

234+
it('does resolve object values', () => {
235+
const propTypeExpression = statement(`
236+
PropTypes.oneOf(Object.values(TYPES));
237+
var TYPES = { FOO: "foo", BAR: "bar" };
238+
`).get('expression');
239+
240+
expect(getPropType(propTypeExpression)).toMatchSnapshot();
241+
});
242+
234243
it('does not resolve external values', () => {
235244
const propTypeExpression = statement(`
236245
PropTypes.oneOf(TYPES);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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 resolveObjectValuesToArray from '../resolveObjectValuesToArray';
17+
import * as utils from '../../../tests/utils';
18+
19+
describe('resolveObjectValuesToArray', () => {
20+
function parse(src) {
21+
const root = utils.parse(src);
22+
return root.get('body', root.node.body.length - 1, 'expression');
23+
}
24+
25+
it('resolves Object.values with strings', () => {
26+
const path = parse(
27+
['var foo = { 1: "bar", 2: "foo" };', 'Object.values(foo);'].join('\n'),
28+
);
29+
30+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
31+
builders.arrayExpression([
32+
builders.literal('bar'),
33+
builders.literal('foo'),
34+
]),
35+
);
36+
});
37+
38+
it('resolves Object.values with numbers', () => {
39+
const path = parse(
40+
['var foo = { 1: 0, 2: 5 };', 'Object.values(foo);'].join('\n'),
41+
);
42+
43+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
44+
builders.arrayExpression([builders.literal(0), builders.literal(5)]),
45+
);
46+
});
47+
48+
it('resolves Object.values with undefined or null', () => {
49+
const path = parse(
50+
['var foo = { 1: null, 2: undefined };', 'Object.values(foo);'].join(
51+
'\n',
52+
),
53+
);
54+
55+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
56+
builders.arrayExpression([
57+
builders.literal(null),
58+
builders.literal(null),
59+
]),
60+
);
61+
});
62+
63+
it('resolves Object.values with literals as computed key', () => {
64+
const path = parse(
65+
['var foo = { ["bar"]: 1, [5]: 2};', 'Object.values(foo);'].join('\n'),
66+
);
67+
68+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
69+
builders.arrayExpression([builders.literal(2), builders.literal(1)]),
70+
);
71+
});
72+
73+
it('resolves Object.values when using resolvable spread', () => {
74+
const path = parse(
75+
[
76+
'var bar = { doo: 4 }',
77+
'var foo = { boo: 1, foo: 2, ...bar };',
78+
'Object.values(foo);',
79+
].join('\n'),
80+
);
81+
82+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
83+
builders.arrayExpression([
84+
builders.literal(1),
85+
builders.literal(4),
86+
builders.literal(2),
87+
]),
88+
);
89+
});
90+
91+
it('resolves Object.values when using getters', () => {
92+
const path = parse(
93+
[
94+
'var foo = { boo: 1, foo: 2, get bar() {} };',
95+
'Object.values(foo);',
96+
].join('\n'),
97+
);
98+
99+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
100+
builders.arrayExpression([builders.literal(1), builders.literal(2)]),
101+
);
102+
});
103+
104+
it('resolves Object.values when using setters', () => {
105+
const path = parse(
106+
[
107+
'var foo = { boo: 1, foo: 2, set bar(e) {} };',
108+
'Object.values(foo);',
109+
].join('\n'),
110+
);
111+
112+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
113+
builders.arrayExpression([builders.literal(1), builders.literal(2)]),
114+
);
115+
});
116+
117+
it('resolves Object.values but ignores duplicates', () => {
118+
const path = parse(
119+
[
120+
'var bar = { doo: 4, doo: 5 }',
121+
'var foo = { boo: 1, foo: 2, doo: 1, ...bar };',
122+
'Object.values(foo);',
123+
].join('\n'),
124+
);
125+
126+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
127+
builders.arrayExpression([
128+
builders.literal(1),
129+
builders.literal(5),
130+
builders.literal(2),
131+
]),
132+
);
133+
});
134+
135+
it('resolves Object.values but ignores duplicates with getter and setter', () => {
136+
const path = parse(
137+
['var foo = { get x() {}, set x(a) {} };', 'Object.values(foo);'].join(
138+
'\n',
139+
),
140+
);
141+
142+
expect(resolveObjectValuesToArray(path)).toEqualASTNode(
143+
builders.arrayExpression([]),
144+
);
145+
});
146+
147+
it('does not resolve Object.values when using unresolvable spread', () => {
148+
const path = parse(
149+
['var foo = { bar: 1, foo: 2, ...bar };', 'Object.values(foo);'].join(
150+
'\n',
151+
),
152+
);
153+
154+
expect(resolveObjectValuesToArray(path)).toBeNull();
155+
});
156+
});

src/utils/getPropType.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import printValue from './printValue';
2020
import recast from 'recast';
2121
import resolveToValue from './resolveToValue';
2222
import resolveObjectKeysToArray from './resolveObjectKeysToArray';
23+
import resolveObjectValuesToArray from './resolveObjectValuesToArray';
2324
import type { PropTypeDescriptor, PropDescriptor } from '../types';
2425

2526
const {
@@ -60,7 +61,8 @@ function getPropTypeOneOf(argumentPath) {
6061
const type: PropTypeDescriptor = { name: 'enum' };
6162
let value = resolveToValue(argumentPath);
6263
if (!types.ArrayExpression.check(value.node)) {
63-
value = resolveObjectKeysToArray(value);
64+
value =
65+
resolveObjectKeysToArray(value) || resolveObjectValuesToArray(value);
6466
if (value) {
6567
type.value = getEnumValues(value);
6668
} else {
+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
type ObjectPropMap = {
17+
properties: Array<string>,
18+
values: Object,
19+
};
20+
21+
const {
22+
types: { ASTNode, NodePath, builders, namedTypes: types },
23+
} = recast;
24+
25+
function isObjectValuesCall(node: ASTNode): boolean {
26+
return (
27+
types.CallExpression.check(node) &&
28+
node.arguments.length === 1 &&
29+
types.MemberExpression.check(node.callee) &&
30+
types.Identifier.check(node.callee.object) &&
31+
node.callee.object.name === 'Object' &&
32+
types.Identifier.check(node.callee.property) &&
33+
node.callee.property.name === 'values'
34+
);
35+
}
36+
37+
function isWhitelistedObjectProperty(prop) {
38+
return (
39+
(types.Property.check(prop) &&
40+
((types.Identifier.check(prop.key) && !prop.computed) ||
41+
types.Literal.check(prop.key))) ||
42+
types.SpreadElement.check(prop)
43+
);
44+
}
45+
46+
function isWhiteListedObjectTypeProperty(prop) {
47+
return (
48+
types.ObjectTypeProperty.check(prop) ||
49+
types.ObjectTypeSpreadProperty.check(prop)
50+
);
51+
}
52+
53+
// Resolves an ObjectExpression or an ObjectTypeAnnotation
54+
export function resolveObjectToPropMap(
55+
object: NodePath,
56+
raw: boolean = false,
57+
): ?ObjectPropMap {
58+
if (
59+
(types.ObjectExpression.check(object.value) &&
60+
object.value.properties.every(isWhitelistedObjectProperty)) ||
61+
(types.ObjectTypeAnnotation.check(object.value) &&
62+
object.value.properties.every(isWhiteListedObjectTypeProperty))
63+
) {
64+
const properties = [];
65+
let values = {};
66+
let error = false;
67+
object.get('properties').each(propPath => {
68+
if (error) return;
69+
const prop = propPath.value;
70+
71+
if (prop.kind === 'get' || prop.kind === 'set') return;
72+
73+
if (types.Property.check(prop) || types.ObjectTypeProperty.check(prop)) {
74+
// Key is either Identifier or Literal
75+
const name = prop.key.name || (raw ? prop.key.raw : prop.key.value);
76+
const propValue = propPath.get(name).parentPath.value;
77+
const value =
78+
propValue.value.value ||
79+
(raw ? propValue.value.raw : propValue.value.value);
80+
81+
if (properties.indexOf(name) === -1) {
82+
properties.push(name);
83+
}
84+
values[name] = value;
85+
} else if (
86+
types.SpreadElement.check(prop) ||
87+
types.ObjectTypeSpreadProperty.check(prop)
88+
) {
89+
let spreadObject = resolveToValue(propPath.get('argument'));
90+
if (types.GenericTypeAnnotation.check(spreadObject.value)) {
91+
const typeAlias = resolveToValue(spreadObject.get('id'));
92+
if (types.ObjectTypeAnnotation.check(typeAlias.get('right').value)) {
93+
spreadObject = resolveToValue(typeAlias.get('right'));
94+
}
95+
}
96+
97+
const spreadValues = resolveObjectToPropMap(spreadObject);
98+
if (!spreadValues) {
99+
error = true;
100+
return;
101+
}
102+
spreadValues.properties.forEach(spreadProp => {
103+
if (properties.indexOf(spreadProp) === -1) {
104+
properties.push(spreadProp);
105+
}
106+
});
107+
values = { ...values, ...spreadValues.values };
108+
}
109+
});
110+
111+
if (!error) {
112+
return { properties: properties.sort(), values };
113+
}
114+
}
115+
116+
return null;
117+
}
118+
119+
/**
120+
* Returns an ArrayExpression which contains all the keys resolved from an object
121+
*
122+
* Ignores setters in objects
123+
*
124+
* Returns null in case of
125+
* unresolvable spreads
126+
* computed identifier values
127+
*/
128+
export default function resolveObjectValuesToArray(path: NodePath): ?NodePath {
129+
const node = path.node;
130+
131+
if (isObjectValuesCall(node)) {
132+
const objectExpression = resolveToValue(path.get('arguments').get(0));
133+
const propMap = resolveObjectToPropMap(objectExpression);
134+
135+
if (propMap) {
136+
const nodes = propMap.properties.map(prop => {
137+
const value = propMap.values[prop];
138+
139+
return typeof value === 'undefined'
140+
? builders.literal(null)
141+
: builders.literal(value);
142+
});
143+
144+
return new NodePath(builders.arrayExpression(nodes));
145+
}
146+
}
147+
148+
return null;
149+
}

0 commit comments

Comments
 (0)