Skip to content

Commit

Permalink
Add type relationship functions to checker api
Browse files Browse the repository at this point in the history
  • Loading branch information
weswigham committed Aug 10, 2016
1 parent 3f1ec7a commit 806ffe1
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 1 deletion.
1 change: 1 addition & 0 deletions Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ var harnessSources = harnessCoreSources.concat([
"transpile.ts",
"reuseProgramStructure.ts",
"cachingInServerLSHost.ts",
"checkerPublicRelationships.ts",
"moduleResolution.ts",
"tsconfigParsing.ts",
"commandLineParsing.ts",
Expand Down
54 changes: 53 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TupleType> = {};
Expand Down
94 changes: 94 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.Type
* 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.Type
* 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[];
Expand Down
1 change: 1 addition & 0 deletions src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
187 changes: 187 additions & 0 deletions src/harness/unittests/checkerPublicRelationships.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/// <reference path="..\harness.ts" />
/// <reference path="..\..\harness\harnessLanguageService.ts" />

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<T>(x: T) {
type Function = { myBrand: T } & FunctionAlias;
const ret = (() => {}) as any as Function;
ret.myBrand = x;
return ret;
}`
}], () => 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");
});

after(() => {
checker = undefined;
host = undefined;
program = undefined;
});
});
}

0 comments on commit 806ffe1

Please sign in to comment.