Skip to content

Commit 471813a

Browse files
Jack Aldridgedanez
authored andcommitted
Resolve flow $Keys to union when typeParameter is an ObjectTypeAnnotation (#290)
* Add support for ObjectTypeAnnotation as typeParameter of $Keys * Add a test * Refactor to use getPropertyName * Update resolveObjectExpressionToNameArray to resolveObjectToNameArray * Remove correctly failing $Keys test and add another $Keys test * Re-add support for simple identifiers
1 parent b048d58 commit 471813a

File tree

3 files changed

+87
-20
lines changed

3 files changed

+87
-20
lines changed

src/utils/__tests__/getFlowType-test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,46 @@ describe('getFlowType', () => {
536536
});
537537
});
538538

539+
it('resolves $Keys with an ObjectTypeAnnotation typeParameter to union', () => {
540+
const typePath = statement(`
541+
var x: $Keys<{| apple: string, banana: string |}> = 2;
542+
`)
543+
.get('declarations', 0)
544+
.get('id')
545+
.get('typeAnnotation')
546+
.get('typeAnnotation');
547+
548+
expect(getFlowType(typePath)).toEqual({
549+
name: 'union',
550+
elements: [
551+
{ name: 'literal', value: 'apple' },
552+
{ name: 'literal', value: 'banana' },
553+
],
554+
raw: '$Keys<{| apple: string, banana: string |}>',
555+
});
556+
});
557+
558+
it('resolves $Keys with an ObjectTypeAnnotation typeParameter to union with an ObjectTypeSpreadProperty', () => {
559+
const typePath = statement(`
560+
var x: $Keys<{| apple: string, banana: string, ...OtherFruits |}> = 2;
561+
type OtherFruits = { orange: string }
562+
`)
563+
.get('declarations', 0)
564+
.get('id')
565+
.get('typeAnnotation')
566+
.get('typeAnnotation');
567+
568+
expect(getFlowType(typePath)).toEqual({
569+
name: 'union',
570+
elements: [
571+
{ name: 'literal', value: 'apple' },
572+
{ name: 'literal', value: 'banana' },
573+
{ name: 'literal', value: 'orange' },
574+
],
575+
raw: '$Keys<{| apple: string, banana: string, ...OtherFruits |}>',
576+
});
577+
});
578+
539579
it('handles multiple references to one type', () => {
540580
const typePath = statement(`
541581
let action: { a: Action, b: Action };

src/utils/getFlowType.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import printValue from './printValue';
1717
import recast from 'recast';
1818
import getTypeAnnotation from '../utils/getTypeAnnotation';
1919
import resolveToValue from '../utils/resolveToValue';
20-
import { resolveObjectExpressionToNameArray } from '../utils/resolveObjectKeysToArray';
20+
import { resolveObjectToNameArray } from '../utils/resolveObjectKeysToArray';
2121
import type {
2222
FlowTypeDescriptor,
2323
FlowElementsType,
@@ -69,12 +69,16 @@ function handleKeysHelper(path: NodePath): ?FlowElementsType {
6969
let value = path.get('typeParameters', 'params', 0);
7070
if (types.TypeofTypeAnnotation.check(value.node)) {
7171
value = value.get('argument', 'id');
72-
} else {
72+
} else if (!types.ObjectTypeAnnotation.check(value.node)) {
7373
value = value.get('id');
7474
}
7575
const resolvedPath = resolveToValue(value);
76-
if (resolvedPath && types.ObjectExpression.check(resolvedPath.node)) {
77-
const keys = resolveObjectExpressionToNameArray(resolvedPath, true);
76+
if (
77+
resolvedPath &&
78+
(types.ObjectExpression.check(resolvedPath.node) ||
79+
types.ObjectTypeAnnotation.check(resolvedPath.node))
80+
) {
81+
const keys = resolveObjectToNameArray(resolvedPath, true);
7882

7983
if (keys) {
8084
return {

src/utils/resolveObjectKeysToArray.js

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,34 +29,57 @@ function isObjectKeysCall(node: ASTNode): boolean {
2929
);
3030
}
3131

32-
export function resolveObjectExpressionToNameArray(
33-
objectExpression: NodePath,
32+
function isWhitelistedObjectProperty(prop) {
33+
return (
34+
(types.Property.check(prop) &&
35+
((types.Identifier.check(prop.key) && !prop.computed) ||
36+
types.Literal.check(prop.key))) ||
37+
types.SpreadElement.check(prop)
38+
);
39+
}
40+
41+
function isWhiteListedObjectTypeProperty(prop) {
42+
return (
43+
types.ObjectTypeProperty.check(prop) ||
44+
types.ObjectTypeSpreadProperty.check(prop)
45+
);
46+
}
47+
48+
// Resolves an ObjectExpression or an ObjectTypeAnnotation
49+
export function resolveObjectToNameArray(
50+
object: NodePath,
3451
raw: boolean = false,
3552
): ?Array<string> {
3653
if (
37-
types.ObjectExpression.check(objectExpression.value) &&
38-
objectExpression.value.properties.every(
39-
prop =>
40-
(types.Property.check(prop) &&
41-
((types.Identifier.check(prop.key) && !prop.computed) ||
42-
types.Literal.check(prop.key))) ||
43-
types.SpreadElement.check(prop),
44-
)
54+
(types.ObjectExpression.check(object.value) &&
55+
object.value.properties.every(isWhitelistedObjectProperty)) ||
56+
(types.ObjectTypeAnnotation.check(object.value) &&
57+
object.value.properties.every(isWhiteListedObjectTypeProperty))
4558
) {
4659
let values = [];
4760
let error = false;
48-
objectExpression.get('properties').each(propPath => {
61+
object.get('properties').each(propPath => {
4962
if (error) return;
5063
const prop = propPath.value;
5164

52-
if (types.Property.check(prop)) {
65+
if (types.Property.check(prop) || types.ObjectTypeProperty.check(prop)) {
5366
// Key is either Identifier or Literal
5467
const name = prop.key.name || (raw ? prop.key.raw : prop.key.value);
5568

5669
values.push(name);
57-
} else if (types.SpreadElement.check(prop)) {
58-
const spreadObject = resolveToValue(propPath.get('argument'));
59-
const spreadValues = resolveObjectExpressionToNameArray(spreadObject);
70+
} else if (
71+
types.SpreadElement.check(prop) ||
72+
types.ObjectTypeSpreadProperty.check(prop)
73+
) {
74+
let spreadObject = resolveToValue(propPath.get('argument'));
75+
if (types.GenericTypeAnnotation.check(spreadObject.value)) {
76+
const typeAlias = resolveToValue(spreadObject.get('id'));
77+
if (types.ObjectTypeAnnotation.check(typeAlias.get('right').value)) {
78+
spreadObject = resolveToValue(typeAlias.get('right'));
79+
}
80+
}
81+
82+
const spreadValues = resolveObjectToNameArray(spreadObject);
6083
if (!spreadValues) {
6184
error = true;
6285
return;
@@ -87,7 +110,7 @@ export default function resolveObjectKeysToArray(path: NodePath): ?NodePath {
87110

88111
if (isObjectKeysCall(node)) {
89112
const objectExpression = resolveToValue(path.get('arguments').get(0));
90-
const values = resolveObjectExpressionToNameArray(objectExpression);
113+
const values = resolveObjectToNameArray(objectExpression);
91114

92115
if (values) {
93116
const nodes = values

0 commit comments

Comments
 (0)