-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Infer string literals at comparison locations #6196
Changes from 10 commits
0d2fb26
639d9bf
13ec5d7
e109b17
de9789a
d0a8573
e452955
ced65ac
881b52d
8365410
58580ed
069ff33
2874156
e2ddb29
f7e9135
e6bd7ad
16fe01b
bc34ebb
4eced90
01cc2f1
259a3cf
cc2ab55
5b9e5d1
fdd7fde
09d1762
dec70f1
1dd43fa
ab25584
9673152
6b8bac3
740792c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7337,11 +7337,13 @@ namespace ts { | |
function getContextualTypeForBinaryOperand(node: Expression): Type { | ||
const binaryExpression = <BinaryExpression>node.parent; | ||
const operator = binaryExpression.operatorToken.kind; | ||
if (operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment) { | ||
|
||
if (SyntaxKind.FirstAssignment <= operator && operator <= SyntaxKind.LastAssignment) { | ||
// In an assignment expression, the right operand is contextually typed by the type of the left operand. | ||
if (node === binaryExpression.right) { | ||
return checkExpression(binaryExpression.left); | ||
} | ||
return undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably don't need this |
||
} | ||
else if (operator === SyntaxKind.BarBarToken) { | ||
// When an || expression has a contextual type, the operands are contextually typed by that type. When an || | ||
|
@@ -7352,6 +7354,7 @@ namespace ts { | |
} | ||
return type; | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
|
@@ -7509,6 +7512,7 @@ namespace ts { | |
* @returns the contextual type of an expression. | ||
*/ | ||
function getContextualType(node: Expression): Type { | ||
|
||
if (isInsideWithStatementBody(node)) { | ||
// We cannot answer semantic questions within a with block, do not proceed any further | ||
return undefined; | ||
|
@@ -7555,6 +7559,62 @@ namespace ts { | |
return undefined; | ||
} | ||
|
||
function shouldAcquireLiteralType(literalNode: StringLiteral) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative way to structure this is to have an |
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove whitespace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And elsewhere in this file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whitespace here and below is intentional and it's for readability so that everything doesn't look like one big clump of code. |
||
let current: Node = literalNode; | ||
while (true) { | ||
const { parent } = current; | ||
switch (parent.kind) { | ||
// The operand of a 'switch' should get a literal type. | ||
case SyntaxKind.SwitchStatement: | ||
return current === (parent as SwitchStatement).expression; | ||
|
||
// The tested expression of a 'case' clause should get a literal type. | ||
case SyntaxKind.CaseClause: | ||
return current === (parent as CaseClause).expression; | ||
|
||
case SyntaxKind.BinaryExpression: | ||
const binaryExpr = parent as BinaryExpression; | ||
switch (binaryExpr.operatorToken.kind) { | ||
// Either operand of an equality/inequality comparison | ||
// should get a literal type. | ||
case SyntaxKind.EqualsEqualsEqualsToken: | ||
case SyntaxKind.ExclamationEqualsEqualsToken: | ||
case SyntaxKind.EqualsEqualsToken: | ||
case SyntaxKind.ExclamationEqualsToken: | ||
return current === binaryExpr.left || current === binaryExpr.right; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't think |
||
|
||
case SyntaxKind.BarBarToken: | ||
current = parent; | ||
continue; | ||
|
||
} | ||
break; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just break from the loop directly (using a label). If you do that you can put the break in a default clause. Then you can remove the final break at the bottom. Then you can convert the continues to breaks. And that will allow you to put There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally had it that way, but I thought it would be too indirect. I could go either way on the style, so I'll just change it back. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like Jason's idea, so that's two votes for. |
||
|
||
case SyntaxKind.ConditionalExpression: | ||
case SyntaxKind.ParenthesizedExpression: | ||
current = parent; | ||
continue; | ||
} | ||
|
||
// This is not a node we can account for. | ||
// Let contextual typing take over. | ||
break; | ||
} | ||
|
||
// We haven't found a "literal match location" (i.e. a location that signals | ||
// a literal should get a literal type). Check whether the contextual type | ||
// has a literal type in it. | ||
// | ||
// We could perform our walk, check if 'current' is an expression when we get to a | ||
// a node that isn't a match location, and then get the contextual type of that. | ||
// That would save a few steps but the checks in 'isExpression' seem so involved | ||
// that it would probably be better to simply grab the contextual type if we didn't. | ||
const contextualType = getContextualType(literalNode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I assumed too much when I wrote the above comment. Additionally, you need to because we just skipped There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see what you mean now about isExpression. And good point about |
||
return !!contextualType && contextualTypeIsStringLiteralType(contextualType); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is it necessary to do this? What is an unsupported node? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You still want to get the contextual type in case you didn't hit a I'll add a comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I get it now, thanks. My question was, why does the contextual type matter at all? And now I realize you do still care whether the contextual type specifies a string literal, for basic cases like this: var s: "hello" = "hello"; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's exactly it |
||
} | ||
|
||
|
||
// If the given type is an object or union type, if that type has a single signature, and if | ||
// that signature is non-generic, return the signature. Otherwise return undefined. | ||
function getNonGenericSignature(type: Type): Signature { | ||
|
@@ -9869,11 +9929,7 @@ namespace ts { | |
if (produceDiagnostics && targetType !== unknownType) { | ||
const widenedType = getWidenedType(exprType); | ||
|
||
// Permit 'number[] | "foo"' to be asserted to 'string'. | ||
const bothAreStringLike = | ||
someConstituentTypeHasKind(targetType, TypeFlags.StringLike) && | ||
someConstituentTypeHasKind(widenedType, TypeFlags.StringLike); | ||
if (!bothAreStringLike && !(isTypeAssignableTo(targetType, widenedType))) { | ||
if (!isTypeAssignableTo(targetType, widenedType)) { | ||
checkTypeAssignableTo(exprType, targetType, node, Diagnostics.Neither_type_0_nor_type_1_is_assignable_to_the_other); | ||
} | ||
} | ||
|
@@ -10723,10 +10779,6 @@ namespace ts { | |
case SyntaxKind.ExclamationEqualsToken: | ||
case SyntaxKind.EqualsEqualsEqualsToken: | ||
case SyntaxKind.ExclamationEqualsEqualsToken: | ||
// Permit 'number[] | "foo"' to be asserted to 'string'. | ||
if (someConstituentTypeHasKind(leftType, TypeFlags.StringLike) && someConstituentTypeHasKind(rightType, TypeFlags.StringLike)) { | ||
return booleanType; | ||
} | ||
if (!isTypeAssignableTo(leftType, rightType) && !isTypeAssignableTo(rightType, leftType)) { | ||
reportOperatorError(); | ||
} | ||
|
@@ -10866,8 +10918,7 @@ namespace ts { | |
} | ||
|
||
function checkStringLiteralExpression(node: StringLiteral): Type { | ||
const contextualType = getContextualType(node); | ||
if (contextualType && contextualTypeIsStringLiteralType(contextualType)) { | ||
if (shouldAcquireLiteralType(node)) { | ||
return getStringLiteralTypeForText(node.text); | ||
} | ||
|
||
|
@@ -13249,7 +13300,6 @@ namespace ts { | |
let hasDuplicateDefaultClause = false; | ||
|
||
const expressionType = checkExpression(node.expression); | ||
const expressionTypeIsStringLike = someConstituentTypeHasKind(expressionType, TypeFlags.StringLike); | ||
forEach(node.caseBlock.clauses, clause => { | ||
// Grammar check for duplicate default clauses, skip if we already report duplicate default clause | ||
if (clause.kind === SyntaxKind.DefaultClause && !hasDuplicateDefaultClause) { | ||
|
@@ -13271,12 +13321,7 @@ namespace ts { | |
// In a 'switch' statement, each 'case' expression must be of a type that is assignable to or from the type of the 'switch' expression. | ||
const caseType = checkExpression(caseClause.expression); | ||
|
||
const expressionTypeIsAssignableToCaseType = | ||
// Permit 'number[] | "foo"' to be asserted to 'string'. | ||
(expressionTypeIsStringLike && someConstituentTypeHasKind(caseType, TypeFlags.StringLike)) || | ||
isTypeAssignableTo(expressionType, caseType); | ||
|
||
if (!expressionTypeIsAssignableToCaseType) { | ||
if (!isTypeAssignableTo(expressionType, caseType)) { | ||
// 'expressionType is not assignable to caseType', try the reversed check and report errors if it fails | ||
checkTypeAssignableTo(caseType, expressionType, caseClause.expression, /*headMessage*/ undefined); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
tests/cases/conformance/types/stringLiteral/stringLiteralTypeAssertion01.ts(22,5): error TS2352: Neither type 'string' nor type '("a" | "b")[] | "a" | "b"' is assignable to the other. | ||
Type 'string' is not assignable to type '"b"'. | ||
tests/cases/conformance/types/stringLiteral/stringLiteralTypeAssertion01.ts(23,5): error TS2352: Neither type 'string' nor type '("a" | "b")[] | "a" | "b"' is assignable to the other. | ||
Type 'string' is not assignable to type '"b"'. | ||
tests/cases/conformance/types/stringLiteral/stringLiteralTypeAssertion01.ts(30,7): error TS2352: Neither type '("a" | "b")[] | "a" | "b"' nor type 'string' is assignable to the other. | ||
Type '("a" | "b")[]' is not assignable to type 'string'. | ||
tests/cases/conformance/types/stringLiteral/stringLiteralTypeAssertion01.ts(31,7): error TS2352: Neither type '("a" | "b")[] | "a" | "b"' nor type 'string' is assignable to the other. | ||
Type '("a" | "b")[]' is not assignable to type 'string'. | ||
|
||
|
||
==== tests/cases/conformance/types/stringLiteral/stringLiteralTypeAssertion01.ts (4 errors) ==== | ||
|
||
type S = "a" | "b"; | ||
type T = S[] | S; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it would be easier to read to use the naming scheme type Ab = "a" | "b";
type AbOrAbs = Ab[] | Ab;
var ab: Ab; ... |
||
|
||
var s: S; | ||
var t: T; | ||
var str: string; | ||
|
||
//////////////// | ||
|
||
s = <S>t; | ||
s = t as S; | ||
|
||
s = <S>str; | ||
s = str as S; | ||
|
||
//////////////// | ||
|
||
t = <T>s; | ||
t = s as T; | ||
|
||
t = <T>str; | ||
~~~~~~ | ||
!!! error TS2352: Neither type 'string' nor type '("a" | "b")[] | "a" | "b"' is assignable to the other. | ||
!!! error TS2352: Type 'string' is not assignable to type '"b"'. | ||
t = str as T; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to test two different syntaxes to make sure they behave the same? It seems like it's not the point of this test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I don't see why it's a problem to have more coverage given that subtle inconsistencies are found all the time. |
||
~~~~~~~~ | ||
!!! error TS2352: Neither type 'string' nor type '("a" | "b")[] | "a" | "b"' is assignable to the other. | ||
!!! error TS2352: Type 'string' is not assignable to type '"b"'. | ||
|
||
//////////////// | ||
|
||
str = <string>s; | ||
str = s as string; | ||
|
||
str = <string>t; | ||
~~~~~~~~~ | ||
!!! error TS2352: Neither type '("a" | "b")[] | "a" | "b"' nor type 'string' is assignable to the other. | ||
!!! error TS2352: Type '("a" | "b")[]' is not assignable to type 'string'. | ||
str = t as string; | ||
~~~~~~~~~~~ | ||
!!! error TS2352: Neither type '("a" | "b")[] | "a" | "b"' nor type 'string' is assignable to the other. | ||
!!! error TS2352: Type '("a" | "b")[]' is not assignable to type 'string'. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Purely stylistic, but everywhere else in the compiler we always put the constant on the right hand side (as the code was originally written). I'd prefer to keep things consistent.