Implement --strictReadonly compiler option

This commit is contained in:
Anders Hejlsberg
2024-04-20 11:43:36 -07:00
parent c3f185417a
commit f8fa8b2726
4 changed files with 66 additions and 23 deletions

View File

@@ -1478,6 +1478,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis");
var useUnknownInCatchVariables = getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables");
var exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes;
var strictReadonly = compilerOptions.strictReadonly;
var checkBinaryExpression = createCheckBinaryExpression();
var emitResolver = createResolver();
@@ -23479,9 +23480,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// They're still assignable to one another, since `readonly` doesn't affect assignability.
// This is only applied during the strictSubtypeRelation -- currently used in subtype reduction
if (
relation === strictSubtypeRelation &&
isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp)
(relation === strictSubtypeRelation || strictReadonly) &&
isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp) && !(targetProp.flags & SymbolFlags.Method)
) {
if (reportErrors) {
reportError(Diagnostics.Property_0_is_readonly_in_the_source_but_not_in_the_target, symbolToString(targetProp));
}
return Ternary.False;
}
// If the target comes from a partial union prop, allow `undefined` in the target type
@@ -23940,13 +23944,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function indexInfoRelatedTo(sourceInfo: IndexInfo, targetInfo: IndexInfo, reportErrors: boolean, intersectionState: IntersectionState) {
const related = isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState);
if (!related && reportErrors) {
if (sourceInfo.keyType === targetInfo.keyType) {
reportError(Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType));
if (!related) {
if (reportErrors) {
if (sourceInfo.keyType === targetInfo.keyType) {
reportError(Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType));
}
else {
reportError(Diagnostics._0_and_1_index_signatures_are_incompatible, typeToString(sourceInfo.keyType), typeToString(targetInfo.keyType));
}
}
else {
reportError(Diagnostics._0_and_1_index_signatures_are_incompatible, typeToString(sourceInfo.keyType), typeToString(targetInfo.keyType));
return Ternary.False;
}
if (strictReadonly && sourceInfo.isReadonly && !targetInfo.isReadonly) {
if (reportErrors) {
reportError(Diagnostics._0_index_signature_is_readonly_in_the_source_but_not_in_the_target, typeToString(sourceInfo.keyType));
}
return Ternary.False;
}
return related;
}
@@ -30968,10 +30981,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function getTypeOfPropertyOfContextualType(type: Type, name: __String, nameType?: Type) {
return mapType(type, t => {
if (isGenericMappedType(t) && !t.declaration.nameType) {
const constraint = getConstraintTypeFromMappedType(t);
const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint;
const constraint = getBaseConstraintOrType(getConstraintTypeFromMappedType(t));
const propertyNameType = nameType || getStringLiteralType(unescapeLeadingUnderscores(name));
if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) {
if (isTypeAssignableTo(propertyNameType, constraint)) {
return substituteIndexedMappedType(t, propertyNameType);
}
}
@@ -31019,25 +31031,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const symbol = getSymbolOfDeclaration(element);
return getTypeOfPropertyOfContextualType(type, symbol.escapedName, getSymbolLinks(symbol).nameType);
}
if (hasDynamicName(element)) {
const name = getNameOfDeclaration(element);
if (name && isComputedPropertyName(name)) {
const exprType = checkExpression(name.expression);
const propType = isTypeUsableAsPropertyName(exprType) && getTypeOfPropertyOfContextualType(type, getPropertyNameFromType(exprType));
if (propType) {
return propType;
}
const name = getNameOfDeclaration(element);
if (name && isComputedPropertyName(name)) {
const exprType = checkExpression(name.expression);
if (isTypeUsableAsPropertyName(exprType)) {
return getTypeOfPropertyOfContextualType(type, getPropertyNameFromType(exprType), exprType);
}
}
if (element.name) {
const nameType = getLiteralTypeFromPropertyName(element.name);
// We avoid calling getApplicableIndexInfo here because it performs potentially expensive intersection reduction.
return mapType(type, t => findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType)?.type, /*noReductions*/ true);
return mapType(type, t => findApplicableIndexInfo(getIndexInfosOfStructuredType(t), exprType)?.type, /*noReductions*/ true);
}
}
return undefined;
}
function isContextualPropertyMutable(type: Type, name: __String, nameType: Type | undefined) {
return someType(type, t => {
const propName = nameType ? isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined : name;
const prop = propName && getPropertyOfType(t, propName);
if (prop) {
return !isReadonlySymbol(prop);
}
const indexInfo = findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType || getStringLiteralType(unescapeLeadingUnderscores(name)));
if (indexInfo) {
return !indexInfo.isReadonly;
}
return false;
});
}
function getSpreadIndices(elements: readonly Node[]) {
let first, last;
for (let i = 0; i < elements.length; i++) {
@@ -31956,7 +31977,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const contextualTypeHasPattern = contextualType && contextualType.pattern &&
(contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression);
const inConstContext = isConstContext(node);
const checkFlags = inConstContext ? CheckFlags.Readonly : 0;
const isInJavascript = isInJSFile(node) && !isInJsonFile(node);
const enumTag = isInJavascript ? getJSDocEnumTag(node) : undefined;
const isJSObjectLiteral = !contextualType && isInJavascript && !enumTag;
@@ -32003,6 +32023,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags;
const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined;
const checkFlags = inConstContext && !(strictReadonly && contextualType && isContextualPropertyMutable(contextualType, member.escapedName, nameType)) ? CheckFlags.Readonly : 0;
const prop = nameType ?
createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) :
createSymbol(SymbolFlags.Property | member.flags, member.escapedName, checkFlags);

View File

@@ -998,6 +998,15 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
description: Diagnostics.Enforces_using_indexed_accessors_for_keys_declared_using_an_indexed_type,
defaultValueDescription: false,
},
{
name: "strictReadonly",
type: "boolean",
affectsSemanticDiagnostics: true,
affectsBuildInfo: true,
category: Diagnostics.Type_Checking,
description: Diagnostics.Ensure_that_readonly_properties_remain_read_only_in_type_relationships,
defaultValueDescription: false,
},
// Module Resolution
{

View File

@@ -4192,6 +4192,14 @@
"category": "Error",
"code": 4126
},
"Property '{0}' is 'readonly' in the source but not in the target.": {
"category": "Error",
"code": 4127
},
"'{0}' index signature is 'readonly' in the source but not in the target.": {
"category": "Error",
"code": 4128
},
"The current host does not support the '{0}' option.": {
"category": "Error",
@@ -6223,6 +6231,10 @@
"category": "Message",
"code": 6718
},
"Ensure that 'readonly' properties remain read-only in type relationships.": {
"category": "Message",
"code": 6719
},
"Default catch clause variables as 'unknown' instead of 'any'.": {
"category": "Message",
"code": 6803

View File

@@ -7225,6 +7225,7 @@ export interface CompilerOptions {
strictBindCallApply?: boolean; // Always combine with strict property
strictNullChecks?: boolean; // Always combine with strict property
strictPropertyInitialization?: boolean; // Always combine with strict property
strictReadonly?: boolean;
stripInternal?: boolean;
/** @deprecated */
suppressExcessPropertyErrors?: boolean;