diff --git a/packages/babel-plugin-flow-runtime/src/ConversionContext.js b/packages/babel-plugin-flow-runtime/src/ConversionContext.js index d81d33b6..3d651406 100644 --- a/packages/babel-plugin-flow-runtime/src/ConversionContext.js +++ b/packages/babel-plugin-flow-runtime/src/ConversionContext.js @@ -32,6 +32,7 @@ export default class ConversionContext { shouldAssert: boolean = true; shouldWarn: boolean = false; shouldAnnotate: boolean = true; + shouldReferenceGlobalTypeValues: boolean = false; optInOnly: boolean = false; isAnnotating: boolean = false; suppressCommentPatterns: RegExp[] = [/\$FlowFixMe/]; diff --git a/packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/177-map-class-property-transform-runtime.js b/packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/177-map-class-property-transform-runtime.js new file mode 100644 index 00000000..3c9536f2 --- /dev/null +++ b/packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/177-map-class-property-transform-runtime.js @@ -0,0 +1,63 @@ +/* @flow */ + +export const input = ` + class Foo { + bar: Map = new Map(); + + getValue(name: string) { + return this.bar.get(name); + } + } +`; + +export const expected = ` + import t from "flow-runtime"; + + class Foo { + @t.decorate(t.ref(Map, t.string(), t.any())) + bar = new Map(); + + getValue(name) { + let _nameType = t.string(); + t.param("name", _nameType).assert(name); + return this.bar.get(name); + } + } +`; + +export const annotated = ` + import t from "flow-runtime"; + + @t.annotate(t.class( + "Foo", + t.property("bar", t.ref("Map", t.string(), t.any())), + t.method("getValue", t.param("name", t.string())) + )) + class Foo { + bar = new Map(); + + getValue(name) { + return this.bar.get(name); + } + } +`; + +export const combined = ` + import t from "flow-runtime"; + + @t.annotate(t.class( + "Foo", + t.property("bar", t.ref("Map", t.string(), t.any())), + t.method("getValue", t.param("name", t.string())) + )) + class Foo { + @t.decorate(t.ref(Map, t.string(), t.any())) + bar = new Map(); + + getValue(name) { + let _nameType = t.string(); + t.param("name", _nameType).assert(name); + return this.bar.get(name); + } + } +`; diff --git a/packages/babel-plugin-flow-runtime/src/convert.js b/packages/babel-plugin-flow-runtime/src/convert.js index d273df80..56639b7a 100644 --- a/packages/babel-plugin-flow-runtime/src/convert.js +++ b/packages/babel-plugin-flow-runtime/src/convert.js @@ -13,6 +13,7 @@ export type Converter = (context: ConversionContext, path: NodePath) => Node; export type ConverterDict = {[name: string]: Converter}; const converters: ConverterDict = {}; +const GLOBAL_TYPE_VALUE_REFS = ['Date', 'Map', 'Promise', 'Set']; function getPropertyName(path: NodePath): string { if (path.get('key').isIdentifier()) return path.node.key.name; @@ -639,6 +640,12 @@ converters.GenericTypeAnnotation = (context: ConversionContext, path: NodePath): if (flowTypeName) { return context.call(flowTypeName, ...typeParameters); } + else if ( + context.shouldReferenceGlobalTypeValues && + GLOBAL_TYPE_VALUE_REFS.indexOf(name) !== -1 + ) { + return context.call('ref', subject, ...typeParameters); + } else { return context.call('ref', t.stringLiteral(name), ...typeParameters); } diff --git a/packages/babel-plugin-flow-runtime/src/transformVisitors.js b/packages/babel-plugin-flow-runtime/src/transformVisitors.js index e88d0f71..f99c16d5 100644 --- a/packages/babel-plugin-flow-runtime/src/transformVisitors.js +++ b/packages/babel-plugin-flow-runtime/src/transformVisitors.js @@ -813,7 +813,7 @@ export default function transformVisitors (context: ConversionContext): Object { null, [], t.blockStatement([ - t.returnStatement(convert(context, typeAnnotation)) + t.returnStatement(convertClassPropertyAnnotation(context, typeAnnotation)) ]) ) ]; @@ -827,12 +827,12 @@ export default function transformVisitors (context: ConversionContext): Object { } else if (context.shouldWarn) { decorator = t.decorator( - context.call('decorate', convert(context, typeAnnotation), t.booleanLiteral(false)) + context.call('decorate', convertClassPropertyAnnotation(context, typeAnnotation), t.booleanLiteral(false)) ); } else { decorator = t.decorator( - context.call('decorate', convert(context, typeAnnotation)) + context.call('decorate', convertClassPropertyAnnotation(context, typeAnnotation)) ); } if (!path.has('decorators')) { @@ -867,6 +867,17 @@ function isReactComponentClass (path: NodePath): boolean { } } +function convertClassPropertyAnnotation (context: ConversionContext, typeAnnotation: NodePath): Node { + const previous = context.shouldReferenceGlobalTypeValues; + context.shouldReferenceGlobalTypeValues = true; + try { + return convert(context, typeAnnotation); + } + finally { + context.shouldReferenceGlobalTypeValues = previous; + } +} + const supportedIterableNames = { Generator: true, Iterable: true, diff --git a/packages/flow-runtime/src/registerTypePredicates.js b/packages/flow-runtime/src/registerTypePredicates.js index beefe65b..5209fcee 100644 --- a/packages/flow-runtime/src/registerTypePredicates.js +++ b/packages/flow-runtime/src/registerTypePredicates.js @@ -4,7 +4,9 @@ import type TypeContext from './TypeContext'; export default function registerTypePredicates (context: TypeContext) { context.setPredicate('Array', (input: any) => Array.isArray(input)); - context.setPredicate('Map', (input: any) => input instanceof Map); + context.setPredicate('Map', (input: any) => { + return input instanceof Map || Object.prototype.toString.call(input) === '[object Map]'; + }); context.setPredicate('Set', (input: any) => input instanceof Set); context.setPredicate('Promise', (input: any) => { if (input instanceof Promise) { diff --git a/packages/flow-runtime/src/typed.test.js b/packages/flow-runtime/src/typed.test.js index d2eb3380..3d7f4f8b 100644 --- a/packages/flow-runtime/src/typed.test.js +++ b/packages/flow-runtime/src/typed.test.js @@ -1,5 +1,6 @@ /* @flow */ import {ok, equal, throws} from 'assert'; +import {runInNewContext} from 'vm'; import t from './globalContext'; @@ -259,6 +260,13 @@ describe('Typed API', () => { }); + it('should accept a Map from another realm', () => { + const type = t.ref(Map, t.string(), t.number()); + const map = runInNewContext('new Map([["valid", 123]])'); + + ok(type.accepts(map)); + }); + it('should make a simple function type', () => { const type = t.fn( t.param('input', t.boolean()), @@ -558,4 +566,4 @@ describe('Typed API', () => { )) ); }); -}); \ No newline at end of file +});