Separate type for keeping inferred information about strings

This commit is contained in:
Mark Probst 2018-04-23 21:51:58 -04:00
Родитель c26b96071e
Коммит 3462d40dbb
11 изменённых файлов: 231 добавлений и 104 удалений

Просмотреть файл

@ -22,6 +22,7 @@ import * as graphql from "graphql/language";
import { TypeNames, makeNamesTypeAttributes, namesTypeAttributeKind } from "./TypeNames";
import { TypeAttributes, emptyTypeAttributes } from "./TypeAttributes";
import { ErrorMessage, messageAssert } from "./Messages";
import { StringTypes } from "./StringTypes";
interface GQLType {
kind: TypeKind;
@ -113,7 +114,7 @@ function makeScalar(builder: TypeBuilder, ft: GQLType): TypeRef {
return builder.getPrimitiveType("double");
default:
// FIXME: support ID specifically?
return builder.getStringType(emptyTypeAttributes, null);
return builder.getStringType(emptyTypeAttributes, StringTypes.unrestricted);
}
}
@ -427,7 +428,9 @@ export function makeGraphQLQueryTypes(
namesTypeAttributeKind.makeAttributes(
TypeNames.make(OrderedSet(["error"]), OrderedSet(["graphQLError"]), false)
),
OrderedMap({ message: new ClassProperty(builder.getStringType(emptyTypeAttributes, null), false) })
OrderedMap({
message: new ClassProperty(builder.getStringType(emptyTypeAttributes, StringTypes.unrestricted), false)
})
);
const errorArray = builder.getArrayType(errorType);
builder.addAttributes(

Просмотреть файл

@ -2,13 +2,14 @@
import { Set, OrderedMap, OrderedSet } from "immutable";
import { Type, PrimitiveType, UnionType, stringEnumCasesTypeAttributeKind } from "./Type";
import { Type, PrimitiveType, UnionType } from "./Type";
import { combineTypeAttributesOfTypes, stringEnumCases } from "./TypeUtils";
import { TypeGraph } from "./TypeGraph";
import { TypeRef, StringTypeMapping } from "./TypeBuilder";
import { GraphRewriteBuilder } from "./GraphRewriting";
import { assert, defined } from "./Support";
import { combineTypeAttributes } from "./TypeAttributes";
import { stringTypesTypeAttributeKind, StringTypes } from "./StringTypes";
const MIN_LENGTH_FOR_ENUM = 10;
@ -32,12 +33,12 @@ function replaceString(
): TypeRef {
assert(group.size === 1);
const t = defined(group.first());
const attributes = t.getAttributes().filterNot((_, k) => k === stringEnumCasesTypeAttributeKind);
const attributes = t.getAttributes().filterNot((_, k) => k === stringTypesTypeAttributeKind);
const maybeEnumCases = shouldBeEnum(t);
if (maybeEnumCases !== undefined) {
return builder.getEnumType(attributes, maybeEnumCases.keySeq().toOrderedSet(), forwardingRef);
}
return builder.getStringType(attributes, null, forwardingRef);
return builder.getStringType(attributes, StringTypes.unrestricted, forwardingRef);
}
// A union needs replacing if it contains more than one string type, one of them being

Просмотреть файл

@ -9,6 +9,7 @@ import { UnionBuilder, UnionAccumulator } from "./UnionBuilder";
import { isTime, isDateTime, isDate } from "./DateTime";
import { ClassProperty } from "./Type";
import { TypeAttributes, emptyTypeAttributes } from "./TypeAttributes";
import { StringTypes } from "./StringTypes";
// This should be the recursive type
// Value[] | NestedValueArray[]
@ -43,13 +44,11 @@ class InferenceUnionBuilder extends UnionBuilder<TypeBuilder, NestedValueArray,
}
protected makeEnum(
cases: string[],
counts: { [name: string]: number },
stringTypes: StringTypes,
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
const caseMap = OrderedMap(cases.map((c: string): [string, number] => [c, counts[c]]));
return this.typeBuilder.getStringType(typeAttributes, caseMap, forwardingRef);
return this.typeBuilder.getStringType(typeAttributes, stringTypes, forwardingRef);
}
protected makeObject(

Просмотреть файл

@ -39,6 +39,7 @@ import {
AccessorEntry
} from "./AccessorNames";
import { ErrorMessage, messageAssert, messageError } from "./Messages";
import { StringTypes } from "./StringTypes";
export enum PathElementKind {
Root,
@ -616,10 +617,10 @@ export async function addTypesInSchema(
default:
// FIXME: Output a warning here instead to indicate that
// the format is uninterpreted.
return typeBuilder.getStringType(inferredAttributes, null);
return typeBuilder.getStringType(inferredAttributes, StringTypes.unrestricted);
}
}
return typeBuilder.getStringType(inferredAttributes, null);
return typeBuilder.getStringType(inferredAttributes, StringTypes.unrestricted);
}
async function makeArrayType(): Promise<TypeRef> {

Просмотреть файл

@ -35,6 +35,7 @@ import {
emptyTypeAttributes,
makeTypeAttributesInferred
} from "./TypeAttributes";
import { MutableStringTypes, StringTypes } from "./StringTypes";
function canResolve(t: IntersectionType): boolean {
const members = setOperationMembersRecursively(t)[0];
@ -59,7 +60,7 @@ class IntersectionAccumulator
private _otherPrimitiveTypes: OrderedSet<PrimitiveTypeKind> | undefined;
private _otherPrimitiveAttributes: TypeAttributeMap<PrimitiveTypeKind> = OrderedMap();
private _enumCases: OrderedSet<string> | undefined;
private _stringTypes: MutableStringTypes = MutableStringTypes.unrestricted;
private _enumAttributes: TypeAttributes = emptyTypeAttributes;
// * undefined: We haven't seen any types yet.
@ -130,11 +131,7 @@ class IntersectionAccumulator
return;
}
const newCases = OrderedSet<string>().union(...enums.map(t => t.cases).toArray());
if (this._enumCases === undefined) {
this._enumCases = newCases;
} else {
this._enumCases = this._enumCases.intersect(newCases);
}
this._stringTypes.intersectCasesWithSet(newCases);
}
private updateArrayItemTypes(members: OrderedSet<Type>): void {
@ -269,14 +266,8 @@ class IntersectionAccumulator
return [this._objectProperties, this._additionalPropertyTypes];
}
get enumCases(): string[] {
return defined(this._enumCases).toArray();
}
get enumCaseMap(): { [name: string]: number } {
const caseMap: { [name: string]: number } = {};
defined(this._enumCases).forEach(n => (caseMap[n] = 1));
return caseMap;
get stringTypes(): StringTypes {
return this._stringTypes.toImmutable();
}
getMemberKinds(): TypeAttributeMap<TypeKind> {
@ -303,7 +294,7 @@ class IntersectionAccumulator
let kinds: TypeAttributeMap<TypeKind> = primitiveStringKinds.merge(otherPrimitiveKinds);
if (this._enumCases !== undefined && this._enumCases.size > 0) {
if (this._stringTypes.isRestrictedAndAllowed) {
kinds = kinds.set("enum", this._enumAttributes);
} else if (!this._enumAttributes.isEmpty()) {
if (kinds.has("string")) {

139
src/StringTypes.ts Normal file
Просмотреть файл

@ -0,0 +1,139 @@
import { is, hash, OrderedMap, OrderedSet } from "immutable";
import { TypeAttributeKind } from "./TypeAttributes";
let unrestrictedStringTypes: StringTypes | undefined = undefined;
export class StringTypes {
static get unrestricted(): StringTypes {
if (unrestrictedStringTypes === undefined) {
unrestrictedStringTypes = new StringTypes(undefined);
}
return unrestrictedStringTypes;
}
static fromCase(s: string, count: number): StringTypes {
const caseMap: { [name: string]: number } = {};
caseMap[s] = count;
return new StringTypes(OrderedMap([[s, count] as [string, number]]));
}
static fromCases(cases: string[]): StringTypes {
const caseMap: { [name: string]: number } = {};
for (const s of cases) {
caseMap[s] = 1;
}
return new StringTypes(OrderedMap(cases.map(s => [s, 1] as [string, number])));
}
// undefined means no restrictions
constructor(readonly cases: OrderedMap<string, number> | undefined) {}
get isRestricted(): boolean {
return this.cases !== undefined;
}
union(other: StringTypes): StringTypes {
if (this.cases === undefined || other.cases === undefined) {
return new StringTypes(undefined);
}
return new StringTypes(this.cases.mergeWith((x, y) => x + y, other.cases));
}
equals(other: any): boolean {
if (!(other instanceof StringTypes)) return false;
return is(this.cases, other.cases);
}
hashCode(): number {
return hash(this.cases);
}
toString(): string {
const enumCases = this.cases;
if (enumCases === undefined) {
return "no enum";
}
const firstKey = enumCases.keySeq().first();
if (firstKey === undefined) {
return "enum with no cases";
}
return `${enumCases.size.toString()} enums: ${firstKey} (${enumCases.get(firstKey)}), ...`;
}
}
export class MutableStringTypes {
static get unrestricted(): MutableStringTypes {
return new MutableStringTypes({}, undefined);
}
static get none(): MutableStringTypes {
return new MutableStringTypes({}, []);
}
// _enumCases === undefined means no restrictions
protected constructor(private _enumCaseMap: { [name: string]: number }, private _enumCases: string[] | undefined) {}
get isRestrictedAndAllowed(): boolean {
return this._enumCases !== undefined && this._enumCases.length > 0;
}
makeUnrestricted(): void {
this._enumCaseMap = {};
this._enumCases = undefined;
}
addCase(s: string, count: number): void {
if (!Object.prototype.hasOwnProperty.call(this._enumCaseMap, s)) {
this._enumCaseMap[s] = 0;
if (this._enumCases === undefined) {
this._enumCases = [];
}
this._enumCases.push(s);
}
this._enumCaseMap[s] += count;
}
addCases(cases: string[]): void {
for (const s of cases) {
this.addCase(s, 1);
}
}
intersectCasesWithSet(newCases: OrderedSet<string>): void {
let newEnumCases: string[];
if (this._enumCases === undefined) {
newEnumCases = newCases.toArray();
for (const s of newEnumCases) {
this._enumCaseMap[s] = 1;
}
} else {
newEnumCases = [];
for (const s of this._enumCases) {
if (newCases.has(s)) {
newEnumCases.push(s);
} else {
this._enumCaseMap[s] = 0;
}
}
}
this._enumCases = newEnumCases;
}
toImmutable(): StringTypes {
if (this._enumCases === undefined) {
return new StringTypes(undefined);
}
return new StringTypes(OrderedMap(this._enumCases.map(s => [s, this._enumCaseMap[s]] as [string, number])));
}
}
export const stringTypesTypeAttributeKind = new TypeAttributeKind<StringTypes>(
"stringTypes",
true,
st => st.isRestricted,
(a, b) => a.union(b),
_ => undefined,
st => st.toString()
);

Просмотреть файл

@ -6,7 +6,7 @@ import { defined, panic, assert, mapOptional } from "./Support";
import { TypeRef } from "./TypeBuilder";
import { TypeReconstituter, BaseGraphRewriteBuilder } from "./GraphRewriting";
import { TypeNames, namesTypeAttributeKind } from "./TypeNames";
import { TypeAttributes, TypeAttributeKind } from "./TypeAttributes";
import { TypeAttributes } from "./TypeAttributes";
import { ErrorMessage, messageAssert } from "./Messages";
export type PrimitiveStringTypeKind = "string" | "date" | "time" | "date-time";
@ -44,29 +44,6 @@ function orderedSetUnion<T>(sets: OrderedSet<OrderedSet<T>>): OrderedSet<T> {
return setArray[0].union(...setArray.slice(1));
}
export const stringEnumCasesTypeAttributeKind = new TypeAttributeKind<OrderedMap<string, number> | null>(
"stringEnumCases",
false,
true,
(a, b) => {
if (a === null || b === null) {
return null;
}
return a.mergeWith((x, y) => x + y, b);
},
_ => undefined,
m => {
if (m === null) {
return "no enum";
}
const firstKey = m.keySeq().first();
if (firstKey === undefined) {
return "enum with no cases";
}
return `${m.size.toString()} enums: ${firstKey} (${m.get(firstKey)}), ...`;
}
);
// undefined in case the identity is unique
export type TypeIdentity = List<any> | undefined;
@ -213,7 +190,7 @@ export abstract class Type {
}
function hasUniqueIdentityAttributes(attributes: TypeAttributes): boolean {
return attributes.keySeq().some(ta => ta.uniqueIdentity);
return attributes.some((v, ta) => ta.requiresUniqueIdentity(v));
}
function identityAttributes(attributes: TypeAttributes): TypeAttributes {

Просмотреть файл

@ -12,7 +12,7 @@ export class TypeAttributeKind<T> {
constructor(
readonly name: string,
private readonly _inIdentity: boolean,
readonly uniqueIdentity: boolean,
private readonly _uniqueIdentity: ((a: T) => boolean) | boolean,
combine: ((a: T, b: T) => T) | undefined,
makeInferred: ((a: T) => T | undefined) | undefined,
stringify: ((a: T) => string | undefined) | undefined
@ -37,8 +37,16 @@ export class TypeAttributeKind<T> {
this.stringify = stringify;
}
requiresUniqueIdentity(a: T): boolean {
const ui = this._uniqueIdentity;
if (typeof ui === "boolean") {
return ui;
}
return ui(a);
}
get inIdentity(): boolean {
assert(!this.uniqueIdentity, "inIdentity is invalid for unique identity attributes");
assert(this._uniqueIdentity !== true, "inIdentity is invalid for unique identity attributes");
return this._inIdentity;
}

Просмотреть файл

@ -21,13 +21,13 @@ import {
arrayTypeIdentity,
classTypeIdentity,
unionTypeIdentity,
intersectionTypeIdentity,
stringEnumCasesTypeAttributeKind
intersectionTypeIdentity
} from "./Type";
import { removeNullFromUnion } from "./TypeUtils";
import { TypeGraph } from "./TypeGraph";
import { TypeAttributes, combineTypeAttributes, TypeAttributeKind, emptyTypeAttributes } from "./TypeAttributes";
import { defined, assert, panic, setUnion, mapOptional } from "./Support";
import { stringTypesTypeAttributeKind, StringTypes } from "./StringTypes";
export class TypeRef {
constructor(readonly graph: TypeGraph, readonly index: number) {}
@ -256,12 +256,13 @@ export class TypeBuilder {
if (attributes === undefined) {
attributes = emptyTypeAttributes;
}
let enumCases = kind === "string" ? undefined : null;
// FIXME: Why do date/time types need a StringTypes attribute?
let stringTypes = kind === "string" ? undefined : StringTypes.unrestricted;
if (kind === "date") kind = this._stringTypeMapping.date;
if (kind === "time") kind = this._stringTypeMapping.time;
if (kind === "date-time") kind = this._stringTypeMapping.dateTime;
if (kind === "string") {
return this.getStringType(attributes, enumCases, forwardingRef);
return this.getStringType(attributes, stringTypes, forwardingRef);
}
return this.getOrAddType(
primitiveTypeIdentity(kind, emptyTypeAttributes),
@ -271,20 +272,16 @@ export class TypeBuilder {
);
}
getStringType(
attributes: TypeAttributes,
cases: OrderedMap<string, number> | null | undefined,
forwardingRef?: TypeRef
): TypeRef {
const existingEnumAttribute = attributes.find((_, k) => k === stringEnumCasesTypeAttributeKind);
getStringType(attributes: TypeAttributes, stringTypes: StringTypes | undefined, forwardingRef?: TypeRef): TypeRef {
const existingStringTypes = attributes.find((_, k) => k === stringTypesTypeAttributeKind);
assert(
(cases === undefined) !== (existingEnumAttribute === undefined),
(stringTypes === undefined) !== (existingStringTypes === undefined),
"Must instantiate string type with one enum case attribute"
);
if (existingEnumAttribute === undefined) {
if (existingStringTypes === undefined) {
attributes = combineTypeAttributes(
attributes,
stringEnumCasesTypeAttributeKind.makeAttributes(defined(cases))
stringTypesTypeAttributeKind.makeAttributes(defined(stringTypes))
);
}
return this.getOrAddType(

Просмотреть файл

@ -14,9 +14,9 @@ import {
ClassType,
ClassProperty,
SetOperationType,
UnionType,
stringEnumCasesTypeAttributeKind
UnionType
} from "./Type";
import { stringTypesTypeAttributeKind } from "./StringTypes";
export function assertIsObject(t: Type): ObjectType {
if (t instanceof ObjectType) {
@ -193,14 +193,11 @@ export function directlyReachableSingleNamedType(type: Type): Type | undefined {
export function stringEnumCases(t: PrimitiveType): OrderedMap<string, number> | undefined {
assert(t.kind === "string", "Only strings can be considered enums");
const enumCases = stringEnumCasesTypeAttributeKind.tryGetInAttributes(t.getAttributes());
if (enumCases === undefined) {
const stringTypes = stringTypesTypeAttributeKind.tryGetInAttributes(t.getAttributes());
if (stringTypes === undefined) {
return panic("All strings must have an enum case attribute");
}
if (enumCases === null) {
return undefined;
}
return enumCases;
return stringTypes.cases;
}
export type StringTypeMatchers<U> = {

Просмотреть файл

@ -2,7 +2,7 @@
import { Map, Set, OrderedMap, OrderedSet } from "immutable";
import { TypeKind, PrimitiveStringTypeKind, Type, UnionType, stringEnumCasesTypeAttributeKind } from "./Type";
import { TypeKind, PrimitiveStringTypeKind, Type, UnionType } from "./Type";
import { matchTypeExhaustive } from "./TypeUtils";
import {
TypeAttributes,
@ -12,19 +12,18 @@ import {
} from "./TypeAttributes";
import { defined, assert, panic, assertNever } from "./Support";
import { TypeRef, TypeBuilder } from "./TypeBuilder";
import { MutableStringTypes, StringTypes, stringTypesTypeAttributeKind } from "./StringTypes";
// FIXME: This interface is badly designed. All the properties
// should use immutable types, and getMemberKinds should be
// implementable using the interface, not be part of it. That
// means we'll have to expose primitive types, too.
//
// FIXME: Also, only UnionAccumulator seems to implement it.
// Well, maybe getMemberKinds() is fine as it is.
export interface UnionTypeProvider<TArrayData, TObjectData> {
readonly arrayData: TArrayData;
readonly objectData: TObjectData;
// FIXME: We're losing order here.
enumCaseMap: { [name: string]: number };
enumCases: string[];
readonly stringTypes: StringTypes;
getMemberKinds(): TypeAttributeMap<TypeKind>;
@ -59,15 +58,13 @@ export class UnionAccumulator<TArray, TObject> implements UnionTypeProvider<TArr
private _nonStringTypeAttributes: TypeAttributeMap<TypeKind> = OrderedMap();
private _stringTypeAttributes: TypeAttributeMap<PrimitiveStringTypeKind | "enum"> = OrderedMap();
private _stringTypes: MutableStringTypes = MutableStringTypes.none;
readonly arrayData: TArray[] = [];
readonly objectData: TObject[] = [];
private _lostTypeAttributes: boolean = false;
// FIXME: we're losing order here
enumCaseMap: { [name: string]: number } = {};
enumCases: string[] = [];
constructor(private readonly _conflateNumbers: boolean) {}
private have(kind: TypeKind): boolean {
@ -80,6 +77,10 @@ export class UnionAccumulator<TArray, TObject> implements UnionTypeProvider<TArr
return this.have("string");
}
get stringTypes(): StringTypes {
return this._stringTypes.toImmutable();
}
addNone(_attributes: TypeAttributes): void {
// FIXME: Add them to all members? Or add them to the union, which means we'd have
// to change getMemberKinds() to also return the attributes for the union itself,
@ -114,8 +115,7 @@ export class UnionAccumulator<TArray, TObject> implements UnionTypeProvider<TArr
const newAttributes = addAttributes(oldAttributes, attributes);
this._stringTypeAttributes = this._stringTypeAttributes.clear().set(kind, newAttributes);
this.enumCaseMap = {};
this.enumCases = [];
this._stringTypes.makeUnrestricted();
} else {
this._stringTypeAttributes = setAttributes(this._stringTypeAttributes, kind, attributes);
}
@ -129,25 +129,34 @@ export class UnionAccumulator<TArray, TObject> implements UnionTypeProvider<TArr
this._nonStringTypeAttributes = setAttributes(this._nonStringTypeAttributes, "object", attributes);
}
addEnumCases(cases: OrderedMap<string, number>, attributes: TypeAttributes): void {
private addStringOrEnumCases(
attributes: TypeAttributes,
makeStringTypes: () => StringTypes,
addCases: () => void
): void {
if (this.have("string")) {
const enumAttributes = stringEnumCasesTypeAttributeKind.makeAttributes(cases);
const enumAttributes = stringTypesTypeAttributeKind.makeAttributes(makeStringTypes());
this.addStringType("string", combineTypeAttributes(attributes, enumAttributes));
return;
}
cases.forEach((count, s) => {
if (!Object.prototype.hasOwnProperty.call(this.enumCaseMap, s)) {
this.enumCaseMap[s] = 0;
this.enumCases.push(s);
}
this.enumCaseMap[s] += count;
});
addCases();
this._stringTypeAttributes = setAttributes(this._stringTypeAttributes, "enum", attributes);
}
addEnumCases(cases: string[], attributes: TypeAttributes): void {
this.addStringOrEnumCases(
attributes,
() => StringTypes.fromCases(cases),
() => this._stringTypes.addCases(cases)
);
}
addEnumCase(s: string, count: number, attributes: TypeAttributes): void {
this.addEnumCases(OrderedMap([[s, count] as [string, number]]), attributes);
this.addStringOrEnumCases(
attributes,
() => StringTypes.fromCase(s, count),
() => this._stringTypes.addCase(s, count)
);
}
getMemberKinds(): TypeAttributeMap<TypeKind> {
@ -250,7 +259,7 @@ export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef>
// FIXME: We're not carrying counts, so this is not correct if we do enum
// inference. JSON Schema input uses this case, however, without enum
// inference, which is fine, but still a bit ugly.
enumType => this.addEnumCases(enumType.cases.toOrderedMap().map(_ => 1), attributes),
enumType => this.addEnumCases(enumType.cases.toArray(), attributes),
_unionType => {
return panic("The unions should have been eliminated in attributesForTypesInUnion");
},
@ -271,12 +280,17 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TOb
constructor(protected readonly typeBuilder: TBuilder) {}
protected makeEnum(
cases: string[],
_counts: { [name: string]: number },
stringTypes: StringTypes,
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
return this.typeBuilder.getEnumType(typeAttributes, OrderedSet(cases), forwardingRef);
return this.typeBuilder.getEnumType(
typeAttributes,
defined(stringTypes.cases)
.keySeq()
.toOrderedSet(),
forwardingRef
);
}
protected abstract makeObject(
@ -310,13 +324,13 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TOb
case "string":
return this.typeBuilder.getStringType(
typeAttributes,
typeAttributes.findKey((_, k) => k === stringEnumCasesTypeAttributeKind) !== undefined
? undefined
: null,
stringTypesTypeAttributeKind.tryGetInAttributes(typeAttributes) === undefined
? StringTypes.unrestricted
: undefined,
forwardingRef
);
case "enum":
return this.makeEnum(typeProvider.enumCases, typeProvider.enumCaseMap, typeAttributes, forwardingRef);
return this.makeEnum(typeProvider.stringTypes, typeAttributes, forwardingRef);
case "object":
return this.makeObject(typeProvider.objectData, typeAttributes, forwardingRef);
case "array":