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

Enable --enforceReadonly in TypeScript code base #59326

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
4d1a5ff
Enable enforceReadonly mode
ahejlsberg Jul 17, 2024
5cf719a
Fix inconsistencies uncovered by --enforceReadonly
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
Prev Previous commit
Next Next commit
Enforce read-only semantics in generic mapped types
ahejlsberg committed Apr 24, 2024
commit dd6c3a8909042ce8d9449dc1d4e43be93f500e89
54 changes: 32 additions & 22 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -1318,6 +1318,10 @@ const enum MappedTypeModifiers {
ExcludeReadonly = 1 << 1,
IncludeOptional = 1 << 2,
ExcludeOptional = 1 << 3,
Include = IncludeReadonly | IncludeOptional,
Exclude = ExcludeReadonly | ExcludeOptional,
Readonly = IncludeReadonly | ExcludeReadonly,
Optional = IncludeOptional | ExcludeOptional,
}

const enum MappedTypeNameTypeKind {
@@ -13875,27 +13879,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
(declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0);
}

// Return -1, 0, or 1, where -1 means optionality is stripped (i.e. -?), 0 means optionality is unchanged, and 1 means
// optionality is added (i.e. +?).
function getMappedTypeOptionality(type: MappedType): number {
const modifiers = getMappedTypeModifiers(type);
return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0;
}

// Return -1, 0, or 1, for stripped, unchanged, or added optionality respectively. When a homomorphic mapped type doesn't
// modify optionality, recursively consult the optionality of the type being mapped over to see if it strips or adds optionality.
// For intersections, return -1 or 1 when all constituents strip or add optionality, otherwise return 0.
function getCombinedMappedTypeOptionality(type: Type): number {
// Return the effective modifiers of a mapped type. When a homomorphic mapped type doesn't include modifiers, instead
// recursively obtain the effective modifiers of the type being mapped over. For intersections, return 0 if the effective
// modifiers differ between constituent types.
function getEffectiveMappedTypeModifiers(type: Type, mask: MappedTypeModifiers): number {
if (getObjectFlags(type) & ObjectFlags.Mapped) {
return getMappedTypeOptionality(type as MappedType) || getCombinedMappedTypeOptionality(getModifiersTypeFromMappedType(type as MappedType));
return getMappedTypeModifiers(type as MappedType) & mask ||
getEffectiveMappedTypeModifiers(getModifiersTypeFromMappedType(type as MappedType), mask);
}
if (type.flags & TypeFlags.Intersection) {
const optionality = getCombinedMappedTypeOptionality((type as IntersectionType).types[0]);
return every((type as IntersectionType).types, (t, i) => i === 0 || getCombinedMappedTypeOptionality(t) === optionality) ? optionality : 0;
const modifiers = getEffectiveMappedTypeModifiers((type as IntersectionType).types[0], mask);
return every((type as IntersectionType).types, (t, i) => i === 0 || getEffectiveMappedTypeModifiers(t, mask) === modifiers) ? modifiers : 0;
}
return 0;
}

// Return -1, 0, or 1, where -1 means optionality is stripped (i.e. -?), 0 means optionality is unchanged, and 1 means
// optionality is added (i.e. +?).
function getMappedTypeModifiersRank(type: Type, mask: MappedTypeModifiers): number {
const modifiers = getEffectiveMappedTypeModifiers(type, mask);
return modifiers & MappedTypeModifiers.Exclude ? -1 : modifiers & MappedTypeModifiers.Include ? 1 : 0;
}

function isPartialMappedType(type: Type) {
return !!(getObjectFlags(type) & ObjectFlags.Mapped && getMappedTypeModifiers(type as MappedType) & MappedTypeModifiers.IncludeOptional);
}
@@ -18439,10 +18444,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [index]);
const templateMapper = combineTypeMappers(objectType.mapper, mapper);
const instantiatedTemplateType = instantiateType(getTemplateTypeFromMappedType(objectType.target as MappedType || objectType), templateMapper);
const isOptional = getMappedTypeOptionality(objectType) > 0 || (isGenericType(objectType) ?
getCombinedMappedTypeOptionality(getModifiersTypeFromMappedType(objectType)) > 0 :
const isOptional = getMappedTypeModifiers(objectType) & MappedTypeModifiers.IncludeOptional || (isGenericType(objectType) ?
getEffectiveMappedTypeModifiers(getModifiersTypeFromMappedType(objectType), MappedTypeModifiers.Optional) & MappedTypeModifiers.IncludeOptional :
couldAccessOptionalProperty(objectType, index));
return addOptionality(instantiatedTemplateType, /*isProperty*/ true, isOptional);
return addOptionality(instantiatedTemplateType, /*isProperty*/ true, !!isOptional);
}

// Return true if an indexed access with the given object and index types could access an optional property.
@@ -22399,7 +22404,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (targetFlags & TypeFlags.TypeParameter) {
// A source type { [P in Q]: X } is related to a target type T if keyof T is related to Q and X is related to T[Q].
if (getObjectFlags(source) & ObjectFlags.Mapped && !(source as MappedType).declaration.nameType && isRelatedTo(getIndexType(target), getConstraintTypeFromMappedType(source as MappedType), RecursionFlags.Both)) {
if (!(getMappedTypeModifiers(source as MappedType) & MappedTypeModifiers.IncludeOptional)) {
if (
!(getMappedTypeModifiers(source as MappedType) & MappedTypeModifiers.IncludeOptional) &&
!(enforceReadonly && getMappedTypeModifiers(source as MappedType) & MappedTypeModifiers.IncludeReadonly)
) {
const templateType = getTemplateTypeFromMappedType(source as MappedType);
const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(source as MappedType));
if (result = isRelatedTo(templateType, indexedAccessType, RecursionFlags.Both, reportErrors)) {
@@ -22522,7 +22530,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const keysRemapped = !!target.declaration.nameType;
const templateType = getTemplateTypeFromMappedType(target);
const modifiers = getMappedTypeModifiers(target);
if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) {
if (!(modifiers & MappedTypeModifiers.ExcludeOptional) && !(enforceReadonly && modifiers & MappedTypeModifiers.ExcludeReadonly)) {
// If the mapped type has shape `{ [P in Q]: T[P] }`,
// source `S` is related to target if `T` = `S`, i.e. `S` is related to `{ [P in Q]: S[P] }`.
if (
@@ -22908,11 +22916,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// that S and T are contra-variant whereas X and Y are co-variant.
function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary {
const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) :
getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target));
getMappedTypeModifiersRank(source, MappedTypeModifiers.Optional) <= getMappedTypeModifiersRank(target, MappedTypeModifiers.Optional) &&
(!enforceReadonly || getMappedTypeModifiersRank(source, MappedTypeModifiers.Readonly) <= getMappedTypeModifiersRank(target, MappedTypeModifiers.Readonly))
);
if (modifiersRelated) {
let result: Ternary;
const targetConstraint = getConstraintTypeFromMappedType(target);
const sourceConstraint = instantiateType(getConstraintTypeFromMappedType(source), getCombinedMappedTypeOptionality(source) < 0 ? reportUnmeasurableMapper : reportUnreliableMapper);
const sourceConstraint = instantiateType(getConstraintTypeFromMappedType(source), getEffectiveMappedTypeModifiers(source, MappedTypeModifiers.Optional) & MappedTypeModifiers.ExcludeOptional ? reportUnmeasurableMapper : reportUnreliableMapper);
if (result = isRelatedTo(targetConstraint, sourceConstraint, RecursionFlags.Both, reportErrors)) {
const mapper = createTypeMapper([getTypeParameterFromMappedType(source)], [getTypeParameterFromMappedType(target)]);
if (instantiateType(getNameTypeFromMappedType(source), mapper) === instantiateType(getNameTypeFromMappedType(target), mapper)) {
@@ -23081,7 +23091,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// This is only applied during the strictSubtypeRelation -- currently used in subtype reduction
if (
(relation === strictSubtypeRelation || enforceReadonly) &&
isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp) && !(targetProp.flags & SymbolFlags.Method)
isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp)
) {
if (reportErrors) {
reportError(Diagnostics.Property_0_is_readonly_in_the_source_but_not_in_the_target, symbolToString(targetProp));