Skip to content
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

New --enforceReadonly compiler option to enforce read-only semantics in type relations #58296

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 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
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
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 enforceReadonly = compilerOptions.enforceReadonly;

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 || enforceReadonly) &&
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 (enforceReadonly && 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 && !(enforceReadonly && 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: "enforceReadonly",
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.": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a drive-by comment that as an everyday user of TS, I don't really know what a "type relationship" is. Maybe this could be worded more explicitly as:

Suggested change
"Ensure that 'readonly' properties remain read-only in type relationships.": {
"Enforce that mutable properties cannot satisfy 'readonly' requirements in assignment or subtype 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 @@ -7132,6 +7132,7 @@ export interface CompilerOptions {
downlevelIteration?: boolean;
emitBOM?: boolean;
emitDecoratorMetadata?: boolean;
enforceReadonly?: boolean;
exactOptionalPropertyTypes?: boolean;
experimentalDecorators?: boolean;
forceConsistentCasingInFileNames?: boolean;
Expand Down
14 changes: 7 additions & 7 deletions src/lib/dom.generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,8 +1266,8 @@ interface PushSubscriptionOptionsInit {
}

interface QueuingStrategy<T = any> {
highWaterMark?: number;
size?: QueuingStrategySize<T>;
readonly highWaterMark?: number;
readonly size?: QueuingStrategySize<T>;
}

interface QueuingStrategyInit {
Expand All @@ -1276,7 +1276,7 @@ interface QueuingStrategyInit {
*
* Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw.
*/
highWaterMark: number;
readonly highWaterMark: number;
}

interface RTCAnswerOptions extends RTCOfferAnswerOptions {
Expand Down Expand Up @@ -5965,9 +5965,9 @@ interface DOMException extends Error {
*/
readonly code: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */
readonly message: string;
message: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */
readonly name: string;
name: string;
readonly INDEX_SIZE_ERR: 1;
readonly DOMSTRING_SIZE_ERR: 2;
readonly HIERARCHY_REQUEST_ERR: 3;
Expand Down Expand Up @@ -18625,7 +18625,7 @@ interface ReadableStream<R = any> {

declare var ReadableStream: {
prototype: ReadableStream;
new(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number }): ReadableStream<Uint8Array>;
new(underlyingSource: UnderlyingByteSource, strategy?: { readonly highWaterMark?: number }): ReadableStream<Uint8Array>;
new<R = any>(underlyingSource: UnderlyingDefaultSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;
new<R = any>(underlyingSource?: UnderlyingSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;
};
Expand Down Expand Up @@ -19365,7 +19365,7 @@ interface SVGElementEventMap extends ElementEventMap, GlobalEventHandlersEventMa
*/
interface SVGElement extends Element, ElementCSSInlineStyle, GlobalEventHandlers, HTMLOrSVGElement {
/** @deprecated */
readonly className: any;
className: any;
readonly ownerSVGElement: SVGSVGElement | null;
readonly viewportElement: SVGElement | null;
addEventListener<K extends keyof SVGElementEventMap>(type: K, listener: (this: SVGElement, ev: SVGElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/es2015.collection.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface Map<K, V> {
interface MapConstructor {
new (): Map<any, any>;
new <K, V>(entries?: readonly (readonly [K, V])[] | null): Map<K, V>;
readonly prototype: Map<any, any>;
prototype: Map<any, any>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned with this change as prototype is actually read-only on Map, Set, WeakMap, WeakSet, etc.:

image

}
declare var Map: MapConstructor;

Expand Down Expand Up @@ -64,7 +64,7 @@ interface WeakMap<K extends WeakKey, V> {

interface WeakMapConstructor {
new <K extends WeakKey = WeakKey, V = any>(entries?: readonly (readonly [K, V])[] | null): WeakMap<K, V>;
readonly prototype: WeakMap<WeakKey, any>;
prototype: WeakMap<WeakKey, any>;
}
declare var WeakMap: WeakMapConstructor;

Expand Down Expand Up @@ -96,7 +96,7 @@ interface Set<T> {

interface SetConstructor {
new <T = any>(values?: readonly T[] | null): Set<T>;
readonly prototype: Set<any>;
prototype: Set<any>;
}
declare var Set: SetConstructor;

Expand Down Expand Up @@ -124,6 +124,6 @@ interface WeakSet<T extends WeakKey> {

interface WeakSetConstructor {
new <T extends WeakKey = WeakKey>(values?: readonly T[] | null): WeakSet<T>;
readonly prototype: WeakSet<WeakKey>;
prototype: WeakSet<WeakKey>;
}
declare var WeakSet: WeakSetConstructor;
4 changes: 2 additions & 2 deletions src/lib/es2015.generator.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface GeneratorFunction {
/**
* A reference to the prototype.
*/
readonly prototype: Generator;
prototype: Generator;
}

interface GeneratorFunctionConstructor {
Expand All @@ -55,5 +55,5 @@ interface GeneratorFunctionConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: GeneratorFunction;
prototype: GeneratorFunction;
}
2 changes: 1 addition & 1 deletion src/lib/es2015.promise.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ interface PromiseConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: Promise<any>;
prototype: Promise<any>;

/**
* Creates a new Promise.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2015.symbol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ interface SymbolConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: Symbol;
prototype: Symbol;

/**
* Returns a new unique Symbol value.
Expand Down
4 changes: 2 additions & 2 deletions src/lib/es2017.object.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ interface ObjectConstructor {
* Returns an array of values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
values<T>(o: { [s: string]: T; } | ArrayLike<T>): T[];
values<T>(o: { readonly [s: string]: T; } | ArrayLike<T>): T[];

/**
* Returns an array of values of the enumerable properties of an object
Expand All @@ -15,7 +15,7 @@ interface ObjectConstructor {
* Returns an array of key/values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T>(o: { [s: string]: T; } | ArrayLike<T>): [string, T][];
entries<T>(o: { readonly [s: string]: T; } | ArrayLike<T>): [string, T][];

/**
* Returns an array of key/values of the enumerable properties of an object
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2017.sharedmemory.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface SharedArrayBuffer {
}

interface SharedArrayBufferConstructor {
readonly prototype: SharedArrayBuffer;
prototype: SharedArrayBuffer;
new (byteLength: number): SharedArrayBuffer;
}
declare var SharedArrayBuffer: SharedArrayBufferConstructor;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/es2018.asyncgenerator.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface AsyncGeneratorFunction {
/**
* A reference to the prototype.
*/
readonly prototype: AsyncGenerator;
prototype: AsyncGenerator;
}

interface AsyncGeneratorFunctionConstructor {
Expand All @@ -55,5 +55,5 @@ interface AsyncGeneratorFunctionConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: AsyncGeneratorFunction;
prototype: AsyncGeneratorFunction;
}
2 changes: 1 addition & 1 deletion src/lib/es2018.intl.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ declare namespace Intl {
interface PluralRulesConstructor {
new (locales?: string | readonly string[], options?: PluralRulesOptions): PluralRules;
(locales?: string | readonly string[], options?: PluralRulesOptions): PluralRules;
supportedLocalesOf(locales: string | readonly string[], options?: { localeMatcher?: "lookup" | "best fit"; }): string[];
supportedLocalesOf(locales: string | readonly string[], options?: { readonly localeMatcher?: "lookup" | "best fit"; }): string[];
}

const PluralRules: PluralRulesConstructor;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/es2020.bigint.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ interface BigInt {

interface BigIntConstructor {
(value: bigint | boolean | number | string): bigint;
readonly prototype: BigInt;
prototype: BigInt;

/**
* Interprets the low bits of a BigInt as a 2's-complement signed integer.
Expand Down Expand Up @@ -370,7 +370,7 @@ interface BigInt64Array {
}

interface BigInt64ArrayConstructor {
readonly prototype: BigInt64Array;
prototype: BigInt64Array;
new (length?: number): BigInt64Array;
new (array: Iterable<bigint>): BigInt64Array;
new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): BigInt64Array;
Expand Down Expand Up @@ -642,7 +642,7 @@ interface BigUint64Array {
}

interface BigUint64ArrayConstructor {
readonly prototype: BigUint64Array;
prototype: BigUint64Array;
new (length?: number): BigUint64Array;
new (array: Iterable<bigint>): BigUint64Array;
new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): BigUint64Array;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2020.intl.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ declare namespace Intl {
*
* [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/supportedLocalesOf).
*/
supportedLocalesOf(locales?: LocalesArgument, options?: { localeMatcher?: RelativeTimeFormatLocaleMatcher; }): UnicodeBCP47LocaleIdentifier[];
supportedLocalesOf(locales?: LocalesArgument, options?: { readonly localeMatcher?: RelativeTimeFormatLocaleMatcher; }): UnicodeBCP47LocaleIdentifier[];
};

interface CollatorConstructor {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2021.promise.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface AggregateError extends Error {
interface AggregateErrorConstructor {
new (errors: Iterable<any>, message?: string): AggregateError;
(errors: Iterable<any>, message?: string): AggregateError;
readonly prototype: AggregateError;
prototype: AggregateError;
}

declare var AggregateError: AggregateErrorConstructor;
Expand Down
Loading