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
Show file tree
Hide file tree
Changes from all 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
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
6 changes: 3 additions & 3 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2320,7 +2320,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
}

function bindModuleDeclaration(node: ModuleDeclaration) {
setExportContextFlag(node);
setExportContextFlag(node as Mutable<ModuleDeclaration>);
if (isAmbientModule(node)) {
if (hasSyntacticModifier(node, ModifierFlags.Export)) {
errorOnFirstToken(node, Diagnostics.export_modifier_cannot_be_applied_to_ambient_modules_and_module_augmentations_since_they_are_always_visible);
Expand Down Expand Up @@ -2778,7 +2778,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
bindChildren(node);
}
else {
bindContainer(node as HasContainerFlags, containerFlags);
bindContainer(node as Mutable<HasContainerFlags>, containerFlags);
}
parent = saveParent;
}
Expand Down Expand Up @@ -3091,7 +3091,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
}

function bindSourceFileIfExternalModule() {
setExportContextFlag(file);
setExportContextFlag(file as Mutable<SourceFile>);
if (isExternalModule(file)) {
bindSourceFileAsExternalModule();
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/builderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ export namespace BuilderState {
/**
* Returns true if oldState is reusable, that is the emitKind = module/non module has not changed
*/
export function canReuseOldState(newReferencedMap: ReadonlyManyToManyPathMap | undefined, oldState: BuilderState | undefined) {
export function canReuseOldState(newReferencedMap: ReadonlyManyToManyPathMap | undefined, oldState: Readonly<BuilderState> | undefined) {
return oldState && !oldState.referencedMap === !newReferencedMap;
}

Expand Down
118 changes: 74 additions & 44 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,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 Expand Up @@ -2506,7 +2515,7 @@ export interface TSConfig {
/** @internal */
export interface ConvertToTSConfigHost {
getCurrentDirectory(): string;
useCaseSensitiveFileNames: boolean;
readonly useCaseSensitiveFileNames: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,7 @@ export function getProperty<T>(map: MapLike<T>, key: string): T | undefined {
*
* @internal
*/
export function getOwnKeys<T>(map: MapLike<T>): string[] {
export function getOwnKeys<T>(map: Readonly<MapLike<T>>): string[] {
const keys: string[] = [];
for (const key in map) {
if (hasOwnProperty.call(map, key)) {
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4340,6 +4340,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 @@ -6363,6 +6371,10 @@
"category": "Message",
"code": 6719
},
"Ensure that 'readonly' properties remain read-only in type relationships.": {
"category": "Message",
"code": 6720
},

"Default catch clause variables as 'unknown' instead of 'any'.": {
"category": "Message",
Expand Down
9 changes: 5 additions & 4 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ import {
rangeIsOnSingleLine,
rangeStartPositionsAreOnSameLine,
readJsonOrUndefined,
ReadonlyTextRange,
removeFileExtension,
resolvePath,
RestTypeNode,
Expand Down Expand Up @@ -4629,7 +4630,7 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
*
* NOTE: You probably don't want to call this directly and should be using `emitList` or `emitExpressionList` instead.
*/
function emitNodeListItems<Child extends Node>(emit: EmitFunction, parentNode: Node | undefined, children: readonly Child[], format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector<Child> | undefined, start: number, count: number, hasTrailingComma: boolean, childrenTextRange: TextRange | undefined) {
function emitNodeListItems<Child extends Node>(emit: EmitFunction, parentNode: Node | undefined, children: readonly Child[], format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector<Child> | undefined, start: number, count: number, hasTrailingComma: boolean, childrenTextRange: ReadonlyTextRange | undefined) {
// Write the opening line terminator or leading whitespace.
const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0;
let shouldEmitInterveningComments = mayEmitInterveningComments;
Expand Down Expand Up @@ -5010,7 +5011,7 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return format & ListFormat.MultiLine ? 1 : 0;
}

function getClosingLineTerminatorCount(parentNode: Node | undefined, lastChild: Node | undefined, format: ListFormat, childrenTextRange: TextRange | undefined): number {
function getClosingLineTerminatorCount(parentNode: Node | undefined, lastChild: Node | undefined, format: ListFormat, childrenTextRange: ReadonlyTextRange | undefined): number {
if (format & ListFormat.PreserveLines || preserveSourceNewlines) {
if (format & ListFormat.PreferNewLine) {
return 1;
Expand Down Expand Up @@ -5822,7 +5823,7 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
: `//${comment.text}`;
}

function emitBodyWithDetachedComments<T extends Node>(node: T, detachedRange: TextRange, emitCallback: (node: T) => void) {
function emitBodyWithDetachedComments<T extends Node>(node: T, detachedRange: ReadonlyTextRange, emitCallback: (node: T) => void) {
enterComment();
const { pos, end } = detachedRange;
const emitFlags = getEmitFlags(node);
Expand Down Expand Up @@ -6042,7 +6043,7 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos);
}

function emitDetachedCommentsAndUpdateCommentsInfo(range: TextRange) {
function emitDetachedCommentsAndUpdateCommentsInfo(range: ReadonlyTextRange) {
const currentDetachedCommentInfo = currentSourceFile && emitDetachedComments(currentSourceFile.text, getCurrentLineMap(), writer, emitComment, range, newLine, commentsDisabled);
if (currentDetachedCommentInfo) {
if (detachedCommentsInfo) {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/factory/emitHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ObjectLiteralElementLike,
ParameterDeclaration,
PrivateIdentifier,
ReadonlyTextRange,
ScriptTarget,
setEmitFlags,
setInternalEmitFlags,
Expand Down Expand Up @@ -113,7 +114,7 @@ export interface EmitHelperFactory {
createAsyncDelegatorHelper(expression: Expression): Expression;
createAsyncValuesHelper(expression: Expression): Expression;
// ES2018 Destructuring Helpers
createRestHelper(value: Expression, elements: readonly BindingOrAssignmentElement[], computedTempVariables: readonly Expression[] | undefined, location: TextRange): Expression;
createRestHelper(value: Expression, elements: readonly BindingOrAssignmentElement[], computedTempVariables: readonly Expression[] | undefined, location: ReadonlyTextRange): Expression;
// ES2017 Helpers
createAwaiterHelper(hasLexicalThis: boolean, argumentsExpression: Expression | undefined, promiseConstructor: EntityName | Expression | undefined, parameters: readonly ParameterDeclaration[] | undefined, body: Block): Expression;
// ES2015 Helpers
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/factory/emitNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
NodeArray,
orderedRemoveItem,
PrivateIdentifier,
ReadonlyTextRange,
SnippetElement,
some,
SourceFile,
Expand Down Expand Up @@ -180,14 +181,14 @@ export function setStartsOnNewLine<T extends Node>(node: T, newLine: boolean) {
/**
* Gets a custom text range to use when emitting comments.
*/
export function getCommentRange(node: Node): TextRange {
export function getCommentRange(node: Node): ReadonlyTextRange {
return node.emitNode?.commentRange ?? node;
}

/**
* Sets a custom text range to use when emitting comments.
*/
export function setCommentRange<T extends Node>(node: T, range: TextRange) {
export function setCommentRange<T extends Node>(node: T, range: ReadonlyTextRange) {
getOrCreateEmitNode(node).commentRange = range;
return node;
}
Expand Down
39 changes: 20 additions & 19 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ import {
QuestionDotToken,
QuestionToken,
ReadonlyKeyword,
ReadonlyTextRange,
RedirectInfo,
reduceLeft,
RegularExpressionLiteral,
Expand Down Expand Up @@ -505,8 +506,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
const getJSDocPrePostfixUnaryTypeUpdateFunction = memoizeOne(<T extends JSDocType & { readonly type: TypeNode | undefined; readonly postfix: boolean; }>(kind: T["kind"]) => (node: T, type: T["type"]) => updateJSDocPrePostfixUnaryTypeWorker<T>(kind, node, type));
const getJSDocSimpleTagCreateFunction = memoizeOne(<T extends JSDocTag>(kind: T["kind"]) => (tagName: Identifier | undefined, comment?: NodeArray<JSDocComment>) => createJSDocSimpleTagWorker(kind, tagName, comment));
const getJSDocSimpleTagUpdateFunction = memoizeOne(<T extends JSDocTag>(kind: T["kind"]) => (node: T, tagName: Identifier | undefined, comment?: NodeArray<JSDocComment>) => updateJSDocSimpleTagWorker(kind, node, tagName, comment));
const getJSDocTypeLikeTagCreateFunction = memoizeOne(<T extends JSDocTag & { typeExpression?: JSDocTypeExpression; }>(kind: T["kind"]) => (tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: NodeArray<JSDocComment>) => createJSDocTypeLikeTagWorker(kind, tagName, typeExpression, comment));
const getJSDocTypeLikeTagUpdateFunction = memoizeOne(<T extends JSDocTag & { typeExpression?: JSDocTypeExpression; }>(kind: T["kind"]) => (node: T, tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: NodeArray<JSDocComment>) => updateJSDocTypeLikeTagWorker(kind, node, tagName, typeExpression, comment));
const getJSDocTypeLikeTagCreateFunction = memoizeOne(<T extends JSDocTag & { readonly typeExpression?: JSDocTypeExpression; }>(kind: T["kind"]) => (tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: NodeArray<JSDocComment>) => createJSDocTypeLikeTagWorker(kind, tagName, typeExpression, comment));
const getJSDocTypeLikeTagUpdateFunction = memoizeOne(<T extends JSDocTag & { readonly typeExpression?: JSDocTypeExpression; }>(kind: T["kind"]) => (node: T, tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: NodeArray<JSDocComment>) => updateJSDocTypeLikeTagWorker(kind, node, tagName, typeExpression, comment));

const factory: NodeFactory = {
get parenthesizer() {
Expand Down Expand Up @@ -1214,10 +1215,10 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
return node;
}

function finishUpdateBaseSignatureDeclaration<T extends SignatureDeclarationBase>(updated: Mutable<T>, original: T) {
function finishUpdateBaseSignatureDeclaration<T extends SignatureDeclarationBase>(updated: T, original: T) {
if (updated !== original) {
// copy children used for quick info
updated.typeArguments = original.typeArguments;
(updated as Mutable<T>).typeArguments = original.typeArguments;
}
return update(updated, original);
}
Expand Down Expand Up @@ -1507,7 +1508,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
if (transformFlags) {
node.transformFlags |= transformFlags;
}
return node;
return node as Token<TKind>;
}

//
Expand Down Expand Up @@ -1750,10 +1751,10 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

function finishUpdatePropertySignature(updated: Mutable<PropertySignature>, original: PropertySignature) {
function finishUpdatePropertySignature(updated: PropertySignature, original: PropertySignature) {
if (updated !== original) {
// copy children used only for error reporting
updated.initializer = original.initializer;
(updated as Mutable<PropertySignature>).initializer = original.initializer;
}
return update(updated, original);
}
Expand Down Expand Up @@ -1966,10 +1967,10 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

function finishUpdateClassStaticBlockDeclaration(updated: Mutable<ClassStaticBlockDeclaration>, original: ClassStaticBlockDeclaration) {
function finishUpdateClassStaticBlockDeclaration(updated: ClassStaticBlockDeclaration, original: ClassStaticBlockDeclaration) {
if (updated !== original) {
// copy children used only for error reporting
updated.modifiers = original.modifiers;
(updated as Mutable<ClassStaticBlockDeclaration>).modifiers = original.modifiers;
}
return update(updated, original);
}
Expand Down Expand Up @@ -2343,10 +2344,10 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

function finishUpdateFunctionTypeNode(updated: Mutable<FunctionTypeNode>, original: FunctionTypeNode) {
function finishUpdateFunctionTypeNode(updated: FunctionTypeNode, original: FunctionTypeNode) {
if (updated !== original) {
// copy children used only for error reporting
updated.modifiers = original.modifiers;
(updated as Mutable<FunctionTypeNode>).modifiers = original.modifiers;
}
return finishUpdateBaseSignatureDeclaration(updated, original);
}
Expand Down Expand Up @@ -3027,7 +3028,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createCallExpression(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) {
function createCallExpression(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined): CallExpression {
const node = createBaseCallExpression(
parenthesizerRules().parenthesizeLeftSideOfAccess(expression, /*optionalChain*/ false),
/*questionDotToken*/ undefined,
Expand Down Expand Up @@ -3408,7 +3409,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createBinaryExpression(left: Expression, operator: BinaryOperator | BinaryOperatorToken, right: Expression) {
function createBinaryExpression(left: Expression, operator: BinaryOperator | BinaryOperatorToken, right: Expression): BinaryExpression {
const node = createBaseDeclaration<BinaryExpression>(SyntaxKind.BinaryExpression);
const operatorToken = asToken(operator);
const operatorKind = operatorToken.kind;
Expand Down Expand Up @@ -3567,7 +3568,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createTemplateLiteralLikeNode(kind: TemplateLiteralToken["kind"], text: string, rawText: string | undefined, templateFlags: TokenFlags | undefined) {
function createTemplateLiteralLikeNode(kind: TemplateLiteralToken["kind"], text: string, rawText: string | undefined, templateFlags: TokenFlags | undefined): TemplateLiteralLikeNode {
if (kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
return createTemplateLiteralLikeDeclaration(kind, text, rawText, templateFlags);
}
Expand Down Expand Up @@ -5474,7 +5475,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
// createJSDocThisTag
// createJSDocEnumTag
// createJSDocSatisfiesTag
function createJSDocTypeLikeTagWorker<T extends JSDocTag & { typeExpression?: JSDocTypeExpression; }>(kind: T["kind"], tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: string | NodeArray<JSDocComment>) {
function createJSDocTypeLikeTagWorker<T extends JSDocTag & { readonly typeExpression?: JSDocTypeExpression; }>(kind: T["kind"], tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: string | NodeArray<JSDocComment>) {
const node = createBaseJSDocTag<T>(kind, tagName ?? createIdentifier(getDefaultTagNameForKind(kind)), comment);
node.typeExpression = typeExpression;
return node;
Expand All @@ -5486,7 +5487,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
// updateJSDocThisTag
// updateJSDocEnumTag
// updateJSDocSatisfiesTag
function updateJSDocTypeLikeTagWorker<T extends JSDocTag & { typeExpression?: JSDocTypeExpression; }>(kind: T["kind"], node: T, tagName: Identifier = getDefaultTagName(node), typeExpression: JSDocTypeExpression | undefined, comment: string | NodeArray<JSDocComment> | undefined) {
function updateJSDocTypeLikeTagWorker<T extends JSDocTag & { readonly typeExpression?: JSDocTypeExpression; }>(kind: T["kind"], node: T, tagName: Identifier = getDefaultTagName(node), typeExpression: JSDocTypeExpression | undefined, comment: string | NodeArray<JSDocComment> | undefined) {
return node.tagName !== tagName
|| node.typeExpression !== typeExpression
|| node.comment !== comment
Expand Down Expand Up @@ -6354,7 +6355,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
return node;
}
if (isSourceFile(node)) {
return cloneSourceFile(node) as T & SourceFile;
return cloneSourceFile(node) as SourceFile as T & SourceFile;
}
if (isGeneratedIdentifier(node)) {
return cloneGeneratedIdentifier(node) as T & GeneratedIdentifier;
Expand Down Expand Up @@ -7154,7 +7155,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
return variableDeclaration;
}

function update<T extends Node>(updated: Mutable<T>, original: T): T {
function update<T extends Node>(updated: T, original: T): T {
if (updated !== original) {
setOriginal(updated, original);
setTextRange(updated, original);
Expand Down Expand Up @@ -7511,7 +7512,7 @@ function mergeEmitNode(sourceEmitNode: EmitNode, destEmitNode: EmitNode | undefi
return destEmitNode;
}

function mergeTokenSourceMapRanges(sourceRanges: (TextRange | undefined)[], destRanges: (TextRange | undefined)[]) {
function mergeTokenSourceMapRanges(sourceRanges: (ReadonlyTextRange | undefined)[], destRanges: (ReadonlyTextRange | undefined)[]) {
if (!destRanges) destRanges = [];
for (const key in sourceRanges) {
destRanges[key] = sourceRanges[key];
Expand Down
Loading
Loading