From 974858a82236c3e6c5bb16759c4e0d969d9172ff Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 19:07:35 +1300 Subject: [PATCH 1/4] fix(type-utils): make isTypeReadonly's options param optional fix #4410 --- packages/type-utils/src/isTypeReadonly.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/type-utils/src/isTypeReadonly.ts b/packages/type-utils/src/isTypeReadonly.ts index c6bc0f5761a4..7f689b5b7df1 100644 --- a/packages/type-utils/src/isTypeReadonly.ts +++ b/packages/type-utils/src/isTypeReadonly.ts @@ -243,7 +243,7 @@ function isTypeReadonlyRecurser( function isTypeReadonly( checker: ts.TypeChecker, type: ts.Type, - options: ReadonlynessOptions, + options: ReadonlynessOptions = readonlynessOptionsDefaults, ): boolean { return ( isTypeReadonlyRecurser(checker, type, options, new Set()) === From bb209f41e5961931fd27e5bc26b39136efeba13d Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 19:59:56 +1300 Subject: [PATCH 2/4] test(type-utils): add basic tests for isTypeReadonly --- .../type-utils/tests/isTypeReadonly.test.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 packages/type-utils/tests/isTypeReadonly.test.ts diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts new file mode 100644 index 000000000000..5ecfc078f988 --- /dev/null +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -0,0 +1,138 @@ +import * as ts from 'typescript'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { parseForESLint } from '@typescript-eslint/parser'; +import { + isTypeReadonly, + type ReadonlynessOptions, +} from '../src/isTypeReadonly'; +import path from 'path'; + +describe('isTypeReadonly', () => { + const rootDir = path.join(__dirname, 'fixtures'); + + describe('TSTypeAliasDeclaration ', () => { + function getType(code: string): { + type: ts.Type; + checker: ts.TypeChecker; + } { + const { ast, services } = parseForESLint(code, { + project: './tsconfig.json', + filePath: path.join(rootDir, 'file.ts'), + tsconfigRootDir: rootDir, + }); + const checker = services.program.getTypeChecker(); + const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap; + + const declaration = ast.body[0] as TSESTree.TSTypeAliasDeclaration; + return { + type: checker.getTypeAtLocation( + esTreeNodeToTSNodeMap.get(declaration.id), + ), + checker, + }; + } + + function runTestForAliasDeclaration( + code: string, + options: ReadonlynessOptions | undefined, + expected: boolean, + ): void { + const { type, checker } = getType(code); + + const result = isTypeReadonly(checker, type, options); + expect(result).toBe(expected); + } + + describe('default options', () => { + const options = undefined; + + function runTestIsReadonly(code: string): void { + runTestForAliasDeclaration(code, options, true); + } + + function runTestIsNotReadonly(code: string): void { + runTestForAliasDeclaration(code, options, false); + } + + describe('basics', () => { + describe('is readonly', () => { + const runTests = runTestIsReadonly; + + // Record. + it.each([ + ['type Test = { readonly bar: string; };'], + ['type Test = Readonly<{ bar: string; }>;'], + ])('handles fully readonly records', runTests); + + // Array. + it.each([ + ['type Test = Readonly;'], + ['type Test = Readonly>;'], + ])('handles fully readonly arrays', runTests); + + // Array - special case. + // Note: Methods are mutable but arrays are treated special; hence no failure. + it.each([ + ['type Test = readonly string[];'], + ['type Test = ReadonlyArray;'], + ])('treats readonly arrays as fully readonly', runTests); + + // Set and Map. + it.each([ + ['type Test = Readonly>;'], + ['type Test = Readonly>;'], + ])('handles fully readonly sets and maps', runTests); + }); + + describe('is not readonly', () => { + const runTests = runTestIsNotReadonly; + + // Record. + it.each([ + ['type Test = { foo: string; };'], + ['type Test = { foo: string; readonly bar: number; };'], + ])('handles non fully readonly records', runTests); + + // Array. + it.each([['type Test = string[]'], ['type Test = Array']])( + 'handles non fully readonly arrays', + runTests, + ); + + // Set and Map. + // Note: Methods are mutable for ReadonlySet and ReadonlyMet; hence failure. + it.each([ + ['type Test = Set;'], + ['type Test = Map;'], + ['type Test = ReadonlySet;'], + ['type Test = ReadonlyMap;'], + ])('handles non fully readonly sets and maps', runTests); + }); + }); + }); + + describe('treatMethodsAsReadonly', () => { + const options: ReadonlynessOptions = { + treatMethodsAsReadonly: true, + }; + + function runTestIsReadonly(code: string): void { + runTestForAliasDeclaration(code, options, true); + } + + // function runTestIsNotReadonly(code: string): void { + // runTestForAliasDeclaration(code, options, false); + // } + + describe('is readonly', () => { + const runTests = runTestIsReadonly; + + // Set and Map. + it.each([ + ['type Test = ReadonlySet;'], + ['type Test = ReadonlyMap;'], + ])('handles non fully readonly sets and maps', runTests); + }); + }); + }); +}); From 468ac17c48b739f2c758a53f878f085d5deaf67a Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 21:38:45 +1300 Subject: [PATCH 3/4] test: add union tests for isTypeReadonly --- .../type-utils/tests/isTypeReadonly.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts index 5ecfc078f988..f2d277ff88e1 100644 --- a/packages/type-utils/tests/isTypeReadonly.test.ts +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -109,6 +109,33 @@ describe('isTypeReadonly', () => { ])('handles non fully readonly sets and maps', runTests); }); }); + + describe('Union', () => { + describe('is readonly', () => { + const runTests = runTestIsReadonly; + + it.each([ + [ + 'type Test = Readonly<{ foo: string; bar: number; }> & Readonly<{ bar: number; }>;', + ], + ['type Test = readonly string[] | readonly number[];'], + ])('handles a union of 2 fully readonly types', runTests); + }); + + describe('is not readonly', () => { + const runTests = runTestIsNotReadonly; + + it.each([ + ['type Test = { foo: string; bar: number; } | { bar: number; };'], + [ + 'type Test = { foo: string; bar: number; } | Readonly<{ bar: number; }>;', + ], + [ + 'type Test = Readonly<{ foo: string; bar: number; }> | { bar: number; };', + ], + ])('handles a union of non fully readonly types', runTests); + }); + }); }); describe('treatMethodsAsReadonly', () => { From dfd5a248305cd499cec9e8d8c3bb43c364285270 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 21:39:32 +1300 Subject: [PATCH 4/4] fix(type-utils): union types always being marked as readonly fix #4418 --- packages/type-utils/src/isTypeReadonly.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/type-utils/src/isTypeReadonly.ts b/packages/type-utils/src/isTypeReadonly.ts index 7f689b5b7df1..7d24aabaa0bc 100644 --- a/packages/type-utils/src/isTypeReadonly.ts +++ b/packages/type-utils/src/isTypeReadonly.ts @@ -192,7 +192,8 @@ function isTypeReadonlyRecurser( const result = unionTypeParts(type).every( t => seenTypes.has(t) || - isTypeReadonlyRecurser(checker, t, options, seenTypes), + isTypeReadonlyRecurser(checker, t, options, seenTypes) === + Readonlyness.Readonly, ); const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable; return readonlyness;