Skip to content

feat(resolve): resolve Object.keys() in PropType.oneOf() #211

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

Merged
merged 2 commits into from
Sep 5, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/utils/__tests__/getPropType-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
],
});
});

Expand Down
149 changes: 149 additions & 0 deletions src/utils/__tests__/resolveObjectKeysToArray-test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
16 changes: 11 additions & 5 deletions src/utils/getPropType.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down
105 changes: 105 additions & 0 deletions src/utils/resolveObjectKeysToArray.js
Original file line number Diff line number Diff line change
@@ -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<string> {
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;
}