Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f8fa8b2
Implement --strictReadonly compiler option
ahejlsberg Apr 20, 2024
453d778
Fix inconsistent 'readonly' modifiers in lib files
ahejlsberg Apr 22, 2024
0b5728a
Accept new baselines
ahejlsberg Apr 22, 2024
170bb5f
Change option name to --enforceReadonly
ahejlsberg Apr 23, 2024
97adafc
Accept new API baselines
ahejlsberg Apr 23, 2024
800c901
More baseline changes
ahejlsberg Apr 23, 2024
8c1ac64
Add tests
ahejlsberg Apr 23, 2024
88c2579
Merge branch 'main' into enforceReadonly
ahejlsberg Apr 23, 2024
d7c8851
Remove unused file
ahejlsberg Apr 23, 2024
dd6c3a8
Enforce read-only semantics in generic mapped types
ahejlsberg Apr 24, 2024
638e55c
Add more tests
ahejlsberg Apr 24, 2024
6fafe58
Compile APILibCheck.ts with --enforceReadonly
ahejlsberg Apr 24, 2024
4d16813
Accept new API baselines
ahejlsberg Apr 24, 2024
f9a132b
Fix formatting
ahejlsberg Apr 24, 2024
d616ca0
Fix comment
ahejlsberg Apr 25, 2024
474a34b
Exclude methods from strict checking
ahejlsberg Apr 25, 2024
3ee4e91
Add more tests
ahejlsberg Apr 25, 2024
5afbd2c
Remove test
ahejlsberg Apr 26, 2024
dbd7d0b
Accept new baselines
ahejlsberg Apr 29, 2024
f3bdc07
Merge branch 'main' into enforceReadonly
ahejlsberg Jul 15, 2024
90ce450
Exclude comparable relation from strict readonly checking
ahejlsberg Jul 17, 2024
c8a66e0
Add more tests
ahejlsberg Jul 17, 2024
43cc32f
Align String.raw template parameter with TemplateStringsArray
ahejlsberg Jul 17, 2024
4a668c3
Accept new baselines
ahejlsberg Jul 17, 2024
7494b23
Revert unnecessary change
ahejlsberg Jul 17, 2024
adee2e1
Accept new baselines
ahejlsberg Jul 17, 2024
ccdb99c
In obj[key], don't check for nullable when obj and key are generic
ahejlsberg Jul 24, 2024
c706f5b
Update tests
ahejlsberg Jul 24, 2024
d27548c
Accept new baselines
ahejlsberg Jul 24, 2024
9451c93
Add more tests
ahejlsberg Jul 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Implement --strictReadonly compiler option
  • Loading branch information
ahejlsberg committed Apr 20, 2024
commit f8fa8b272617f77f9ddd71399d741088f63c4755
67 changes: 44 additions & 23 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down