From 70c3024ab88a0f3d335c4f5c591c938be8023bfc Mon Sep 17 00:00:00 2001 From: Roven Gabriel Date: Thu, 30 Oct 2014 15:09:13 +0100 Subject: [PATCH 1/5] Singleton types using String literals types. Compatibility work remaining on function returning different string literals. --- src/compiler/checker.ts | 18 ++++++++++++------ src/compiler/parser.ts | 10 ++++++++++ src/compiler/types.ts | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ae371c41686da..f5eeca0f4cb85 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1646,7 +1646,8 @@ module ts { // Widening of property assignments is handled by checkObjectLiteral, exclude them here if (declaration.kind !== SyntaxKind.PropertyAssignment) { var unwidenedType = type; - type = getWidenedType(type); + // Widen StringLiteralType to stringType when the type is infered + type = getWidenedType(type, /* suppressNoImplicitAnyErrors */ false, /* widenStringLiteral */ true); if (type !== unwidenedType) { checkImplicitAny(type); } @@ -2985,6 +2986,8 @@ module ts { return booleanType; case SyntaxKind.VoidKeyword: return voidType; + case SyntaxKind.StringLiteralType: + return getTypeFromStringLiteral(node); case SyntaxKind.StringLiteral: return getTypeFromStringLiteral(node); case SyntaxKind.TypeReference: @@ -3876,13 +3879,16 @@ module ts { } /* If we are widening on a literal, then we may need to the 'node' parameter for reporting purposes */ - function getWidenedType(type: Type, suppressNoImplicitAnyErrors?: boolean): Type { + function getWidenedType(type: Type, suppressNoImplicitAnyErrors?: boolean, widenStringLiteral?: boolean): Type { if (type.flags & (TypeFlags.Undefined | TypeFlags.Null)) { return anyType; } if (type.flags & TypeFlags.Union) { return getWidenedTypeOfUnion(type); } + if (widenStringLiteral && type.flags & TypeFlags.StringLiteral) { + return stringType; + } if (isTypeOfObjectLiteral(type)) { return getWidenedTypeOfObjectLiteral(type); } @@ -3892,7 +3898,7 @@ module ts { return type; function getWidenedTypeOfUnion(type: Type): Type { - return getUnionType(map((type).types, t => getWidenedType(t, suppressNoImplicitAnyErrors))); + return getUnionType(map((type).types, t => getWidenedType(t, suppressNoImplicitAnyErrors, widenStringLiteral))); } function getWidenedTypeOfObjectLiteral(type: Type): Type { @@ -3902,7 +3908,7 @@ module ts { var propTypeWasWidened: boolean = false; forEach(properties, p => { var propType = getTypeOfSymbol(p); - var widenedType = getWidenedType(propType); + var widenedType = getWidenedType(propType, /* suppressNoImplicitAnyErrors*/ false, widenStringLiteral); if (propType !== widenedType) { propTypeWasWidened = true; if (!suppressNoImplicitAnyErrors && compilerOptions.noImplicitAny && getInnermostTypeOfNestedArrayTypes(widenedType) === anyType) { @@ -3935,7 +3941,7 @@ module ts { function getWidenedTypeOfArrayLiteral(type: Type): Type { var elementType = (type).typeArguments[0]; - var widenedType = getWidenedType(elementType, suppressNoImplicitAnyErrors); + var widenedType = getWidenedType(elementType, suppressNoImplicitAnyErrors, widenStringLiteral); type = elementType !== widenedType ? createArrayType(widenedType) : type; return type; } @@ -6199,7 +6205,7 @@ module ts { case SyntaxKind.NumericLiteral: return numberType; case SyntaxKind.StringLiteral: - return stringType; + return getStringLiteralType((node)); case SyntaxKind.RegularExpressionLiteral: return globalRegExpType; case SyntaxKind.QualifiedName: diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 6c2207a08d22d..ce8dd8a180a1c 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1779,6 +1779,8 @@ module ts { return parseTupleType(); case SyntaxKind.OpenParenToken: return parseParenType(); + case SyntaxKind.StringLiteral: + return parseStringLiteralType(); default: if (isIdentifier()) { return parseTypeReference(); @@ -1800,6 +1802,7 @@ module ts { case SyntaxKind.OpenBracketToken: case SyntaxKind.LessThanToken: case SyntaxKind.NewKeyword: + case SyntaxKind.StringLiteral: return true; case SyntaxKind.OpenParenToken: // Only consider '(' the start of a type if followed by ')', '...', an identifier, a modifier, @@ -1824,6 +1827,13 @@ module ts { return type; } + function parseStringLiteralType() : TypeNode { + var node = createNode(SyntaxKind.StringLiteralType); + node.text = scanner.getTokenValue(); + nextToken(); + return finishNode(node); + } + function parseUnionType(): TypeNode { var type = parsePrimaryType(); if (token === SyntaxKind.BarToken) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6a51691f1ab3b..6dcd17bdf6a3f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -157,6 +157,7 @@ module ts { TupleType, UnionType, ParenType, + StringLiteralType, // Expression ArrayLiteral, ObjectLiteral, From d288ece46155f550a99c0f22a0dacfb29848cb58 Mon Sep 17 00:00:00 2001 From: Roven Gabriel Date: Fri, 31 Oct 2014 17:23:30 +0100 Subject: [PATCH 2/5] Extend type guards to filter objects by literal type properties : type Name = { kind : "name"; name : string }; type Id = { kind : "id"; id : number }; type Obj = Name | Id; var o : obj; if (o.kind === "name") { /* o is considered as Name */ } else { /* o is considered as Obj - Name */ } --- src/compiler/checker.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 647564694fe35..4ff17db78c3df 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4196,6 +4196,14 @@ module ts { return type; } + function keepAssignableTypes(type : Type, targetType : Type, assumeAssignable: boolean): Type { + if(type.flags & TypeFlags.Union) { + var types = (type).types; + return getUnionType(filter(types, t => assumeAssignable ? isTypeAssignableTo(t, targetType) : !isTypeAssignableTo(t, targetType))); + } + return type; + } + // Check if a given variable is assigned within a given syntax node function isVariableAssignedWithin(symbol: Symbol, node: Node): boolean { var links = getNodeLinks(node); @@ -4347,6 +4355,28 @@ module ts { } } + function narrowPropTypeByStringTypeEquality(type : Type, expr: BinaryExpression, assumeTrue: boolean): Type { + var left = expr.left; + var right = expr.right; + var right_t = checkExpression(right); + if (left.kind !== SyntaxKind.PropertyAccess || left.left.kind !== SyntaxKind.Identifier || + !(right_t.flags & TypeFlags.StringLiteral) || + getResolvedSymbol(left.left) !== symbol) { + return type; + } + var t = checkPropertyAccess(left); + var smallerType = t; + if (isTypeAssignableTo(right_t, t)) { + smallerType = right_t; + } + var dummyProperties: SymbolTable = {}; + var dummyProperty = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, left.right.text); + dummyProperty.type = smallerType; + dummyProperties[dummyProperty.name] = dummyProperty; + var dummyType = createAnonymousType(undefined, dummyProperties, emptyArray, emptyArray, undefined, undefined); + return keepAssignableTypes(type, dummyType, assumeTrue); + } + function narrowTypeByAnd(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { if (assumeTrue) { // The assumed result is true, therefore we narrow assuming each operand to be true. @@ -4404,7 +4434,11 @@ module ts { case SyntaxKind.BinaryExpression: var operator = (expr).operator; if (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { - return narrowTypeByEquality(type, expr, assumeTrue); + if((expr).left.kind === SyntaxKind.PropertyAccess) { + return narrowPropTypeByStringTypeEquality(type, expr, assumeTrue); + } else { + return narrowTypeByEquality(type, expr, assumeTrue); + } } else if (operator === SyntaxKind.AmpersandAmpersandToken) { return narrowTypeByAnd(type, expr, assumeTrue); From 5c8c82d79ba2eff3c02033540fa797e5c36d2791 Mon Sep 17 00:00:00 2001 From: Roven Gabriel Date: Mon, 3 Nov 2014 16:29:44 +0100 Subject: [PATCH 3/5] Narrow union types without remaining choices to void --- src/compiler/checker.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4ff17db78c3df..ac5c011982d0a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4200,8 +4200,13 @@ module ts { if(type.flags & TypeFlags.Union) { var types = (type).types; return getUnionType(filter(types, t => assumeAssignable ? isTypeAssignableTo(t, targetType) : !isTypeAssignableTo(t, targetType))); + } else if(isTypeAssignableTo(type, targetType) && assumeAssignable) { + return type; + } else if(!isTypeAssignableTo(type, targetType) && !assumeAssignable) { + return type; } - return type; + + return voidType; } // Check if a given variable is assigned within a given syntax node @@ -4278,7 +4283,7 @@ module ts { } return false; } - } + } // Get the narrowed type of a given symbol at a given location function getNarrowedTypeOfSymbol(symbol: Symbol, node: Node) { From d82f8f5bfc055be04d3bb7e4f69767ad7f35d626 Mon Sep 17 00:00:00 2001 From: Roven Gabriel Date: Mon, 3 Nov 2014 16:38:13 +0100 Subject: [PATCH 4/5] Narrow union types without remaining choices to void. This time taking into account union types and single types sources. --- src/compiler/checker.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ac5c011982d0a..1fafd457d52d6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4199,13 +4199,13 @@ module ts { function keepAssignableTypes(type : Type, targetType : Type, assumeAssignable: boolean): Type { if(type.flags & TypeFlags.Union) { var types = (type).types; - return getUnionType(filter(types, t => assumeAssignable ? isTypeAssignableTo(t, targetType) : !isTypeAssignableTo(t, targetType))); - } else if(isTypeAssignableTo(type, targetType) && assumeAssignable) { - return type; - } else if(!isTypeAssignableTo(type, targetType) && !assumeAssignable) { - return type; + } else { + var types = [type]; + } + var remainingTypes = filter(types, t => assumeAssignable ? isTypeAssignableTo(t, targetType) : !isTypeAssignableTo(t, targetType)); + if(remainingTypes.length > 0) { + return getUnionType(remainingTypes); } - return voidType; } From fd5c4017d7a98e0f035b35b05471c4d8da7aef49 Mon Sep 17 00:00:00 2001 From: Roven Gabriel Date: Wed, 5 Nov 2014 18:33:27 +0100 Subject: [PATCH 5/5] Close a hole in type guards of property access with string literals. Add basic but sound switch statement guards over the theses same property access. --- src/compiler/checker.ts | 72 ++++++++++++++++++++++++++++++++++------- src/compiler/parser.ts | 15 +++++---- src/compiler/types.ts | 1 + 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1fafd457d52d6..8e47d1f30e7dc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4196,13 +4196,20 @@ module ts { return type; } - function keepAssignableTypes(type : Type, targetType : Type, assumeAssignable: boolean): Type { + function keepAssignablePropertyTypes(type : Type, propertyName : string, targetPropertyType : Type, assumeAssignable: boolean): Type { if(type.flags & TypeFlags.Union) { var types = (type).types; } else { var types = [type]; } - var remainingTypes = filter(types, t => assumeAssignable ? isTypeAssignableTo(t, targetType) : !isTypeAssignableTo(t, targetType)); + var remainingTypes = filter(types, t => { + var propertyType = getTypeOfPropertyOfContextualType(t, propertyName); + if(propertyType) { + return assumeAssignable ? isTypeAssignableTo(targetPropertyType, propertyType) : !isTypeAssignableTo(propertyType, targetPropertyType); + } else { + return !assumeAssignable; + } + }); if(remainingTypes.length > 0) { return getUnionType(remainingTypes); } @@ -4322,6 +4329,11 @@ module ts { } } break; + case SyntaxKind.SwitchStatement: + if (child !== (node).expression) { + narrowedType = narrowTypeInCaseClause(type, node, child); + } + break; } // Only use narrowed type if construct contains no assignments to variable if (narrowedType !== type) { @@ -4360,9 +4372,7 @@ module ts { } } - function narrowPropTypeByStringTypeEquality(type : Type, expr: BinaryExpression, assumeTrue: boolean): Type { - var left = expr.left; - var right = expr.right; + function narrowPropTypeByStringTypeEquality(type : Type, left : PropertyAccess, right : Expression, assumeTrue: boolean): Type { var right_t = checkExpression(right); if (left.kind !== SyntaxKind.PropertyAccess || left.left.kind !== SyntaxKind.Identifier || !(right_t.flags & TypeFlags.StringLiteral) || @@ -4374,12 +4384,49 @@ module ts { if (isTypeAssignableTo(right_t, t)) { smallerType = right_t; } - var dummyProperties: SymbolTable = {}; - var dummyProperty = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, left.right.text); - dummyProperty.type = smallerType; - dummyProperties[dummyProperty.name] = dummyProperty; - var dummyType = createAnonymousType(undefined, dummyProperties, emptyArray, emptyArray, undefined, undefined); - return keepAssignableTypes(type, dummyType, assumeTrue); + var propertyName = left.right.text; + return keepAssignablePropertyTypes(type, propertyName, smallerType, assumeTrue); + } + + function narrowTypeInCaseClause(type : Type, switchNode : SwitchStatement, caseClause : CaseOrDefaultClause) : Type { + var propertyAccess = switchNode.expression; + if(switchNode.expression.kind !== SyntaxKind.PropertyAccess || + getResolvedSymbol(propertyAccess.left) !== symbol) { + console.log("return from alien switch for " + symbol.name); + return type; + } + var narrowedType = type; + var remainingType = type; + var typesBeforeBreak : Type[] = []; + for (var i = 0; i < switchNode.clauses.length; i++) { + var clause = switchNode.clauses[i]; + if (clause.expression) { + narrowedType = narrowPropTypeByStringTypeEquality(remainingType, switchNode.expression, clause.expression, /* assumeTrue */ true); + typesBeforeBreak.push(narrowedType); + narrowedType = getUnionType(typesBeforeBreak); + remainingType = narrowPropTypeByStringTypeEquality(remainingType, switchNode.expression, clause.expression, /* assumeTrue */ false); + + } else { + narrowedType = remainingType; + } + console.log("clause id : " + clause.id + " while waiting for " + caseClause.id); + if (clause.id === caseClause.id) { + console.log("returning in clause : " + typeToString(narrowedType)); + return narrowedType; + } + if(clause.statements && clause.statements.length > 0) { + var statements = clause.statements; + var last = statements[statements.length - 1]; + if (last.kind === SyntaxKind.ReturnStatement || + last.kind === SyntaxKind.BreakStatement) { + typesBeforeBreak = []; + } + } + } + + console.log("attained default clause"); + + return narrowedType; } function narrowTypeByAnd(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { @@ -4440,7 +4487,8 @@ module ts { var operator = (expr).operator; if (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { if((expr).left.kind === SyntaxKind.PropertyAccess) { - return narrowPropTypeByStringTypeEquality(type, expr, assumeTrue); + var binary_expr = expr; + return narrowPropTypeByStringTypeEquality(type, binary_expr.left, binary_expr.right, assumeTrue); } else { return narrowTypeByEquality(type, expr, assumeTrue); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index ce8dd8a180a1c..905984322d6c6 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1240,15 +1240,16 @@ module ts { } // Parses a list of elements - function parseList(kind: ParsingContext, checkForStrictMode: boolean, parseElement: () => T): NodeArray { + function parseList(kind: ParsingContext, checkForStrictMode: boolean, parseElement: (i:number) => T): NodeArray { var saveParsingContext = parsingContext; parsingContext |= 1 << kind; var result = >[]; result.pos = getNodePos(); var saveIsInStrictMode = isInStrictMode; + var i = 0; while (!isListTerminator(kind)) { if (isListElement(kind, /* inErrorRecovery */ false)) { - var element = parseElement(); + var element = parseElement(i++); result.push(element); // test elements only if we are not already in strict mode if (!isInStrictMode && checkForStrictMode) { @@ -2993,25 +2994,27 @@ module ts { return node; } - function parseCaseClause(): CaseOrDefaultClause { + function parseCaseClause(i: number): CaseOrDefaultClause { var node = createNode(SyntaxKind.CaseClause); parseExpected(SyntaxKind.CaseKeyword); node.expression = parseExpression(); parseExpected(SyntaxKind.ColonToken); node.statements = parseList(ParsingContext.SwitchClauseStatements, /*checkForStrictMode*/ false, parseStatementAllowingLetDeclaration); + node.id = i; return finishNode(node); } - function parseDefaultClause(): CaseOrDefaultClause { + function parseDefaultClause(i: number): CaseOrDefaultClause { var node = createNode(SyntaxKind.DefaultClause); parseExpected(SyntaxKind.DefaultKeyword); parseExpected(SyntaxKind.ColonToken); node.statements = parseList(ParsingContext.SwitchClauseStatements, /*checkForStrictMode*/ false, parseStatementAllowingLetDeclaration); + node.id = i; return finishNode(node); } - function parseCaseOrDefaultClause(): CaseOrDefaultClause { - return token === SyntaxKind.CaseKeyword ? parseCaseClause() : parseDefaultClause(); + function parseCaseOrDefaultClause(i: number): CaseOrDefaultClause { + return token === SyntaxKind.CaseKeyword ? parseCaseClause(i) : parseDefaultClause(i); } function parseSwitchStatement(): SwitchStatement { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6dcd17bdf6a3f..7644638baa468 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -488,6 +488,7 @@ module ts { export interface CaseOrDefaultClause extends Node { expression?: Expression; statements: NodeArray; + id: number; } export interface LabeledStatement extends Statement {