inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
372 lines (349 loc) • 10.9 kB
text/typescript
import * as tsutils from "tsutils/typeguard/3.0";
import ts from "typescript";
import { sliceSet } from "./utils";
import type { VisitorContext } from "./visitor-context";
import * as VisitorIsNumber from "./visitor-is-number";
import * as VisitorIsString from "./visitor-is-string";
import * as VisitorTypeCheck from "./visitor-type-check";
import * as VisitorTypeName from "./visitor-type-name";
import * as VisitorUtils from "./visitor-utils";
function visitRegularObjectType(
type: ts.ObjectType,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
const name = VisitorTypeName.visitType(type, visitorContext, {
type: "indexed-access",
indexType,
});
return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => {
// TODO: check property index
// const stringIndexType = visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.String);
const properties = visitorContext.checker.getPropertiesOfType(type);
const propertiesInfo = properties.map((property) =>
VisitorUtils.getPropertyInfo(type, property, visitorContext),
);
const stringType = VisitorIsString.visitType(indexType, visitorContext);
if (typeof stringType === "boolean") {
if (!stringType) {
throw new Error(
"A non-string type was used to index an object type.",
);
}
const functionNames = propertiesInfo.map((propertyInfo) =>
propertyInfo.isMethod
? VisitorUtils.getIgnoredTypeFunction(visitorContext)
: VisitorTypeCheck.visitType(
propertyInfo.type!,
visitorContext,
),
);
return VisitorUtils.createDisjunctionFunction(
functionNames,
name,
visitorContext,
);
} else {
const strings = sliceSet(stringType);
if (
strings.some((value) =>
propertiesInfo.every(
(propertyInfo) => propertyInfo.name !== value,
),
)
) {
throw new Error(
"Indexed access on object type with an index that does not exist.",
);
}
const stringPropertiesInfo = strings.map(
(value) =>
propertiesInfo.find(
(propertyInfo) => propertyInfo.name === value,
)!,
);
const functionNames = stringPropertiesInfo.map((propertyInfo) =>
propertyInfo.isMethod
? VisitorUtils.getIgnoredTypeFunction(visitorContext)
: VisitorTypeCheck.visitType(
propertyInfo.type!,
visitorContext,
),
);
return VisitorUtils.createDisjunctionFunction(
functionNames,
name,
visitorContext,
);
}
});
}
function visitTupleObjectType(
type: ts.TupleType,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
const name = VisitorTypeName.visitType(type, visitorContext, {
type: "indexed-access",
indexType,
});
return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => {
if (type.typeArguments === undefined) {
throw new Error("Expected tuple type to have type arguments.");
}
const numberType = VisitorIsNumber.visitType(indexType, visitorContext);
if (typeof numberType === "boolean") {
if (!numberType) {
throw new Error(
"A non-number type was used to index a tuple type.",
);
}
const functionNames = type.typeArguments.map((type) =>
VisitorTypeCheck.visitType(type, visitorContext),
);
return VisitorUtils.createDisjunctionFunction(
functionNames,
name,
visitorContext,
);
} else {
const numbers = sliceSet(numberType);
if (numbers.some((value) => value >= type.typeArguments!.length)) {
throw new Error(
"Indexed access on tuple type exceeds length of tuple.",
);
}
const functionNames = numbers.map((value) =>
VisitorTypeCheck.visitType(
type.typeArguments![value],
visitorContext,
),
);
return VisitorUtils.createDisjunctionFunction(
functionNames,
name,
visitorContext,
);
}
});
}
function visitArrayObjectType(
type: ts.ObjectType,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
const numberIndexType = visitorContext.checker.getIndexTypeOfType(
type,
ts.IndexKind.Number,
);
if (numberIndexType === undefined) {
throw new Error(
"Expected array ObjectType to have a number index type.",
);
}
const numberType = VisitorIsNumber.visitType(indexType, visitorContext);
if (numberType !== false) {
return VisitorTypeCheck.visitType(numberIndexType, visitorContext);
} else {
throw new Error("A non-number type was used to index an array type.");
}
}
function visitObjectType(
type: ts.ObjectType,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
if (tsutils.isTupleType(type)) {
// Tuple with finite length.
return visitTupleObjectType(type, indexType, visitorContext);
} else if (
visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number)
) {
// Index type is number -> array type.
return visitArrayObjectType(type, indexType, visitorContext);
} else {
// Index type is string -> regular object type.
return visitRegularObjectType(type, indexType, visitorContext);
}
}
function visitUnionOrIntersectionType(
type: ts.UnionOrIntersectionType,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
const name = VisitorTypeName.visitType(type, visitorContext, {
type: "indexed-access",
indexType,
});
return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => {
const functionNames = type.types.map((type) =>
visitType(type, indexType, visitorContext),
);
if (tsutils.isUnionType(type)) {
// (T | U)[I] = T[I] & U[I]
return VisitorUtils.createConjunctionFunction(functionNames, name);
} else {
// (T & U)[I] = T[I] | U[I]
return VisitorUtils.createDisjunctionFunction(
functionNames,
name,
visitorContext,
);
}
});
}
function visitIndexType(): string {
// (keyof U)[T] is an error (actually it can be String.toString or String.valueOf but we don't support this edge case)
throw new Error("Index types cannot be used as indexed types.");
}
function visitNonPrimitiveType(): string {
// object[T] is an error
throw new Error("Non-primitive object cannot be used as an indexed type.");
}
function visitLiteralType(): string {
// 'string'[T] or 0xFF[T] is an error
throw new Error(
"Literal strings/numbers cannot be used as an indexed type.",
);
}
function visitTypeReference(
type: ts.TypeReference,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
const mapping: Map<ts.Type, ts.Type> = VisitorUtils.getTypeReferenceMapping(
type,
visitorContext,
);
const previousTypeReference = visitorContext.previousTypeReference;
visitorContext.typeMapperStack.push(mapping);
visitorContext.previousTypeReference = type;
const result = visitType(type.target, indexType, visitorContext);
visitorContext.previousTypeReference = previousTypeReference;
visitorContext.typeMapperStack.pop();
return result;
}
function visitTypeParameter(
type: ts.Type,
indexType: ts.Type,
visitorContext: VisitorContext,
) {
const mappedType = VisitorUtils.getResolvedTypeParameter(
type,
visitorContext,
);
if (mappedType === undefined) {
throw new Error("Unbound type parameter, missing type node.");
}
return visitType(mappedType, indexType, visitorContext);
}
function visitBigInt(): string {
// bigint[T] is an error
throw new Error("BigInt cannot be used as an indexed type.");
}
function visitBoolean(): string {
// boolean[T] is an error
throw new Error("Boolean cannot be used as an indexed type.");
}
function visitString(): string {
// string[T] is an error
throw new Error("String cannot be used as an indexed type.");
}
function visitBooleanLiteral(): string {
// true[T] or false[T] is an error
throw new Error("True/false cannot be used as an indexed type.");
}
function visitNumber(): string {
// number[T] is an error
throw new Error("Number cannot be used as an indexed type.");
}
function visitUndefined(): string {
// undefined[T] is an error
throw new Error("Undefined cannot be used as an indexed type.");
}
function visitNull(): string {
// null[T] is an error
throw new Error("Null cannot be used as an indexed type.");
}
function visitNever(visitorContext: VisitorContext) {
// never[T] = never
return VisitorUtils.getNeverFunction(visitorContext);
}
function visitUnknown(visitorContext: VisitorContext) {
// unknown[T] = unknown
return VisitorUtils.getUnknownFunction(visitorContext);
}
function visitAny(visitorContext: VisitorContext) {
// any[T] = any
return VisitorUtils.getAnyFunction(visitorContext);
}
export function visitType(
type: ts.Type,
indexType: ts.Type,
visitorContext: VisitorContext,
): string {
if ((ts.TypeFlags.Any & type.flags) !== 0) {
// Any
return visitAny(visitorContext);
} else if ((ts.TypeFlags.Unknown & type.flags) !== 0) {
// Unknown
return visitUnknown(visitorContext);
} else if ((ts.TypeFlags.Never & type.flags) !== 0) {
// Never
return visitNever(visitorContext);
} else if ((ts.TypeFlags.Null & type.flags) !== 0) {
// Null
return visitNull();
} else if ((ts.TypeFlags.Undefined & type.flags) !== 0) {
// Undefined
return visitUndefined();
} else if ((ts.TypeFlags.Number & type.flags) !== 0) {
// Number
return visitNumber();
} else if (VisitorUtils.isBigIntType(type)) {
// BigInt
return visitBigInt();
} else if ((ts.TypeFlags.Boolean & type.flags) !== 0) {
// Boolean
return visitBoolean();
} else if ((ts.TypeFlags.String & type.flags) !== 0) {
// String
return visitString();
} else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) {
// Boolean literal (true/false)
return visitBooleanLiteral();
} else if (
tsutils.isTypeReference(type) &&
visitorContext.previousTypeReference !== type
) {
// Type references.
return visitTypeReference(type, indexType, visitorContext);
} else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) {
// Type parameter
return visitTypeParameter(type, indexType, visitorContext);
} else if (tsutils.isObjectType(type)) {
// Object type (including interfaces, arrays, tuples)
return visitObjectType(type, indexType, visitorContext);
} else if (tsutils.isLiteralType(type)) {
// Literal string/number types ('foo')
return visitLiteralType();
} else if (tsutils.isUnionOrIntersectionType(type)) {
// Union or intersection type (| or &)
return visitUnionOrIntersectionType(type, indexType, visitorContext);
} else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) {
// Non-primitive such as object
return visitNonPrimitiveType();
} else if ((ts.TypeFlags.Index & type.flags) !== 0) {
// Index type: keyof T
return visitIndexType();
} else if (tsutils.isIndexedAccessType(type)) {
// Indexed access type: T[U]
// return visitIndexedAccessType(type, visitorContext);
// TODO:
throw new Error("Not yet implemented.");
} else {
throw new Error(
`Could not generate type-check; unsupported type with flags: ${type.flags}`,
);
}
}