From acf75c366a0c8dac698422c9224140602ee9dea1 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 25 Jul 2016 18:57:37 -0700 Subject: [PATCH] Add type relationship functions to checker api --- Jakefile.js | 1 + src/compiler/checker.ts | 54 ++++- src/compiler/types.ts | 94 +++++++++ src/harness/tsconfig.json | 1 + .../unittests/checkerPublicRelationships.ts | 195 ++++++++++++++++++ 5 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 src/harness/unittests/checkerPublicRelationships.ts diff --git a/Jakefile.js b/Jakefile.js index a5650a56b16dc..e9e46869bab3c 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -152,6 +152,7 @@ var harnessSources = harnessCoreSources.concat([ "transpile.ts", "reuseProgramStructure.ts", "cachingInServerLSHost.ts", + "checkerPublicRelationships.ts", "moduleResolution.ts", "tsconfigParsing.ts", "commandLineParsing.ts", diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1f2eaea833df0..ca2959956eba3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -103,7 +103,59 @@ namespace ts { getJsxElementAttributesType, getJsxIntrinsicTagNames, - isOptionalParameter + isOptionalParameter, + + isIdenticalTo: (a, b) => checkTypeRelatedTo(a, b, identityRelation, /*errorNode*/undefined), + isSubtypeOf: (a, b) => checkTypeRelatedTo(a, b, subtypeRelation, /*errorNode*/undefined), + isAssignableTo: (a, b) => checkTypeRelatedTo(a, b, assignableRelation, /*errorNode*/undefined), + isComparableTo: areTypesComparable, + isInstantiationOf: (a, b) => { + return a && b && (a.target === b); + }, + + lookupGlobalType: name => { + const symbol = getSymbol(globals, name, SymbolFlags.Type); + return symbol ? getDeclaredTypeOfSymbol(symbol) : unknownType; + }, + lookupGlobalValueType: name => { + const symbol = getSymbol(globals, name, SymbolFlags.Value); + return symbol ? getTypeOfSymbol(symbol) : unknownType; + }, + lookupTypeAt: (name, node) => { + const symbol = resolveName(node, name, SymbolFlags.Type, /*nameNotFoundMessage*/undefined, /*nameArg*/undefined); + return symbol ? getDeclaredTypeOfSymbol(symbol) : unknownType; + }, + lookupValueTypeAt: (name, node) => { + const symbol = resolveName(node, name, SymbolFlags.Value, /*nameNotFoundMessage*/undefined, /*nameArg*/undefined); + return symbol ? getTypeOfSymbol(symbol) : unknownType; + }, + getTypeOfSymbol, + + getAnyType: () => anyType, + getStringType: () => stringType, + getNumberType: () => numberType, + getBooleanType: () => booleanType, + getVoidType: () => voidType, + getUndefinedType: () => undefinedType, + getNullType: () => nullType, + getESSymbolType: () => esSymbolType, + getNeverType: () => neverType, + getUnknownType: () => unknownType, + getStringLiteralType: (text: string) => { + /* tslint:disable:no-null-keyword */ + Debug.assert(text !== undefined && text !== null); + /* tslint:enable:no-null-keyword */ + return getLiteralTypeForText(TypeFlags.StringLiteral, "" + text); + }, + getNumberLiteralType: (text: string) => { + /* tslint:disable:no-null-keyword */ + Debug.assert(text !== undefined && text !== null); + /* tslint:enable:no-null-keyword */ + Debug.assert(typeof text === "string" || typeof text === "number"); // While not formally part of the function signature, allow coercions from numbers + return getLiteralTypeForText(TypeFlags.NumberLiteral, "" + text); + }, + getFalseType: () => falseType, + getTrueType: () => trueType, }; const tupleTypes: Map = {}; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a6e860450c669..fca9e8d228f54 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1872,6 +1872,100 @@ namespace ts { getJsxIntrinsicTagNames(): Symbol[]; isOptionalParameter(node: ParameterDeclaration): boolean; + /** + * Two types are considered identical when + * - they are both the `any` type, + * - they are the same primitive type, + * - they are the same type parameter, + * - they are union types with identical sets of constituent types, or + * - they are intersection types with identical sets of constituent types, or + * - they are object types with identical sets of members. + * + * This relationship is bidirectional. + * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.2) for more information. + */ + isIdenticalTo(a: Type, b: Type): boolean; + /** + * `a` is a ___subtype___ of `b` (and `b` is a ___supertype___ of `a`) if `a` has no excess properties with respect to `b`, + * and one of the following is true: + * - `a` and `b` are identical types. + * - `b` is the `any` type. + * - `a` is the `undefined` type. + * - `a` is the `null` type and `b` is _not_ the `undefined` type. + * - `a` is an enum type and `b` is the primitive type `number`. + * - `a` is a string literal type and `b` is the primitive type `string`. + * - `a` is a union type and each constituient type of `b` is a subtype of `b`. + * - `a` is an intersection type and at least one constituent type of `a` is a subtype of `b`. + * - `b` is a union type and `a` is a subtype of at least one constituent type of `b`. + * - `b` is an intersection type and `a` is a subtype of each constituent type of `b`. + * - `a` is a type parameter and the constraint of `a` is a subtype of `b`. + * - `a` has a subset of the structural members of `b`. + * + * This relationship is directional. + * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.3) for more information. + */ + isSubtypeOf(a: Type, b: Type): boolean; + /** + * The assignable relationship differs only from the subtype relationship in that: + * - the `any` type is assignable to, but not a subtype of, all types + * - the primitive type `number` is assignable to, but not a subtype of, all enum types, and + * - an object type without a particular property is assignable to an object type in which that property is optional. + * + * This relationship is directional. + * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.4) for more information. + */ + isAssignableTo(a: Type, b: Type): boolean; + /** + * True if `a` is assignable to `b`, or `b` is assignable to `a`. Additionally, all unions with + * overlapping constituient types are comparable, and unit types in the same domain are comparable. + * This relationship is bidirectional. + */ + isComparableTo(a: Type, b: Type): boolean; + /** + * Not a formal relationship - returns true if a is an instantiation of the generic type b + */ + isInstantiationOf(a: GenericType, b: GenericType): boolean; + + /** + * Returns the declared type of the globally named symbol with meaning SymbolFlags.Type + * Returns the unknown type on failure. + */ + lookupGlobalType(name: string): Type; + /** + * Returns the declared type of the globally named symbol with meaning SymbolFlags.Value + * Returns the unknown type on failure. + */ + lookupGlobalValueType(name: string): Type; + /** + * Returns the declared type of the named symbol lexically at the position specified with meaning SymbolFlags.Type + * Returns the unknown type on failure. + */ + lookupTypeAt(name: string, position: Node): Type; + /** + * Returns the declared type of the named symbol lexically at the position specified with meaning SymbolFlags.Value + * Returns the unknown type on failure. + */ + lookupValueTypeAt(name: string, position: Node): Type; + /** + * Returns the type of a symbol + */ + getTypeOfSymbol(symbol: Symbol): Type; + + getAnyType(): Type; + getStringType(): Type; + getNumberType(): Type; + getBooleanType(): Type; + getVoidType(): Type; + getUndefinedType(): Type; + getNullType(): Type; + getESSymbolType(): Type; + getNeverType(): Type; + getUnknownType(): Type; + getStringLiteralType(text: string): LiteralType; + getNumberLiteralType(text: string): LiteralType; + getFalseType(): Type; + getTrueType(): Type; + // Should not be called directly. Should only be accessed through the Program instance. /* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[]; /* @internal */ getGlobalDiagnostics(): Diagnostic[]; diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 3b9025c27c362..54b32e4f4cb34 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -83,6 +83,7 @@ "./unittests/transpile.ts", "./unittests/reuseProgramStructure.ts", "./unittests/cachingInServerLSHost.ts", + "./unittests/checkerPublicRelationships.ts", "./unittests/moduleResolution.ts", "./unittests/tsconfigParsing.ts", "./unittests/commandLineParsing.ts", diff --git a/src/harness/unittests/checkerPublicRelationships.ts b/src/harness/unittests/checkerPublicRelationships.ts new file mode 100644 index 0000000000000..c4b1ef895c6a2 --- /dev/null +++ b/src/harness/unittests/checkerPublicRelationships.ts @@ -0,0 +1,195 @@ +/// +/// + +namespace ts { + describe("Type Checker Public Relationship APIs", () => { + let checker: TypeChecker; + let host: CompilerHost; + let program: Program; + before(() => { + host = Harness.Compiler.createCompilerHost([{ + unitName: "test.ts", + content: ` + type FunctionAlias = Function; + function foo() { + type Function = { myBrand: 42 } & FunctionAlias; + return (() => {}) as any as Function; + } + function foo2(x: T) { + type Function = { myBrand: T } & FunctionAlias; + const ret = (() => {}) as any as Function; + ret.myBrand = x; + return ret; + } + const xs: number[] = [1,2,3]; + ` + }], () => void 0, ScriptTarget.ES3, /*useCaseSensitiveFileNames*/true, "", NewLineKind.CarriageReturnLineFeed); + program = ts.createProgram(["test.ts"], ts.defaultInitCompilerOptions, host); + const diag = ts.getPreEmitDiagnostics(program); + if (diag.length) { + const errors = ts.formatDiagnostics(diag, host); + console.log(errors); + } + checker = program.getTypeChecker(); + }); + + it("can get the any type", () => { + assert(checker.getAnyType().flags & TypeFlags.Any); + }); + + it("can get the string type", () => { + assert(checker.getStringType().flags & TypeFlags.String); + }); + + it("can get the number type", () => { + assert(checker.getNumberType().flags & TypeFlags.Number); + }); + + it("can get the boolean type", () => { + assert(checker.getBooleanType().flags & TypeFlags.Boolean); + }); + + it("can get the void type", () => { + assert(checker.getVoidType().flags & TypeFlags.Void); + }); + + it("can get the undefined type", () => { + assert(checker.getUndefinedType().flags & TypeFlags.Undefined); + }); + + it("can get the null type", () => { + assert(checker.getNullType().flags & TypeFlags.Null); + }); + + it("can get the essymbol type", () => { + assert(checker.getESSymbolType().flags & TypeFlags.ESSymbol); + }); + + it("can get the never type", () => { + assert(checker.getNeverType().flags & TypeFlags.Never); + }); + + it("can get the unknown type", () => { + assert(checker.getUnknownType().flags & TypeFlags.Any); + assert(checker.getUnknownType() === checker.getUnknownType()); + }); + + it("can get the true type", () => { + assert(checker.getTrueType().flags & TypeFlags.BooleanLiteral); + }); + + it("can get the false type", () => { + assert(checker.getFalseType().flags & TypeFlags.BooleanLiteral); + }); + + it("ensures true and false are different types", () => { + assert(checker.getFalseType() !== checker.getTrueType()); + }); + + it("can get string literal types", () => { + assert((checker.getStringLiteralType("foobar") as LiteralType).text === "foobar"); + }); + + it("can get numeber literal types", () => { + assert((checker.getNumberLiteralType("42") as LiteralType).text === "42"); + }); + + it("doesn't choke on exceptional input to literal type getters", () => { + assert.equal((checker.getStringLiteralType("") as LiteralType).text, ""); + assert.throws(() => checker.getStringLiteralType(undefined), Error, "Debug Failure. False expression:"); + /* tslint:disable:no-null-keyword */ + assert.throws(() => checker.getStringLiteralType(null), Error, "Debug Failure. False expression:"); + /* tslint:enable:no-null-keyword */ + let hugeStringLiteral = map(new Array(2 ** 16 - 1), () => "a").join(); + assert.equal((checker.getStringLiteralType(hugeStringLiteral) as LiteralType).text, hugeStringLiteral); + hugeStringLiteral = undefined; + + + assert.throws(() => checker.getNumberLiteralType(undefined), Error, "Debug Failure. False expression:"); + /* tslint:disable:no-null-keyword */ + assert.throws(() => checker.getNumberLiteralType(null), Error, "Debug Failure. False expression:"); + /* tslint:enable:no-null-keyword */ + + const sanityChecks = ["000", "0b0", "0x0", "0.0", "0e-0", "-010", "-0b10", "-0x10", "-0o10", "-10.0", "-1e-1", "NaN", "Infinity", "-Infinity"]; + forEach(sanityChecks, num => { + assert.equal((checker.getNumberLiteralType(num) as LiteralType).text, num, `${num} did not match.`); + }); + + const insanityChecks = [[0, "0"], [0b0, "0"], [-10, "-10"], [NaN, "NaN"], [Infinity, "Infinity"], [-Infinity, "-Infinity"]]; + forEach(insanityChecks, ([num, expected]) => { + assert.equal((checker.getNumberLiteralType(num as any) as LiteralType).text, expected, `${JSON.stringify(num)} should be ${expected}`); + }); + + const instabilityChecks = [{ foo: 42 }, new Date(42), [42], new Number(42), new String("42")]; + forEach(instabilityChecks, (bad) => { + assert.throws(() => checker.getNumberLiteralType(bad as any)); + }); + }); + + it("can look up global types", () => { + assert.equal(checker.lookupGlobalType("Array").symbol.name, "Array", "Array global symbol not named Array"); + const globalFunction = checker.lookupGlobalType("Function"); + const globalAlias = checker.lookupGlobalType("FunctionAlias"); + assert.notEqual(globalFunction, checker.getUnknownType(), "The global function type should not be the unknown type"); + assert.notEqual(globalAlias, checker.getUnknownType(), "The global alias function type should not be the unknown type"); + const globalFunctionLength = globalFunction.getProperty("length"); + const aliasFunctionLength = globalAlias.getProperty("length"); + assert(globalFunctionLength, "Global function length symbol should exist"); + assert(aliasFunctionLength, "Alias function length symbol should exist"); + assert.notEqual(checker.getTypeOfSymbol(globalFunctionLength), checker.getUnknownType(), "The global function's length type should not be unknown"); + assert.notEqual(checker.getTypeOfSymbol(aliasFunctionLength), checker.getUnknownType(), "The alias function's length type should not be unknown"); + assert.equal(checker.getTypeOfSymbol(globalFunctionLength), checker.getTypeOfSymbol(aliasFunctionLength), "Alias and global function length were not identical types"); + assert.equal((checker.getTypeOfSymbol(globalFunctionLength) as IntrinsicType).intrinsicName, (checker.getNumberType() as IntrinsicType).intrinsicName, "Function length was not number type"); + }); + + it("can look up types in a given scope", () => { + assert(program.getSourceFile("test.ts"), "Test file not found"); + const functionBody = forEachChild(program.getSourceFile("test.ts"), node => node.kind === SyntaxKind.FunctionDeclaration ? (node as FunctionDeclaration) : undefined).body; + assert(functionBody, "Function body missing"); + const innerFunction = checker.lookupTypeAt("Function", functionBody.statements[functionBody.statements.length - 1]); + assert(innerFunction, "Inner function type missing"); + assert.notEqual(innerFunction, checker.getUnknownType(), "Inner function type should not be unknown"); + assert.notEqual(checker.lookupGlobalType("Function"), innerFunction, "Inner function type should be different than global"); + const brandNameType = checker.getTypeOfSymbol(innerFunction.getProperty("myBrand")); + assert.notEqual(brandNameType, checker.getUnknownType(), "Brand type on inner function should not be unknown"); + assert.equal(brandNameType, checker.getNumberLiteralType("42"), "Brand type should be 42"); + + let skipped = false; + const functionBody2 = forEachChild(program.getSourceFile("test.ts"), node => node.kind === SyntaxKind.FunctionDeclaration ? skipped ? (node as FunctionDeclaration) : (skipped = true, undefined) : undefined).body; + assert(functionBody2, "Function body missing"); + const innerFunction2 = checker.lookupTypeAt("Function", functionBody2.statements[functionBody2.statements.length - 1]); + assert(innerFunction2, "Inner function type missing"); + assert.notEqual(innerFunction2, checker.getUnknownType(), "Inner function type should not be unknown"); + assert.notEqual(checker.lookupGlobalType("Function"), innerFunction2, "Inner function type should be different than global"); + const brandNameType2 = checker.getTypeOfSymbol(innerFunction2.getProperty("myBrand")); + assert.notEqual(brandNameType2, checker.getUnknownType(), "Brand type on inner function should not be unknown"); + const functionType = checker.lookupGlobalValueType("foo2"); + assert.notEqual(functionType, checker.getUnknownType(), "foo2 function type should not be unknown"); + assert(brandNameType2.flags & TypeFlags.TypeParameter, "Brand should be a type parameter"); + assert.equal(brandNameType2, checker.lookupGlobalValueType("foo2").getCallSignatures()[0].getTypeParameters()[0], "Brand type should be a type parameter"); + }); + + it("can compare types using all the builtin relationships", () => { + assert(checker.isSubtypeOf(checker.getNumberType(), checker.getAnyType()), "Any should be a subtype of number"); + assert.isFalse(checker.isSubtypeOf(checker.getAnyType(), checker.getNumberType()), "Number should not be a subtype of any"); + + assert(checker.isAssignableTo(checker.getAnyType(), checker.getNumberType()), "Any should be assignable to number"); + assert(checker.isAssignableTo(checker.getFalseType(), checker.getBooleanType()), "False should be assignable to boolean"); + + assert(checker.isComparableTo(checker.getBooleanType(), checker.getFalseType()), "False and boolean are comparable"); + assert(checker.isComparableTo(checker.getFalseType(), checker.getBooleanType()), "Boolean and false are comparable"); + + const variableType = checker.lookupGlobalValueType("xs"); + const globalArrayType = checker.lookupGlobalType("Array"); + assert.notEqual(variableType, checker.getUnknownType(), "xs type should not be unknown"); + assert.notEqual(globalArrayType, checker.getUnknownType(), "Global array type should not be unknown"); + assert(checker.isInstantiationOf(checker.lookupGlobalValueType("xs") as GenericType, checker.lookupGlobalType("Array") as GenericType)); + }); + + after(() => { + checker = undefined; + host = undefined; + program = undefined; + }); + }); +} \ No newline at end of file