Narrow type in case/default sections in switch on discriminant property

This commit is contained in:
Anders Hejlsberg 2016-06-13 14:29:04 -07:00
parent 4a8f94a553
commit ce156460eb
3 changed files with 102 additions and 23 deletions

View File

@ -693,8 +693,23 @@ namespace ts {
setFlowNodeReferenced(antecedent);
return <FlowCondition>{
flags,
antecedent,
expression,
antecedent
};
}
function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode {
const expr = switchStatement.expression;
if (expr.kind !== SyntaxKind.PropertyAccessExpression || !isNarrowableReference((<PropertyAccessExpression>expr).expression)) {
return antecedent;
}
setFlowNodeReferenced(antecedent);
return <FlowSwitchClause>{
flags: FlowFlags.SwitchClause,
switchStatement,
clauseStart,
clauseEnd,
antecedent
};
}
@ -923,9 +938,9 @@ namespace ts {
preSwitchCaseFlow = currentFlow;
bind(node.caseBlock);
addAntecedent(postSwitchLabel, currentFlow);
const hasNonEmptyDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause && c.statements.length);
if (!hasNonEmptyDefault) {
addAntecedent(postSwitchLabel, preSwitchCaseFlow);
const hasDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause);
if (!hasDefault) {
addAntecedent(postSwitchLabel, createFlowSwitchClause(preSwitchCaseFlow, node, 0, 0));
}
currentBreakTarget = saveBreakTarget;
preSwitchCaseFlow = savePreSwitchCaseFlow;
@ -934,25 +949,22 @@ namespace ts {
function bindCaseBlock(node: CaseBlock): void {
const clauses = node.clauses;
let fallthroughFlow = unreachableFlow;
for (let i = 0; i < clauses.length; i++) {
const clause = clauses[i];
if (clause.statements.length) {
if (currentFlow.flags & FlowFlags.Unreachable) {
currentFlow = preSwitchCaseFlow;
}
else {
const preCaseLabel = createBranchLabel();
addAntecedent(preCaseLabel, preSwitchCaseFlow);
addAntecedent(preCaseLabel, currentFlow);
currentFlow = finishFlowLabel(preCaseLabel);
}
bind(clause);
if (!(currentFlow.flags & FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) {
errorOnFirstToken(clause, Diagnostics.Fallthrough_case_in_switch);
}
const clauseStart = i;
while (!clauses[i].statements.length && i + 1 < clauses.length) {
bind(clauses[i]);
i++;
}
else {
bind(clause);
const preCaseLabel = createBranchLabel();
addAntecedent(preCaseLabel, createFlowSwitchClause(preSwitchCaseFlow, <SwitchStatement>node.parent, clauseStart, i + 1));
addAntecedent(preCaseLabel, fallthroughFlow);
currentFlow = finishFlowLabel(preCaseLabel);
const clause = clauses[i];
bind(clause);
fallthroughFlow = currentFlow;
if (!(currentFlow.flags & FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) {
errorOnFirstToken(clause, Diagnostics.Fallthrough_case_in_switch);
}
}
}

View File

@ -7676,6 +7676,29 @@ namespace ts {
return node;
}
function getTypeOfSwitchClause(clause: CaseClause | DefaultClause) {
if (clause.kind === SyntaxKind.CaseClause) {
const expr = (<CaseClause>clause).expression;
return expr.kind === SyntaxKind.StringLiteral ? getStringLiteralTypeForText((<StringLiteral>expr).text) : checkExpression(expr);
}
return undefined;
}
function getSwitchClauseTypes(switchStatement: SwitchStatement): Type[] {
const links = getNodeLinks(switchStatement);
if (!links.switchTypes) {
// If all case clauses specify expressions that have unit types, we return an array
// of those unit types. Otherwise we return an empty array.
const types = map(switchStatement.caseBlock.clauses, getTypeOfSwitchClause);
links.switchTypes = forEach(types, t => !t || t.flags & TypeFlags.StringLiteral) ? types : emptyArray;
}
return links.switchTypes;
}
function eachTypeContainedIn(source: Type, types: Type[]) {
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
}
function getFlowTypeOfReference(reference: Node, declaredType: Type, assumeInitialized: boolean, includeOuterFunctions: boolean) {
let key: string;
if (!reference.flowNode || assumeInitialized && !(declaredType.flags & TypeFlags.Narrowable)) {
@ -7713,6 +7736,9 @@ namespace ts {
else if (flow.flags & FlowFlags.Condition) {
type = getTypeAtFlowCondition(<FlowCondition>flow);
}
else if (flow.flags & FlowFlags.SwitchClause) {
type = getTypeAtSwitchClause(<FlowSwitchClause>flow);
}
else if (flow.flags & FlowFlags.Label) {
if ((<FlowLabel>flow).antecedents.length === 1) {
flow = (<FlowLabel>flow).antecedents[0];
@ -7796,6 +7822,11 @@ namespace ts {
return type;
}
function getTypeAtSwitchClause(flow: FlowSwitchClause) {
const type = getTypeAtFlowNode(flow.antecedent);
return narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
}
function getTypeAtFlowBranchLabel(flow: FlowLabel) {
const antecedentTypes: Type[] = [];
for (const antecedent of flow.antecedents) {
@ -7938,6 +7969,33 @@ namespace ts {
return type;
}
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
// We have switch statement with property access expression
if (!(type.flags & TypeFlags.Union) || !isMatchingReference(reference, (<PropertyAccessExpression>switchStatement.expression).expression)) {
return type;
}
const propName = (<PropertyAccessExpression>switchStatement.expression).name.text;
const propType = getTypeOfPropertyOfType(type, propName);
if (!propType || !isStringLiteralUnionType(propType)) {
return type;
}
const switchTypes = getSwitchClauseTypes(switchStatement);
if (!switchTypes.length) {
return type;
}
const types = (<UnionType>type).types;
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, undefined);
const caseTypes = hasDefaultClause ? filter(clauseTypes, t => !!t) : clauseTypes;
const discriminantType = caseTypes.length ? getUnionType(caseTypes) : undefined;
const caseType = discriminantType && getUnionType(filter(types, t => isTypeComparableTo(discriminantType, getTypeOfPropertyOfType(t, propName))));
if (!hasDefaultClause) {
return caseType;
}
const defaultType = getUnionType(filter(types, t => !eachTypeContainedIn(getTypeOfPropertyOfType(t, propName), switchTypes)));
return caseType ? getUnionType([caseType, defaultType]) : defaultType;
}
function narrowTypeByTypeof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
// We have '==', '!=', '====', or !==' operator with 'typeof xxx' on the left
// and string literal on the right

View File

@ -1554,8 +1554,9 @@ namespace ts {
Assignment = 1 << 4, // Assignment
TrueCondition = 1 << 5, // Condition known to be true
FalseCondition = 1 << 6, // Condition known to be false
Referenced = 1 << 7, // Referenced as antecedent once
Shared = 1 << 8, // Referenced as antecedent more than once
SwitchClause = 1 << 7, // Switch statement clause
Referenced = 1 << 8, // Referenced as antecedent once
Shared = 1 << 9, // Referenced as antecedent more than once
Label = BranchLabel | LoopLabel,
Condition = TrueCondition | FalseCondition
}
@ -1591,6 +1592,13 @@ namespace ts {
antecedent: FlowNode;
}
export interface FlowSwitchClause extends FlowNode {
switchStatement: SwitchStatement;
clauseStart: number; // Start index of case/default clause range
clauseEnd: number; // End index of case/default clause range
antecedent: FlowNode;
}
export interface AmdDependency {
path: string;
name: string;
@ -2170,6 +2178,7 @@ namespace ts {
resolvedJsxType?: Type; // resolved element attributes type of a JSX openinglike element
hasSuperCall?: boolean; // recorded result when we try to find super-call. We only try to find one if this flag is undefined, indicating that we haven't made an attempt.
superCall?: ExpressionStatement; // Cached first super-call found in the constructor. Used in checking whether super is called before this-accessing
switchTypes?: Type[]; // Cached array of switch case expression types
}
export const enum TypeFlags {