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

fix(type-utils): intersection types involving readonly arrays are now handled in most cases #4429

Merged
merged 23 commits into from Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
974858a
fix(type-utils): make isTypeReadonly's options param optional
RebeccaStevens Jan 9, 2022
bb209f4
test(type-utils): add basic tests for isTypeReadonly
RebeccaStevens Jan 9, 2022
468ac17
test: add union tests for isTypeReadonly
RebeccaStevens Jan 9, 2022
dfd5a24
fix(type-utils): union types always being marked as readonly
RebeccaStevens Jan 9, 2022
1983d52
test(type-utils): add conditional type tests to isTypeReadonly
RebeccaStevens Jan 9, 2022
2b0e454
fix(type-utils): isTypeReadonly now handles conditional types
RebeccaStevens Jan 9, 2022
011647f
test(type-utils): add intersections tests for isTypeReadonly
RebeccaStevens Jan 10, 2022
ba0f0ac
fix(type-utils): intersection types involving readonly arrays are now…
RebeccaStevens Jan 10, 2022
cdb7aff
Merge branch 'main' into issue-4428
Jan 11, 2022
3a3832b
Merge branch 'main' into issue-4420
RebeccaStevens Jan 11, 2022
f2a8cf3
Merge branch 'main' into issue-4418
RebeccaStevens Jan 11, 2022
491c7bb
Merge branch 'main' into issue-4428
RebeccaStevens Jan 12, 2022
669be93
Merge branch 'main' into issue-4420
RebeccaStevens Jan 17, 2022
bce11f3
Merge branch 'main' into issue-4418
RebeccaStevens Jan 17, 2022
18e4fb9
Merge branch 'main' into issue-4428
RebeccaStevens Jan 17, 2022
dc21aa0
Merge branch 'main' into issue-4428
RebeccaStevens Jan 17, 2022
5b9a818
Merge branch 'main' into issue-4420
RebeccaStevens Jan 17, 2022
7b36d90
Merge branch 'main' into issue-4418
RebeccaStevens Jan 17, 2022
727d999
Merge branch 'main' into issue-4418
RebeccaStevens Jan 17, 2022
b3122ef
Merge branch 'issue-4418' into issue-4420
RebeccaStevens Jan 17, 2022
16e6128
Merge branch 'issue-4420' into issue-4428
RebeccaStevens Jan 17, 2022
dd76efd
Merge branch 'main' into issue-4428
bradzacher Jan 17, 2022
bd475fb
Update isTypeReadonly.test.ts
bradzacher Jan 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 29 additions & 3 deletions packages/type-utils/src/isTypeReadonly.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down
182 changes: 182 additions & 0 deletions packages/type-utils/tests/isTypeReadonly.test.ts
@@ -0,0 +1,182 @@
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<readonly string[]>;'],
['type Test = Readonly<ReadonlyArray<string>>;'],
])('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<string>;'],
])('treats readonly arrays as fully readonly', runTests);

// Set and Map.
it.each([
['type Test = Readonly<ReadonlySet<string>>;'],
['type Test = Readonly<ReadonlyMap<string, string>>;'],
])('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<string>']])(
'handles non fully readonly arrays',
runTests,
);

// Set and Map.
// Note: Methods are mutable for ReadonlySet and ReadonlyMet; hence failure.
it.each([
['type Test = Set<string>;'],
['type Test = Map<string, string>;'],
['type Test = ReadonlySet<string>;'],
['type Test = ReadonlyMap<string, string>;'],
])('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<string> & 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', () => {
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<string>;'],
['type Test = ReadonlyMap<string, string>;'],
])('handles non fully readonly sets and maps', runTests);
});
});
});
});