diff --git a/src/utils/__tests__/getPropType-test.js b/src/utils/__tests__/getPropType-test.js index b4745eead01..c503d94a85c 100644 --- a/src/utils/__tests__/getPropType-test.js +++ b/src/utils/__tests__/getPropType-test.js @@ -236,8 +236,10 @@ describe('getPropType', () => { expect(getPropType(propTypeExpression)).toEqual({ name: 'enum', - value: 'Object.keys(TYPES)', - computed: true, + value: [ + {value: '"FOO"', computed: false}, + {value: '"BAR"', computed: false}, + ], }); }); diff --git a/src/utils/__tests__/resolveObjectKeysToArray-test.js b/src/utils/__tests__/resolveObjectKeysToArray-test.js new file mode 100644 index 00000000000..c0e853b959c --- /dev/null +++ b/src/utils/__tests__/resolveObjectKeysToArray-test.js @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +/* eslint-env jest */ + +import recast from 'recast'; + +const builders = recast.types.builders; +import resolveObjectKeysToArray from '../resolveObjectKeysToArray'; +import * as utils from '../../../tests/utils'; + +describe('resolveObjectKeysToArray', () => { + + function parse(src) { + var root = utils.parse(src); + return root.get('body', root.node.body.length - 1, 'expression'); + } + + it('resolves Object.keys with identifiers', () => { + var path = parse([ + 'var foo = { bar: 1, foo: 2 };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('bar'), builders.literal('foo')] + ) + ); + }); + + it('resolves Object.keys with literals', () => { + var path = parse([ + 'var foo = { "bar": 1, 5: 2 };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('bar'), builders.literal('5')] + ) + ); + }); + + it('resolves Object.keys with literals as computed key', () => { + var path = parse([ + 'var foo = { ["bar"]: 1, [5]: 2};', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('bar'), builders.literal('5')] + ) + ); + }); + + it('resolves Object.keys when using resolvable spread', () => { + var path = parse([ + 'var bar = { doo: 4 }', + 'var foo = { boo: 1, foo: 2, ...bar };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('boo'), builders.literal('foo'), builders.literal('doo')] + ) + ); + }); + + it('resolves Object.keys when using getters', () => { + var path = parse([ + 'var foo = { boo: 1, foo: 2, get bar() {} };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('boo'), builders.literal('foo'), builders.literal('bar')] + ) + ); + }); + + it('resolves Object.keys when using setters', () => { + var path = parse([ + 'var foo = { boo: 1, foo: 2, set bar(e) {} };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('boo'), builders.literal('foo'), builders.literal('bar')] + ) + ); + }); + + it('resolves Object.keys but ignores duplicates', () => { + var path = parse([ + 'var bar = { doo: 4, doo: 5 }', + 'var foo = { boo: 1, foo: 2, doo: 1, ...bar };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('boo'), builders.literal('foo'), builders.literal('doo')] + ) + ); + }); + + it('resolves Object.keys but ignores duplicates with getter and setter', () => { + var path = parse([ + 'var foo = { get x() {}, set x(a) {} };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path).node).toEqualASTNode( + builders.arrayExpression( + [builders.literal('x')] + ) + ); + }); + + it('does not resolve Object.keys when using unresolvable spread', () => { + var path = parse([ + 'var foo = { bar: 1, foo: 2, ...bar };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path)).toBeNull(); + }); + + it('does not resolve Object.keys when using computed keys', () => { + var path = parse([ + 'var foo = { [bar]: 1, foo: 2 };', + 'Object.keys(foo);', + ].join('\n')); + + expect(resolveObjectKeysToArray(path)).toBeNull(); + }); +}); diff --git a/src/utils/getPropType.js b/src/utils/getPropType.js index f4115f85de2..d6e9e29b43c 100644 --- a/src/utils/getPropType.js +++ b/src/utils/getPropType.js @@ -20,6 +20,7 @@ import isRequiredPropType from '../utils/isRequiredPropType'; import printValue from './printValue'; import recast from 'recast'; import resolveToValue from './resolveToValue'; +import resolveObjectKeysToArray from './resolveObjectKeysToArray'; var {types: {namedTypes: types}} = recast; @@ -54,12 +55,17 @@ function getEnumValues(path) { } function getPropTypeOneOf(argumentPath) { - var type: PropTypeDescriptor = {name: 'enum'}; - const value = resolveToValue(argumentPath); + const type: PropTypeDescriptor = {name: 'enum'}; + let value = resolveToValue(argumentPath); if (!types.ArrayExpression.check(value.node)) { - // could not easily resolve to an Array, let's print the original value - type.computed = true; - type.value = printValue(argumentPath); + value = resolveObjectKeysToArray(value); + if (value) { + type.value = getEnumValues(value); + } else { + // could not easily resolve to an Array, let's print the original value + type.computed = true; + type.value = printValue(argumentPath); + } } else { type.value = getEnumValues(value); } diff --git a/src/utils/resolveObjectKeysToArray.js b/src/utils/resolveObjectKeysToArray.js new file mode 100644 index 00000000000..a9527a648c2 --- /dev/null +++ b/src/utils/resolveObjectKeysToArray.js @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2017, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * + */ + +import recast from 'recast'; +import resolveToValue from './resolveToValue'; + +var { + types: { + ASTNode, + NodePath, + builders, + namedTypes: types, + }, +} = recast; + +function isObjectKeysCall(node: ASTNode): bool { + return types.CallExpression.check(node) && + node.arguments.length === 1 && + types.MemberExpression.check(node.callee) && + types.Identifier.check(node.callee.object) && + node.callee.object.name === 'Object' && + types.Identifier.check(node.callee.property) && + node.callee.property.name === 'keys'; +} + +function resolveObjectExpressionToNameArray(objectExpression: NodePath): ?Array { + if ( + types.ObjectExpression.check(objectExpression.value) && + objectExpression.value.properties.every( + prop => + types.Property.check(prop) && + ( + (types.Identifier.check(prop.key) && !prop.computed) || + types.Literal.check(prop.key) + ) || + types.SpreadProperty.check(prop) + ) + ) { + let values = []; + let error = false; + objectExpression.get('properties').each(propPath => { + if (error) return; + const prop = propPath.value; + + if (types.Property.check(prop)) { + // Key is either Identifier or Literal + const name = prop.key.name || prop.key.value; + + values.push(name); + } else if (types.SpreadProperty.check(prop)) { + const spreadObject = resolveToValue(propPath.get('argument')); + const spreadValues = resolveObjectExpressionToNameArray(spreadObject); + if (!spreadValues) { + error = true; + return; + } + values = [...values, ...spreadValues]; + } + + }); + + if (!error) { + return values; + } + } + + return null; +} + +/** + * Returns an ArrayExpression which contains all the keys resolved from an object + * + * Ignores setters in objects + * + * Returns null in case of + * unresolvable spreads + * computed identifier keys + */ +export default function resolveObjectKeysToArray(path: NodePath): ?NodePath { + var node = path.node; + + if (isObjectKeysCall(node)) { + const objectExpression = resolveToValue(path.get('arguments').get(0)); + const values = resolveObjectExpressionToNameArray(objectExpression); + + if (values) { + const nodes = values + .filter((value, index, array) => array.indexOf(value) === index) + .map(value => builders.literal(value)) + + return new NodePath(builders.arrayExpression(nodes)); + } + } + + return null; +}