From 974858a82236c3e6c5bb16759c4e0d969d9172ff Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 19:07:35 +1300 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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; From 1983d528cf1500537cb5268a7d56600c989b877a Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 23:28:52 +1300 Subject: [PATCH 5/9] test(type-utils): add conditional type tests to isTypeReadonly --- .../type-utils/tests/isTypeReadonly.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts index 5ecfc078f988..e0c195b7ec42 100644 --- a/packages/type-utils/tests/isTypeReadonly.test.ts +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -109,6 +109,34 @@ describe('isTypeReadonly', () => { ])('handles non fully readonly sets and maps', runTests); }); }); + + describe('Conditional Types', () => { + describe('is readonly', () => { + const runTests = runTestIsReadonly; + + it.each([ + [ + 'type Test = T extends readonly number[] ? readonly string[] : readonly number[];', + ], + ])('handles conditional type that are fully readonly', runTests); + + it.each([ + [ + 'type Test = T extends number[] ? readonly string[] : readonly number[];', + ], + ])('should ignore mutable conditions', runTests); + }); + }); + + describe('is not readonly', () => { + const runTests = runTestIsNotReadonly; + + it.each([ + ['type Test = T extends number[] ? string[] : number[];'], + ['type Test = T extends number[] ? string[] : readonly number[];'], + ['type Test = T extends number[] ? readonly string[] : number[];'], + ])('handles non fully readonly conditional types', runTests); + }); }); describe('treatMethodsAsReadonly', () => { From 2b0e454bc2009c9195190650f16fff23d6f1b8b5 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 9 Jan 2022 23:30:14 +1300 Subject: [PATCH 6/9] fix(type-utils): isTypeReadonly now handles conditional types fix #4420 --- packages/type-utils/src/isTypeReadonly.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/type-utils/src/isTypeReadonly.ts b/packages/type-utils/src/isTypeReadonly.ts index 7f689b5b7df1..ed59e88a5a0f 100644 --- a/packages/type-utils/src/isTypeReadonly.ts +++ b/packages/type-utils/src/isTypeReadonly.ts @@ -1,5 +1,6 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; import { + isConditionalType, isObjectType, isUnionType, isUnionOrIntersectionType, @@ -198,6 +199,20 @@ function isTypeReadonlyRecurser( return readonlyness; } + if (isConditionalType(type)) { + const result = [type.root.node.trueType, type.root.node.falseType] + .map(checker.getTypeFromTypeNode) + .every( + t => + seenTypes.has(t) || + isTypeReadonlyRecurser(checker, t, options, seenTypes) === + Readonlyness.Readonly, + ); + + const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable; + return readonlyness; + } + // all non-object, non-intersection types are readonly. // this should only be primitive types if (!isObjectType(type) && !isUnionOrIntersectionType(type)) { From 011647f1cfac6e00bc97f9d958aed6122aad1743 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 11 Jan 2022 00:36:48 +1300 Subject: [PATCH 7/9] test(type-utils): add intersections tests for isTypeReadonly --- .../type-utils/tests/isTypeReadonly.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts index 5ecfc078f988..6da69896af0e 100644 --- a/packages/type-utils/tests/isTypeReadonly.test.ts +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -109,6 +109,50 @@ describe('isTypeReadonly', () => { ])('handles non fully readonly sets and maps', runTests); }); }); + + describe('Intersection', () => { + describe('is readonly', () => { + const runTests = runTestIsReadonly; + + it.each([ + [ + 'type Test = Readonly<{ foo: string; bar: number; }> & Readonly<{ bar: number; }>;', + ], + ])('handles an intersection of 2 fully readonly types', runTests); + + it.each([ + [ + 'type Test = Readonly<{ foo: string; bar: number; }> & { foo: string; };', + ], + ])( + 'handles an intersection of a fully readonly type with a mutable subtype', + runTests, + ); + + // Array - special case. + // Note: Methods are mutable but arrays are treated special; hence no failure. + it.each([ + ['type Test = ReadonlyArray & Readonly<{ foo: string; }>;'], + [ + 'type Test = readonly [string, number] & Readonly<{ foo: string; }>;', + ], + ])('handles an intersections involving a readonly array', 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<{ bar: number; }> & { foo: string; bar: number; };', + ], + ])('handles an intersection of non fully readonly types', runTests); + }); + }); }); describe('treatMethodsAsReadonly', () => { From ba0f0ac5c8d76ef2a37bf8ac9a009dad0522a02c Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 11 Jan 2022 01:30:51 +1300 Subject: [PATCH 8/9] fix(type-utils): intersection types involving readonly arrays are now handled in most cases fix #4428 --- packages/type-utils/src/isTypeReadonly.ts | 32 ++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/type-utils/src/isTypeReadonly.ts b/packages/type-utils/src/isTypeReadonly.ts index 7f689b5b7df1..d58bf82d3d80 100644 --- a/packages/type-utils/src/isTypeReadonly.ts +++ b/packages/type-utils/src/isTypeReadonly.ts @@ -2,10 +2,10 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; import { isObjectType, isUnionType, - isUnionOrIntersectionType, unionTypeParts, isPropertyReadonlyInType, isSymbolFlagSet, + isIntersectionType, } from 'tsutils'; import * as ts from 'typescript'; import { getTypeOfPropertyOfType } from './propertyTypes'; @@ -198,9 +198,35 @@ function isTypeReadonlyRecurser( return readonlyness; } - // all non-object, non-intersection types are readonly. + if (isIntersectionType(type)) { + // Special case for handling arrays/tuples (as readonly arrays/tuples always have mutable methods). + if ( + type.types.some(t => checker.isArrayType(t) || checker.isTupleType(t)) + ) { + const allReadonlyParts = type.types.every( + t => + seenTypes.has(t) || + isTypeReadonlyRecurser(checker, t, options, seenTypes) === + Readonlyness.Readonly, + ); + return allReadonlyParts ? Readonlyness.Readonly : Readonlyness.Mutable; + } + + // Normal case. + const isReadonlyObject = isTypeReadonlyObject( + checker, + type, + options, + seenTypes, + ); + if (isReadonlyObject !== Readonlyness.UnknownType) { + return isReadonlyObject; + } + } + + // all non-object are readonly. // this should only be primitive types - if (!isObjectType(type) && !isUnionOrIntersectionType(type)) { + if (!isObjectType(type)) { return Readonlyness.Readonly; } From bd475fb1f886f42651e92f65f505504a1892e767 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 17 Jan 2022 03:19:06 -0800 Subject: [PATCH 9/9] Update isTypeReadonly.test.ts --- packages/type-utils/tests/isTypeReadonly.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts index f72a245cc263..f6f2cebd12bc 100644 --- a/packages/type-utils/tests/isTypeReadonly.test.ts +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -266,4 +266,4 @@ describe('isTypeReadonly', () => { }); }); }); -}); \ No newline at end of file +});