import { mapMerge , mapUpdateInto , mapMap , setUnionInto } from "collection-utils" ; import { TypeKind , PrimitiveStringTypeKind , Type , UnionType , PrimitiveTypeKind , isPrimitiveTypeKind } from "./Type" ; import { matchTypeExhaustive } from "./TypeUtils" ; import { TypeAttributes , combineTypeAttributes , emptyTypeAttributes , makeTypeAttributesInferred , increaseTypeAttributesDistance } from "./attributes/TypeAttributes" ; import { defined , assert , panic , assertNever } from "./support/Support" ; import { TypeBuilder } from "./TypeBuilder" ; import { StringTypes , stringTypesTypeAttributeKind } from "./attributes/StringTypes" ; import { TypeRef } from "./TypeGraph" ; export interface UnionTypeProvider < TArrayData , TObjectData > { readonly arrayData : TArrayData ; readonly objectData : TObjectData ; readonly enumCases : ReadonlySet < string > ; getMemberKinds ( ) : TypeAttributeMap < TypeKind > ; readonly lostTypeAttributes : boolean ; } export type TypeAttributeMap < T extends TypeKind > = Map < T , TypeAttributes > ; type TypeAttributeMapBuilder < T extends TypeKind > = Map < T , TypeAttributes [ ] > ; function addAttributes ( accumulatorAttributes , newAttributes ) { if ( accumulatorAttributes === undefined ) return newAttributes ; return combineTypeAttributes ( "union" , accumulatorAttributes , newAttributes ) ; } function setAttributes < T extends TypeKind > ( attributeMap , kind , newAttributes ) { attributeMap . set ( kind , addAttributes ( attributeMap . get ( kind ) , newAttributes ) ) ; } function addAttributesToBuilder < T extends TypeKind > ( builder , kind , newAttributes ) { let arr = builder . get ( kind ) ; if ( arr === undefined ) { arr = [ ] ; builder . set ( kind , arr ) ; } arr . push ( newAttributes ) ; } function buildTypeAttributeMap < T extends TypeKind > ( builder ) { return mapMap ( builder , arr => combineTypeAttributes ( "union" , arr ) ) ; } function moveAttributes < T extends TypeKind > ( map , fromKind , toKind ) { const fromAttributes = defined ( map . get ( fromKind ) ) ; map . delete ( fromKind ) ; setAttributes ( map , toKind , fromAttributes ) ; } export class UnionAccumulator < TArray , TObject > implements UnionTypeProvider < TArray [ ] , TObject [ ] > { private readonly _nonStringTypeAttributes : TypeAttributeMapBuilder < TypeKind > = new Map ( ) ; private readonly _stringTypeAttributes : TypeAttributeMapBuilder < PrimitiveStringTypeKind > = new Map ( ) ; readonly arrayData : TArray [ ] = [ ] ; readonly objectData : TObject [ ] = [ ] ; private readonly _enumCases : Set < string > = new Set ( ) ; private _lostTypeAttributes : boolean = false ; constructor ( private readonly _conflateNumbers ) { } private have ( kind ) { return ( this . _nonStringTypeAttributes . has ( kind ) || this . _stringTypeAttributes . has ( kind as PrimitiveStringTypeKind ) ) ; } addNone ( _attributes ) { this . _lostTypeAttributes = true ; } addAny ( attributes ) { addAttributesToBuilder ( this . _nonStringTypeAttributes , "any" , attributes ) ; this . _lostTypeAttributes = true ; } addPrimitive ( kind , attributes ) { assert ( kind !== "any" , "any must be added with addAny" ) ; addAttributesToBuilder ( this . _nonStringTypeAttributes , kind , attributes ) ; } protected addFullStringType ( attributes , stringTypes ) { let stringTypesAttributes = undefined ; if ( stringTypes === undefined ) { stringTypes = stringTypesTypeAttributeKind . tryGetInAttributes ( attributes ) ; } else { stringTypesAttributes = stringTypesTypeAttributeKind . makeAttributes ( stringTypes ) ; } if ( stringTypes === undefined ) { stringTypes = StringTypes . unrestricted ; stringTypesAttributes = stringTypesTypeAttributeKind . makeAttributes ( stringTypes ) ; } const maybeEnumAttributes = this . _nonStringTypeAttributes . get ( "enum" ) ; if ( stringTypes . isRestricted ) { assert ( maybeEnumAttributes === undefined , "We can't add both an enum as well as a restricted string type to a union builder" ) ; } addAttributesToBuilder ( this . _stringTypeAttributes , "string" , attributes ) ; if ( stringTypesAttributes !== undefined ) { addAttributesToBuilder ( this . _stringTypeAttributes , "string" , stringTypesAttributes ) ; } } addStringType ( kind , attributes , stringTypes ? ) { if ( kind === "string" ) { this . addFullStringType ( attributes , stringTypes ) ; return ; } addAttributesToBuilder ( this . _stringTypeAttributes , kind , attributes ) ; if ( stringTypes !== undefined ) { addAttributesToBuilder ( this . _stringTypeAttributes , kind , stringTypesTypeAttributeKind . makeAttributes ( stringTypes ) ) ; } } addArray ( t , attributes ) { this . arrayData . push ( t ) ; addAttributesToBuilder ( this . _nonStringTypeAttributes , "array" , attributes ) ; } addObject ( t , attributes ) { this . objectData . push ( t ) ; addAttributesToBuilder ( this . _nonStringTypeAttributes , "object" , attributes ) ; } addEnum ( cases , attributes ) { const maybeStringAttributes = this . _stringTypeAttributes . get ( "string" ) ; if ( maybeStringAttributes !== undefined ) { addAttributesToBuilder ( this . _stringTypeAttributes , "string" , attributes ) ; return ; } addAttributesToBuilder ( this . _nonStringTypeAttributes , "enum" , attributes ) ; setUnionInto ( this . _enumCases , cases ) ; } addStringCases ( cases , attributes ) { this . addFullStringType ( attributes , StringTypes . fromCases ( cases ) ) ; } addStringCase ( s , count , attributes ) { this . addFullStringType ( attributes , StringTypes . fromCase ( s , count ) ) ; } get enumCases ( ) : ReadonlySet < string > { return this . _enumCases ; } getMemberKinds ( ) { assert ( ! ( this . have ( "enum" ) && this . have ( "string" ) ) , "We can't have both strings and enums in the same union" ) ; let merged = mapMerge ( buildTypeAttributeMap ( this . _nonStringTypeAttributes ) , buildTypeAttributeMap ( this . _stringTypeAttributes ) ) ; if ( merged . size === 0 ) { return new Map ( [ [ "none" , emptyTypeAttributes ] as [ TypeKind , TypeAttributes ] ] ) ; } if ( this . _nonStringTypeAttributes . has ( "any" ) ) { assert ( this . _lostTypeAttributes , "This had to be set when we added 'any'" ) ; const allAttributes = combineTypeAttributes ( "union" , Array . from ( merged . values ( ) ) ) ; return new Map ( [ [ "any" , allAttributes ] as [ TypeKind , TypeAttributes ] ] ) ; } if ( this . _conflateNumbers && this . have ( "integer" ) && this . have ( "double" ) ) { moveAttributes ( merged , "integer" , "double" ) ; } if ( this . have ( "map" ) ) { moveAttributes ( merged , "map" , "class" ) ; } return merged ; } get lostTypeAttributes ( ) : boolean { return this . _lostTypeAttributes ; } } class FauxUnion { getAttributes ( ) { return emptyTypeAttributes ; } } type UnionOrFaux = UnionType | FauxUnion ; function attributesForTypes ( types ) { const unionsForType = new Map < Type , Set < UnionOrFaux > > ( ) ; const typesForUnion = new Map < UnionOrFaux , Set < Type > > ( ) ; const unions = new Set < UnionType > ( ) ; let unionsEquivalentToRoot = new Set ( ) ; function traverse ( t , path , isEquivalentToRoot ) { if ( t instanceof UnionType ) { unions . add ( t ) ; if ( isEquivalentToRoot ) { unionsEquivalentToRoot = unionsEquivalentToRoot . add ( t ) ; } isEquivalentToRoot = isEquivalentToRoot && t . members . size === 1 ; path . push ( t ) ; for ( const m of t . members ) { traverse ( m , path , isEquivalentToRoot ) ; } path . pop ( ) ; } else { mapUpdateInto ( unionsForType , t , s => ( s === undefined ? new Set ( path ) : setUnionInto ( s , path ) ) ) ; for ( const u of path ) { mapUpdateInto ( typesForUnion , u , s => ( s === undefined ? new Set ( [ t ] ) : s . add ( t ) ) ) ; } } } const rootPath = [ new FauxUnion ( ) ] ; const typesArray = Array . from ( types ) ; for ( const t of typesArray ) { traverse ( t , rootPath , typesArray . length === 1 ) ; } const resultAttributes = mapMap ( unionsForType , ( unionForType , t ) => { const singleAncestors = Array . from ( unionForType ) . filter ( u => defined ( typesForUnion . get ( u ) ) . size === 1 ) ; assert ( singleAncestors . every ( u => defined ( typesForUnion . get ( u ) ) . has ( t ) ) , "We messed up bookkeeping" ) ; const inheritedAttributes = singleAncestors . map ( u => u . getAttributes ( ) ) ; return combineTypeAttributes ( "union" , [ t . getAttributes ( ) ] . concat ( inheritedAttributes ) ) ; } ) ; const unionAttributes = Array . from ( unions ) . map ( u => { const t = typesForUnion . get ( u ) ; if ( t !== undefined && t . size === 1 ) { return emptyTypeAttributes ; } const attributes = u . getAttributes ( ) ; if ( unionsEquivalentToRoot . has ( u ) ) { return attributes ; } return makeTypeAttributesInferred ( attributes ) ; } ) ; return [ resultAttributes , combineTypeAttributes ( "union" , unionAttributes ) ] ; } export class TypeRefUnionAccumulator extends UnionAccumulator < TypeRef , TypeRef > { private addType ( t , attributes ) { matchTypeExhaustive ( t , _noneType => this . addNone ( attributes ) , _anyType => this . addAny ( attributes ) , _nullType => this . addPrimitive ( "null" , attributes ) , _boolType => this . addPrimitive ( "bool" , attributes ) , _integerType => this . addPrimitive ( "integer" , attributes ) , _doubleType => this . addPrimitive ( "double" , attributes ) , _stringType => this . addStringType ( "string" , attributes ) , arrayType => this . addArray ( arrayType . items . typeRef , attributes ) , classType => this . addObject ( classType . typeRef , attributes ) , mapType => this . addObject ( mapType . typeRef , attributes ) , objectType => this . addObject ( objectType . typeRef , attributes ) , enumType => this . addEnum ( enumType . cases , attributes ) , _unionType => { return panic ( "The unions should have been eliminated in attributesForTypesInUnion" ) ; } , transformedStringType => this . addStringType ( transformedStringType . kind as PrimitiveStringTypeKind , attributes ) ) ; } addTypes ( types ) { const [ attributesMap , unionAttributes ] = attributesForTypes ( types ) ; for ( const [ t , attributes ] of attributesMap ) { this . addType ( t , attributes ) ; } return unionAttributes ; } } export abstract class UnionBuilder < TBuilder extends TypeBuilder , TArrayData , TObjectData > { constructor ( protected readonly typeBuilder ) { } protected abstract makeObject ( objects , typeAttributes , forwardingRef ) ; protected abstract makeArray ( arrays , typeAttributes , forwardingRef ) ; private makeTypeOfKind ( typeProvider , kind , typeAttributes , forwardingRef ) { switch ( kind ) { case "string" : return this . typeBuilder . getStringType ( typeAttributes , undefined , forwardingRef ) ; case "enum" : return this . typeBuilder . getEnumType ( typeAttributes , typeProvider . enumCases , forwardingRef ) ; case "object" : return this . makeObject ( typeProvider . objectData , typeAttributes , forwardingRef ) ; case "array" : return this . makeArray ( typeProvider . arrayData , typeAttributes , forwardingRef ) ; default : if ( isPrimitiveTypeKind ( kind ) ) { return this . typeBuilder . getPrimitiveType ( kind , typeAttributes , forwardingRef ) ; } if ( kind === "union" || kind === "class" || kind === "map" || kind === "intersection" ) { return panic ( `getMemberKinds() shouldn't return ${ kind }` ) ; } return assertNever ( kind ) ; } } buildUnion ( typeProvider , unique , typeAttributes , forwardingRef ? ) { const kinds = typeProvider . getMemberKinds ( ) ; if ( typeProvider . lostTypeAttributes ) { this . typeBuilder . setLostTypeAttributes ( ) ; } if ( kinds . size === 1 ) { const [ [ kind , memberAttributes ] ] = Array . from ( kinds ) ; const allAttributes = combineTypeAttributes ( "union" , typeAttributes , increaseTypeAttributesDistance ( memberAttributes ) ) ; const t = this . makeTypeOfKind ( typeProvider , kind , allAttributes , forwardingRef ) ; return t ; } const union = unique ? this . typeBuilder . getUniqueUnionType ( typeAttributes , undefined , forwardingRef ) : undefined ; const types = [ ] ; for ( const [ kind , memberAttributes ] of kinds ) { types . push ( this . makeTypeOfKind ( typeProvider , kind , memberAttributes , undefined ) ) ; } const typesSet = new Set ( types ) ; if ( union !== undefined ) { this . typeBuilder . setSetOperationMembers ( union , typesSet ) ; return union ; } else { return this . typeBuilder . getUnionType ( typeAttributes , typesSet , forwardingRef ) ; } } }