-
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
Introduce boolean literal freshness #27042
Changes from all commits
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 |
---|---|---|
|
@@ -296,8 +296,8 @@ namespace ts { | |
createPromiseType, | ||
createArrayType, | ||
getBooleanType: () => booleanType, | ||
getFalseType: () => falseType, | ||
getTrueType: () => trueType, | ||
getFalseType: (fresh?) => fresh ? falseType : regularFalseType, | ||
getTrueType: (fresh?) => fresh ? trueType : regularTrueType, | ||
getVoidType: () => voidType, | ||
getUndefinedType: () => undefinedType, | ||
getNullType: () => nullType, | ||
|
@@ -405,9 +405,22 @@ namespace ts { | |
const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null | TypeFlags.ContainsWideningType, "null"); | ||
const stringType = createIntrinsicType(TypeFlags.String, "string"); | ||
const numberType = createIntrinsicType(TypeFlags.Number, "number"); | ||
const falseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false"); | ||
const trueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true"); | ||
const booleanType = createBooleanType([falseType, trueType]); | ||
const falseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false") as FreshableIntrinsicType; | ||
const regularFalseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false") as FreshableIntrinsicType; | ||
const trueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true") as FreshableIntrinsicType; | ||
const regularTrueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true") as FreshableIntrinsicType; | ||
falseType.flags |= TypeFlags.FreshLiteral; | ||
trueType.flags |= TypeFlags.FreshLiteral; | ||
trueType.regularType = regularTrueType; | ||
regularTrueType.freshType = trueType; | ||
falseType.regularType = regularFalseType; | ||
regularFalseType.freshType = falseType; | ||
const booleanType = createBooleanType([regularFalseType, regularTrueType]); | ||
// Also mark all combinations of fresh/regular booleans as "Boolean" so they print as `boolean` instead of `true | false` | ||
// (The union is cached, so simply doing the marking here is sufficient) | ||
createBooleanType([regularFalseType, trueType]); | ||
createBooleanType([falseType, regularTrueType]); | ||
createBooleanType([falseType, trueType]); | ||
const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol"); | ||
const voidType = createIntrinsicType(TypeFlags.Void, "void"); | ||
const neverType = createIntrinsicType(TypeFlags.Never, "never"); | ||
|
@@ -4170,7 +4183,7 @@ namespace ts { | |
const baseType = t.flags & TypeFlags.BooleanLiteral ? booleanType : getBaseTypeOfEnumLiteralType(<LiteralType>t); | ||
if (baseType.flags & TypeFlags.Union) { | ||
const count = (<UnionType>baseType).types.length; | ||
if (i + count <= types.length && types[i + count - 1] === (<UnionType>baseType).types[count - 1]) { | ||
if (i + count <= types.length && getRegularTypeOfLiteralType(types[i + count - 1]) === getRegularTypeOfLiteralType((<UnionType>baseType).types[count - 1])) { | ||
result.push(baseType); | ||
i += count - 1; | ||
continue; | ||
|
@@ -6157,7 +6170,7 @@ namespace ts { | |
if (type.flags & TypeFlags.UniqueESSymbol) { | ||
return `__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as __String; | ||
} | ||
if (type.flags & TypeFlags.StringOrNumberLiteral) { | ||
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { | ||
return escapeLeadingUnderscores("" + (<LiteralType>type).value); | ||
} | ||
return Debug.fail(); | ||
|
@@ -8810,7 +8823,7 @@ namespace ts { | |
t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String || | ||
t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number || | ||
t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol || | ||
t.flags & TypeFlags.StringOrNumberLiteral && t.flags & TypeFlags.FreshLiteral && containsType(types, (<LiteralType>t).regularType); | ||
t.flags & TypeFlags.Literal && t.flags & TypeFlags.FreshLiteral && containsType(types, (<LiteralType>t).regularType); | ||
if (remove) { | ||
orderedRemoveItemAt(types, i); | ||
} | ||
|
@@ -9810,8 +9823,8 @@ namespace ts { | |
} | ||
|
||
function getFreshTypeOfLiteralType(type: Type): Type { | ||
if (type.flags & TypeFlags.StringOrNumberLiteral && !(type.flags & TypeFlags.FreshLiteral)) { | ||
if (!(<LiteralType>type).freshType) { | ||
if (type.flags & TypeFlags.Literal && !(type.flags & TypeFlags.FreshLiteral)) { | ||
if (!(<LiteralType>type).freshType) { // NOTE: Safe because all freshable intrinsics always have fresh types already | ||
const freshType = createLiteralType(type.flags | TypeFlags.FreshLiteral, (<LiteralType>type).value, (<LiteralType>type).symbol); | ||
freshType.regularType = <LiteralType>type; | ||
(<LiteralType>type).freshType = freshType; | ||
|
@@ -9822,7 +9835,7 @@ namespace ts { | |
} | ||
|
||
function getRegularTypeOfLiteralType(type: Type): Type { | ||
return type.flags & TypeFlags.StringOrNumberLiteral && type.flags & TypeFlags.FreshLiteral ? (<LiteralType>type).regularType : | ||
return type.flags & TypeFlags.Literal && type.flags & TypeFlags.FreshLiteral ? (<LiteralType>type).regularType : | ||
type.flags & TypeFlags.Union ? getUnionType(sameMap((<UnionType>type).types, getRegularTypeOfLiteralType)) : | ||
type; | ||
} | ||
|
@@ -11035,11 +11048,11 @@ namespace ts { | |
} | ||
|
||
function isTypeRelatedTo(source: Type, target: Type, relation: Map<RelationComparisonResult>) { | ||
if (source.flags & TypeFlags.StringOrNumberLiteral && source.flags & TypeFlags.FreshLiteral) { | ||
source = (<LiteralType>source).regularType; | ||
if (source.flags & TypeFlags.Literal && source.flags & TypeFlags.FreshLiteral) { | ||
source = (<FreshableType>source).regularType; | ||
} | ||
if (target.flags & TypeFlags.StringOrNumberLiteral && target.flags & TypeFlags.FreshLiteral) { | ||
target = (<LiteralType>target).regularType; | ||
if (target.flags & TypeFlags.Literal && target.flags & TypeFlags.FreshLiteral) { | ||
target = (<FreshableType>target).regularType; | ||
} | ||
if (source === target || | ||
relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || | ||
|
@@ -11194,11 +11207,11 @@ namespace ts { | |
* * Ternary.False if they are not related. | ||
*/ | ||
function isRelatedTo(source: Type, target: Type, reportErrors = false, headMessage?: DiagnosticMessage): Ternary { | ||
if (source.flags & TypeFlags.StringOrNumberLiteral && source.flags & TypeFlags.FreshLiteral) { | ||
source = (<LiteralType>source).regularType; | ||
if (source.flags & TypeFlags.Literal && source.flags & TypeFlags.FreshLiteral) { | ||
source = (<FreshableType>source).regularType; | ||
} | ||
if (target.flags & TypeFlags.StringOrNumberLiteral && target.flags & TypeFlags.FreshLiteral) { | ||
target = (<LiteralType>target).regularType; | ||
if (target.flags & TypeFlags.Literal && target.flags & TypeFlags.FreshLiteral) { | ||
target = (<FreshableType>target).regularType; | ||
} | ||
if (source.flags & TypeFlags.Substitution) { | ||
source = relation === definitelyAssignableRelation ? (<SubstitutionType>source).typeVariable : (<SubstitutionType>source).substitute; | ||
|
@@ -12766,7 +12779,7 @@ namespace ts { | |
return type.flags & TypeFlags.EnumLiteral && type.flags & TypeFlags.FreshLiteral ? getBaseTypeOfEnumLiteralType(<LiteralType>type) : | ||
type.flags & TypeFlags.StringLiteral && type.flags & TypeFlags.FreshLiteral ? stringType : | ||
type.flags & TypeFlags.NumberLiteral && type.flags & TypeFlags.FreshLiteral ? numberType : | ||
type.flags & TypeFlags.BooleanLiteral ? booleanType : | ||
type.flags & TypeFlags.BooleanLiteral && type.flags & TypeFlags.FreshLiteral ? booleanType : | ||
type.flags & TypeFlags.Union ? getUnionType(sameMap((<UnionType>type).types, getWidenedLiteralType)) : | ||
type; | ||
} | ||
|
@@ -12820,7 +12833,7 @@ namespace ts { | |
return type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((<UnionType>type).types) : | ||
type.flags & TypeFlags.StringLiteral ? (<LiteralType>type).value === "" ? TypeFlags.StringLiteral : 0 : | ||
type.flags & TypeFlags.NumberLiteral ? (<LiteralType>type).value === 0 ? TypeFlags.NumberLiteral : 0 : | ||
type.flags & TypeFlags.BooleanLiteral ? type === falseType ? TypeFlags.BooleanLiteral : 0 : | ||
type.flags & TypeFlags.BooleanLiteral ? (type === falseType || type === regularFalseType) ? TypeFlags.BooleanLiteral : 0 : | ||
type.flags & TypeFlags.PossiblyFalsy; | ||
} | ||
|
||
|
@@ -12837,7 +12850,8 @@ namespace ts { | |
function getDefinitelyFalsyPartOfType(type: Type): Type { | ||
return type.flags & TypeFlags.String ? emptyStringType : | ||
type.flags & TypeFlags.Number ? zeroType : | ||
type.flags & TypeFlags.Boolean || type === falseType ? falseType : | ||
type.flags & TypeFlags.Boolean || type === regularFalseType ? regularFalseType : | ||
type === falseType ? falseType : | ||
type.flags & (TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null) || | ||
type.flags & TypeFlags.StringLiteral && (<LiteralType>type).value === "" || | ||
type.flags & TypeFlags.NumberLiteral && (<LiteralType>type).value === 0 ? type : | ||
|
@@ -14092,7 +14106,10 @@ namespace ts { | |
if (assignedType.flags & TypeFlags.Never) { | ||
return assignedType; | ||
} | ||
const reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); | ||
let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); | ||
if (assignedType.flags & (TypeFlags.FreshLiteral | TypeFlags.Literal)) { | ||
reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types | ||
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 didn't we have to do this before, for string and number literal types? 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. We probably should have been but as it turns out people narrow 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 as I said in person, this is going to break one RWC project along the lines of this: type RuleSeverity = "warning" | "error" | "off";
interface IOptions {
ruleSeverity: RuleSeverity;
}
function parseRuleOptions(): Partial<IOptions> {
let defaultRuleSeverity: RuleSeverity = "error";
let ruleSeverity = defaultRuleSeverity;
return {
ruleSeverity,
};
} previously this worked because 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. So this change appears to be the cause of #27259. I'm really not sure I like this. The intuitive rule was always that once you add a type annotation to const x1: 'x' = 'x';
let z1 = x1; // 'x'
const x2: 'x' | 'y' = 'x';
let z2 = x2; // Was 'x', now string I find it hard to construct a rationale for that. 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. FWIW the individual unit type only behaves differently because it skips the narrowing codepath. Had I realized as much I probably would've altered the unit type behavior, too. In any case, removing this bit if we don't like it is easy; but it's going to make boolean literals a tad more breaky. |
||
} | ||
// Our crude heuristic produces an invalid result in some cases: see GH#26130. | ||
// For now, when that happens, we give up and don't narrow at all. (This also | ||
// means we'll never narrow for erroneous assignments where the assigned type | ||
|
@@ -14145,8 +14162,8 @@ namespace ts { | |
} | ||
if (flags & TypeFlags.BooleanLike) { | ||
return strictNullChecks ? | ||
type === falseType ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : | ||
type === falseType ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; | ||
(type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : | ||
(type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; | ||
} | ||
if (flags & TypeFlags.Object) { | ||
return isFunctionObjectType(<ObjectType>type) ? | ||
|
@@ -28351,19 +28368,20 @@ namespace ts { | |
function isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean { | ||
if (isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node)) { | ||
const type = getTypeOfSymbol(getSymbolOfNode(node)); | ||
return !!(type.flags & TypeFlags.StringOrNumberLiteral && type.flags & TypeFlags.FreshLiteral); | ||
return !!(type.flags & TypeFlags.Literal && type.flags & TypeFlags.FreshLiteral); | ||
} | ||
return false; | ||
} | ||
|
||
function literalTypeToNode(type: LiteralType, enclosing: Node): Expression { | ||
const enumResult = type.flags & TypeFlags.EnumLiteral && nodeBuilder.symbolToExpression(type.symbol, SymbolFlags.Value, enclosing); | ||
return enumResult || createLiteral(type.value); | ||
function literalTypeToNode(type: FreshableType, enclosing: Node): Expression { | ||
const enumResult = type.flags & TypeFlags.EnumLiteral ? nodeBuilder.symbolToExpression(type.symbol, SymbolFlags.Value, enclosing) | ||
: type === trueType ? createTrue() : type === falseType && createFalse(); | ||
return enumResult || createLiteral((type as LiteralType).value); | ||
} | ||
|
||
function createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration) { | ||
const type = getTypeOfSymbol(getSymbolOfNode(node)); | ||
return literalTypeToNode(<LiteralType>type, node); | ||
return literalTypeToNode(<FreshableType>type, node); | ||
} | ||
|
||
function createResolver(): EmitResolver { | ||
|
@@ -29735,7 +29753,7 @@ namespace ts { | |
|
||
function checkAmbientInitializer(node: VariableDeclaration | PropertyDeclaration | PropertySignature) { | ||
if (node.initializer) { | ||
const isInvalidInitializer = !(isStringOrNumberLiteralExpression(node.initializer) || isSimpleLiteralEnumReference(node.initializer)); | ||
const isInvalidInitializer = !(isStringOrNumberLiteralExpression(node.initializer) || isSimpleLiteralEnumReference(node.initializer) || node.initializer.kind === SyntaxKind.TrueKeyword || node.initializer.kind === SyntaxKind.FalseKeyword); | ||
const isConstOrReadonly = isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node); | ||
if (isConstOrReadonly && !node.type) { | ||
if (isInvalidInitializer) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3034,8 +3034,8 @@ namespace ts { | |
/* @internal */ getStringType(): Type; | ||
/* @internal */ getNumberType(): Type; | ||
/* @internal */ getBooleanType(): Type; | ||
/* @internal */ getFalseType(): Type; | ||
/* @internal */ getTrueType(): Type; | ||
/* @internal */ getFalseType(fresh?: boolean): Type; | ||
/* @internal */ getTrueType(fresh?: boolean): Type; | ||
/* @internal */ getVoidType(): Type; | ||
/* @internal */ getUndefinedType(): Type; | ||
/* @internal */ getNullType(): Type; | ||
|
@@ -3731,7 +3731,7 @@ namespace ts { | |
Unit = Literal | UniqueESSymbol | Nullable, | ||
StringOrNumberLiteral = StringLiteral | NumberLiteral, | ||
/* @internal */ | ||
StringOrNumberLiteralOrUnique = StringOrNumberLiteral | UniqueESSymbol, | ||
StringOrNumberLiteralOrUnique = StringLiteral | NumberLiteral | UniqueESSymbol, | ||
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. this change doesn't seem related (and I'm not sure it's an improvement?) 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. Eh, I changed it because I actually removed |
||
/* @internal */ | ||
DefinitelyFalsy = StringLiteral | NumberLiteral | BooleanLiteral | Void | Undefined | Null, | ||
PossiblyFalsy = DefinitelyFalsy | String | Number | Boolean, | ||
|
@@ -3802,6 +3802,15 @@ namespace ts { | |
intrinsicName: string; // Name of intrinsic type | ||
} | ||
|
||
/* @internal */ | ||
export interface FreshableIntrinsicType extends IntrinsicType { | ||
freshType: IntrinsicType; // Fresh version of type | ||
regularType: IntrinsicType; // Regular version of type | ||
} | ||
|
||
/* @internal */ | ||
export type FreshableType = LiteralType | FreshableIntrinsicType; | ||
|
||
// String literal types (TypeFlags.StringLiteral) | ||
// Numeric literal types (TypeFlags.NumberLiteral) | ||
export interface LiteralType extends Type { | ||
|
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.
can we ever get a union of
falseType | regularFalseType
? Note that it doesn't matter how we print that, I'm just curious whether it can happen.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.
Uhmmm, I don't think so, just like how we can't have
"word" | "word"(fresh edition)
, since both have the same type "identity". Specifically,removeRedundantLiteralTypes
handles removing redundant fresh types when the regular one is also present.