From 8a0305a2d7d5a0233faf087db7671eaf897a0a35 Mon Sep 17 00:00:00 2001 From: andretshurotshka Date: Wed, 24 Jul 2019 23:28:08 +0500 Subject: [PATCH 1/4] initial support for $ReadOnlyArray --- .../src/ConversionContext.js | 1 + .../bugs/199-support-$ReadOnlyArray.js | 14 +++ packages/flow-runtime/src/TypeContext.js | 9 +- .../bugs/199-support-$ReadOnlyArray.js | 18 +++ packages/flow-runtime/src/index.js | 11 +- .../src/types/$ReadOnlyArrayType.js | 107 ++++++++++++++++++ packages/flow-runtime/src/types/ArrayType.js | 31 ++--- packages/flow-runtime/src/types/index.js | 38 +++---- 8 files changed, 187 insertions(+), 42 deletions(-) create mode 100644 packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js create mode 100644 packages/flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js create mode 100644 packages/flow-runtime/src/types/$ReadOnlyArrayType.js diff --git a/packages/babel-plugin-flow-runtime/src/ConversionContext.js b/packages/babel-plugin-flow-runtime/src/ConversionContext.js index 78a505c..bc5307f 100644 --- a/packages/babel-plugin-flow-runtime/src/ConversionContext.js +++ b/packages/babel-plugin-flow-runtime/src/ConversionContext.js @@ -21,6 +21,7 @@ const FLOW_TYPENAMES = { $Supertype: '$supertype', $TupleMap: '$tupleMap', $Values: '$values', + $ReadOnlyArray: '$readOnlyArray', Class: 'Class' }; diff --git a/packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js b/packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js new file mode 100644 index 0000000..06bde69 --- /dev/null +++ b/packages/babel-plugin-flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js @@ -0,0 +1,14 @@ +/* @flow */ + +export const input = ` +type Demo = $ReadOnlyArray; +const test: Demo = [123]; +test.push(4); +`; + +export const expected = ` +import t from "flow-runtime"; +const Demo = t.type("Demo", t.$readOnlyArray(t.number())); +const test = Demo.assert([123]); +test.push(4); +`; diff --git a/packages/flow-runtime/src/TypeContext.js b/packages/flow-runtime/src/TypeContext.js index 443ce3b..6977b62 100644 --- a/packages/flow-runtime/src/TypeContext.js +++ b/packages/flow-runtime/src/TypeContext.js @@ -43,6 +43,7 @@ import { SymbolLiteralType, StringType, StringLiteralType, + $ReadOnlyArrayType, ArrayType, ObjectType, ObjectTypeCallProperty, @@ -222,7 +223,7 @@ export default class TypeContext { } // @flowIssue 252 const inferrer = this[InferrerSymbol]; - (inferrer: TypeInferrer); + /*:: (inferrer: TypeInferrer); */ return inferrer.infer(input); } @@ -906,6 +907,12 @@ export default class TypeContext { return target; } + $readOnlyArray (elementType?: Type): $ReadOnlyArrayType { + const target = new $ReadOnlyArrayType(this); + target.elementType = elementType || this.any(); + return target; + } + tuple (...types: Type[]): TupleType { const target = new TupleType(this); target.types = types; diff --git a/packages/flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js b/packages/flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js new file mode 100644 index 0000000..de54b40 --- /dev/null +++ b/packages/flow-runtime/src/__tests__/__fixtures__/bugs/199-support-$ReadOnlyArray.js @@ -0,0 +1,18 @@ +/* @flow */ + +import type TypeContext from '../../../TypeContext'; + +export function pass(t: TypeContext) { + const Demo = t.type('Demo', t.$readOnlyArray(t.number(123))); + + return Demo.assert([123]); +} + +export function fail(t: TypeContext) { + const Demo = t.type('Demo', t.$readOnlyArray(t.number(123))); + + const arr = Demo.assert([123]); + arr.push(4); + + return arr; +} diff --git a/packages/flow-runtime/src/index.js b/packages/flow-runtime/src/index.js index b4f21b3..64bfb79 100644 --- a/packages/flow-runtime/src/index.js +++ b/packages/flow-runtime/src/index.js @@ -2,12 +2,10 @@ import globalContext from './globalContext'; -import { - TypeParametersSymbol, - TypeSymbol -} from './symbols'; +import { TypeParametersSymbol, TypeSymbol } from './symbols'; import AnyType from './types/AnyType'; +import $ReadOnlyArrayType from './types/$ReadOnlyArrayType'; import ArrayType from './types/ArrayType'; import BooleanLiteralType from './types/BooleanLiteralType'; import BooleanType from './types/BooleanType'; @@ -64,6 +62,7 @@ export default globalContext; export { AnyType, + $ReadOnlyArrayType, ArrayType, BooleanLiteralType, BooleanType, @@ -106,7 +105,6 @@ export { TypeTDZ, UnionType, VoidType, - Declaration, TypeDeclaration, VarDeclaration, @@ -115,7 +113,6 @@ export { ClassDeclaration, ParameterizedClassDeclaration, ExtendsDeclaration, - TypeParametersSymbol, - TypeSymbol + TypeSymbol, }; diff --git a/packages/flow-runtime/src/types/$ReadOnlyArrayType.js b/packages/flow-runtime/src/types/$ReadOnlyArrayType.js new file mode 100644 index 0000000..1cbe1e7 --- /dev/null +++ b/packages/flow-runtime/src/types/$ReadOnlyArrayType.js @@ -0,0 +1,107 @@ +/* @flow */ + +import Type from '../types/Type'; +import TupleType from '../types/TupleType'; +import ArrayType from '../types/ArrayType'; +import compareTypes from '../compareTypes'; +import getErrorMessage from '../getErrorMessage'; +import type Validation, {ErrorTuple, IdentifierPath} from '../Validation'; + +import { + inValidationCycle, + startValidationCycle, + endValidationCycle, + inToStringCycle, + startToStringCycle, + endToStringCycle, +} from '../cyclic'; + +export default class $ReadOnlyArrayType extends Type<$ReadOnlyArray> { + typeName: string = '$ReadOnlyArrayType'; + elementType: Type; + + *errors( + validation: Validation, + path: IdentifierPath, + input: any, + ): Generator { + const {context} = this; + if (!context.checkPredicate('Array', input)) { + yield [path, getErrorMessage('ERR_EXPECT_ARRAY'), this]; + return; + } + if (validation.inCycle(this, input)) { + return; + } + validation.startCycle(this, input); + const {elementType} = this; + const {length} = input; + + for (let i = 0; i < length; i++) { + yield* elementType.errors(validation, path.concat(i), input[i]); + } + Object.freeze(input); + validation.endCycle(this, input); + } + + accepts(input: any): boolean { + const {context} = this; + if (!context.checkPredicate('Array', input)) { + return false; + } + if (inValidationCycle(this, input)) { + return true; + } + startValidationCycle(this, input); + const {elementType} = this; + const {length} = input; + for (let i = 0; i < length; i++) { + if (!elementType.accepts(input[i])) { + endValidationCycle(this, input); + return false; + } + } + endValidationCycle(this, input); + return true; + } + + compareWith(input: Type): -1 | 0 | 1 { + const {elementType} = this; + if (input instanceof TupleType) { + const {types} = input; + for (let i = 0; i < types.length; i++) { + const result = compareTypes(elementType, types[i]); + if (result === -1) { + return -1; + } + } + return 1; + } else if (input instanceof ArrayType) { + return compareTypes(elementType, input.elementType); + } else { + return -1; + } + } + + toString(): string { + const {elementType} = this; + if (inToStringCycle(this)) { + if (typeof elementType.name === 'string') { + return `$ReadOnlyArray<$Cycle<${elementType.name}>>`; + } else { + return `$ReadOnlyArray<$Cycle>`; + } + } + startToStringCycle(this); + const output = `$ReadOnlyArray<${elementType.toString()}>`; + endToStringCycle(this); + return output; + } + + toJSON() { + return { + typeName: this.typeName, + elementType: this.elementType, + }; + } +} diff --git a/packages/flow-runtime/src/types/ArrayType.js b/packages/flow-runtime/src/types/ArrayType.js index 7936151..ce7c9f5 100644 --- a/packages/flow-runtime/src/types/ArrayType.js +++ b/packages/flow-runtime/src/types/ArrayType.js @@ -4,7 +4,7 @@ import Type from './Type'; import TupleType from './TupleType'; import compareTypes from '../compareTypes'; -import getErrorMessage from "../getErrorMessage"; +import getErrorMessage from '../getErrorMessage'; import type Validation, {ErrorTuple, IdentifierPath} from '../Validation'; import { @@ -13,14 +13,18 @@ import { endValidationCycle, inToStringCycle, startToStringCycle, - endToStringCycle + endToStringCycle, } from '../cyclic'; -export default class ArrayType extends Type { +export default class ArrayType extends Type> { typeName: string = 'ArrayType'; elementType: Type; - *errors (validation: Validation, path: IdentifierPath, input: any): Generator { + *errors( + validation: Validation, + path: IdentifierPath, + input: any, + ): Generator { const {context} = this; if (!context.checkPredicate('Array', input)) { yield [path, getErrorMessage('ERR_EXPECT_ARRAY'), this]; @@ -39,7 +43,7 @@ export default class ArrayType extends Type { validation.endCycle(this, input); } - accepts (input: any): boolean { + accepts(input: any): boolean { const {context} = this; if (!context.checkPredicate('Array', input)) { return false; @@ -60,7 +64,7 @@ export default class ArrayType extends Type { return true; } - compareWith (input: Type): -1 | 0 | 1 { + compareWith(input: Type): -1 | 0 | 1 { const {elementType} = this; if (input instanceof TupleType) { const {types} = input; @@ -71,22 +75,19 @@ export default class ArrayType extends Type { } } return 1; - } - else if (input instanceof ArrayType) { + } else if (input instanceof ArrayType) { return compareTypes(elementType, input.elementType); - } - else { + } else { return -1; } } - toString (): string { + toString(): string { const {elementType} = this; if (inToStringCycle(this)) { if (typeof elementType.name === 'string') { return `Array<$Cycle<${elementType.name}>>`; - } - else { + } else { return `Array<$Cycle>`; } } @@ -96,10 +97,10 @@ export default class ArrayType extends Type { return output; } - toJSON () { + toJSON() { return { typeName: this.typeName, - elementType: this.elementType + elementType: this.elementType, }; } } diff --git a/packages/flow-runtime/src/types/index.js b/packages/flow-runtime/src/types/index.js index 1923ae5..f3f236c 100644 --- a/packages/flow-runtime/src/types/index.js +++ b/packages/flow-runtime/src/types/index.js @@ -1,38 +1,37 @@ /* @flow */ -export type TypeCreator = (partial: PartialType) => T; -export type TypeRevealer = () => ? Type | Class; +export type TypeCreator = (partial: PartialType) => T; +export type TypeRevealer = () => ?Type | Class; -export type FunctionBodyCreator = ( - partial: PartialType<(...params: P[]) => R> +export type FunctionBodyCreator = ( + partial: PartialType<(...params: P[]) => R>, ) => Array>; -export type ValidFunctionBody - = TypeParameter - | FunctionTypeParam - | FunctionTypeRestParam - | FunctionTypeReturn - ; +export type ValidFunctionBody = + | TypeParameter + | FunctionTypeParam + | FunctionTypeRestParam + | FunctionTypeReturn; export type ObjectPropertyDict = $ObjMap(v: Type) => V>; -export type ValidObjectBody - = ObjectTypeCallProperty - | ObjectTypeProperty<$Keys, $ObjMap(v: Type) => V>> - | ObjectTypeIndexer<*, *> - ; +export type ValidObjectBody = + | ObjectTypeCallProperty + | ObjectTypeProperty<$Keys, $ObjMap(v: Type) => V>> + | ObjectTypeIndexer<*, *>; -export type TypeConstraint = (input: any) => ? string; +export type TypeConstraint = (input: any) => ?string; export type ApplicableType = Type & { - name: string; - apply

(...typeParameters: Type

[]): TypeParameterApplication; + name: string, + apply

(...typeParameters: Type

[]): TypeParameterApplication, }; export type Constructor = Class; import AnyType from './AnyType'; import ArrayType from './ArrayType'; +import $ReadOnlyArrayType from './$ReadOnlyArrayType'; import BooleanLiteralType from './BooleanLiteralType'; import BooleanType from './BooleanType'; import EmptyType from './EmptyType'; @@ -78,6 +77,7 @@ import VoidType from './VoidType'; export { AnyType, ArrayType, + $ReadOnlyArrayType, BooleanLiteralType, BooleanType, EmptyType, @@ -118,5 +118,5 @@ export { TypeReference, TypeTDZ, UnionType, - VoidType + VoidType, }; From 335b426d49dbd94ee09000c9095b2987ec020a2c Mon Sep 17 00:00:00 2001 From: andretshurotshka Date: Wed, 24 Jul 2019 23:58:49 +0500 Subject: [PATCH 2/4] Add subtyping rules --- .../$ReadOnlyArray/subtyping$ReadOnlyArray.js | 15 +++++++++++++++ .../flow-runtime/src/registerTypePredicates.js | 2 ++ packages/flow-runtime/src/types/ArrayType.js | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/flow-runtime/src/__tests__/__fixtures__/$ReadOnlyArray/subtyping$ReadOnlyArray.js diff --git a/packages/flow-runtime/src/__tests__/__fixtures__/$ReadOnlyArray/subtyping$ReadOnlyArray.js b/packages/flow-runtime/src/__tests__/__fixtures__/$ReadOnlyArray/subtyping$ReadOnlyArray.js new file mode 100644 index 0000000..a591007 --- /dev/null +++ b/packages/flow-runtime/src/__tests__/__fixtures__/$ReadOnlyArray/subtyping$ReadOnlyArray.js @@ -0,0 +1,15 @@ +/* @flow */ + +import type TypeContext from '../../../TypeContext'; + +export function pass(t: TypeContext) { + const bar = t.array(t.number()).assert([-1, 1, 2, 3]); + const baz = t.$readOnlyArray(t.number()).assert(bar); + return baz; +} + +export function fail(t: TypeContext) { + const bar = t.$readOnlyArray(t.number()).assert([0, 1, 2, 3]); + const baz = t.array(t.number()).assert(bar); + return baz; +} diff --git a/packages/flow-runtime/src/registerTypePredicates.js b/packages/flow-runtime/src/registerTypePredicates.js index beefe65..8747fd9 100644 --- a/packages/flow-runtime/src/registerTypePredicates.js +++ b/packages/flow-runtime/src/registerTypePredicates.js @@ -3,6 +3,8 @@ import type TypeContext from './TypeContext'; export default function registerTypePredicates (context: TypeContext) { + // TODO: make better subtyping rules + context.setPredicate('$ReadOnlyArray', (input: any) => Array.isArray(input) && Object.isFrozen(input)); context.setPredicate('Array', (input: any) => Array.isArray(input)); context.setPredicate('Map', (input: any) => input instanceof Map); context.setPredicate('Set', (input: any) => input instanceof Set); diff --git a/packages/flow-runtime/src/types/ArrayType.js b/packages/flow-runtime/src/types/ArrayType.js index ce7c9f5..87d79cd 100644 --- a/packages/flow-runtime/src/types/ArrayType.js +++ b/packages/flow-runtime/src/types/ArrayType.js @@ -26,7 +26,7 @@ export default class ArrayType extends Type> { input: any, ): Generator { const {context} = this; - if (!context.checkPredicate('Array', input)) { + if (!context.checkPredicate('Array', input) || context.checkPredicate('$ReadOnlyArray', input)) { yield [path, getErrorMessage('ERR_EXPECT_ARRAY'), this]; return; } From 84f58ea0fa662a535e744a9c2318da85ad2ddec6 Mon Sep 17 00:00:00 2001 From: andretshurotshka Date: Thu, 25 Jul 2019 18:38:21 +0500 Subject: [PATCH 3/4] Remove Object.freeze if it is warning mode --- packages/flow-runtime/src/types/$ReadOnlyArrayType.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flow-runtime/src/types/$ReadOnlyArrayType.js b/packages/flow-runtime/src/types/$ReadOnlyArrayType.js index 1cbe1e7..ae995c7 100644 --- a/packages/flow-runtime/src/types/$ReadOnlyArrayType.js +++ b/packages/flow-runtime/src/types/$ReadOnlyArrayType.js @@ -40,7 +40,11 @@ export default class $ReadOnlyArrayType extends Type<$ReadOnlyArray> { for (let i = 0; i < length; i++) { yield* elementType.errors(validation, path.concat(i), input[i]); } - Object.freeze(input); + if (context.mode === 'assert') { + Object.freeze(input); + } else { + // TODO: support warning mode somehow + } validation.endCycle(this, input); } From 1060e28e1c7d0ac543241f6860c6f67a1c7d07c6 Mon Sep 17 00:00:00 2001 From: andretshurotshka Date: Thu, 25 Jul 2019 18:39:56 +0500 Subject: [PATCH 4/4] Add $ReadOnlyArray compare to Array --- packages/flow-runtime/src/types/ArrayType.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flow-runtime/src/types/ArrayType.js b/packages/flow-runtime/src/types/ArrayType.js index 87d79cd..6dedc50 100644 --- a/packages/flow-runtime/src/types/ArrayType.js +++ b/packages/flow-runtime/src/types/ArrayType.js @@ -2,6 +2,7 @@ import Type from './Type'; import TupleType from './TupleType'; +import $ReadOnlyArrayType from './$ReadOnlyArrayType'; import compareTypes from '../compareTypes'; import getErrorMessage from '../getErrorMessage'; @@ -77,6 +78,8 @@ export default class ArrayType extends Type> { return 1; } else if (input instanceof ArrayType) { return compareTypes(elementType, input.elementType); + } else if (input instanceof $ReadOnlyArrayType) { + return compareTypes(elementType, input.elementType); } else { return -1; }