diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts new file mode 100644 index 000000000000..7153b44d7769 --- /dev/null +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -0,0 +1,219 @@ +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, + }; + } + + describe('basics', () => { + describe('is readonly', () => { + describe('default options', () => { + it('handles a record with only readonly props', () => { + const { type, checker } = getType( + `type Test = { readonly bar: string; };`, + ); + + const result = isTypeReadonly(checker, type); + expect(result).toBe(true); + }); + + it('handles a shallowly mutable record wrapped in Readonly', () => { + const { type: receiver, checker } = getType( + `type Test = Readonly<{ bar: string; }>;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(true); + }); + + it('handles an readonly readonly array', () => { + const { type: receiver, checker } = getType( + `type Test = Readonly;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(true); + }); + + it('handles an readonly ReadonlyArray', () => { + const { type: receiver, checker } = getType( + `type Test = Readonly>;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(true); + }); + + it('handles an readonly ReadonlySet', () => { + const { type: receiver, checker } = getType( + `type Test = Readonly>;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(true); + }); + + // Methods are mutable but arrays have a special exemption; hence no failure. + it('handles a readonly array', () => { + const { type: receiver, checker } = getType( + `type Test = readonly string[];`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(true); + }); + + // Methods are mutable but arrays have a special exemption; hence no failure. + it('handles a ReadonlyArray', () => { + const { type: receiver, checker } = getType( + `type Test = ReadonlyArray;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(true); + }); + }); + + describe('treatMethodsAsReadonly', () => { + const options: ReadonlynessOptions = { + treatMethodsAsReadonly: true, + }; + + it('handles a ReadonlySet', () => { + const { type: receiver, checker } = getType( + `type Test = ReadonlySet;`, + ); + + const result = isTypeReadonly(checker, receiver, options); + expect(result).toBe(true); + }); + + it('handles a ReadonlyMap', () => { + const { type: receiver, checker } = getType( + `type Test = ReadonlyMap;`, + ); + + const result = isTypeReadonly(checker, receiver, options); + expect(result).toBe(true); + }); + }); + }); + + describe('is not readonly', () => { + it('fails with record with mutable props', () => { + const { type: receiver, checker } = getType( + `type Test = { bar: string; };`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(false); + }); + + it('fails with a mutable array', () => { + const { type: receiver, checker } = getType(`type Test = string[];`); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(false); + }); + + it('fails with a mutable Array', () => { + const { type: receiver, checker } = getType( + `type Test = Array;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(false); + }); + + // Methods are mutable; hence failure. + it('fails with a ReadonlySet', () => { + const { type: receiver, checker } = getType( + `type Test = ReadonlySet;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(false); + }); + + // Methods are mutable; hence failure. + it('fails with a ReadonlyMap', () => { + const { type: receiver, checker } = getType( + `type Test = ReadonlyMap;`, + ); + + const result = isTypeReadonly(checker, receiver); + expect(result).toBe(false); + }); + }); + }); + + describe('Intersection', () => { + describe('is readonly', () => { + it('handles an intersection of 2', () => { + const { type, checker } = getType( + `type Test = Readonly<{ foo: string; bar: number; }> & Readonly<{ bar: number; }>;`, + ); + + const result = isTypeReadonly(checker, type); + expect(result).toBe(true); + }); + }); + + describe('is not readonly', () => { + it('fails with an intersection of mutables', () => { + const { type, checker } = getType( + `type Test = { foo: string; bar: number; } & { bar: number; };`, + ); + + const result = isTypeReadonly(checker, type); + expect(result).toBe(false); + }); + + it('fails with an intersection of mutable to readonly', () => { + const { type, checker } = getType( + `type Test = { foo: string; bar: number; } & Readonly<{ bar: number; }>;`, + ); + + const result = isTypeReadonly(checker, type); + expect(result).toBe(false); + }); + + it('fails with an intersection of readonly to mutable', () => { + const { type, checker } = getType( + `type Test = { foo: string; bar: number; } & Readonly<{ bar: number; }>;`, + ); + + const result = isTypeReadonly(checker, type); + expect(result).toBe(false); + }); + }); + }); + }); +});