Skip to content

Commit ada5ed8

Browse files
chore(prefer-readonly-type-declaration): add upstream utils to this project
Once typescript-eslint/typescript-eslint#3658 is merged, this commit can be reverted
1 parent 0acf24e commit ada5ed8

File tree

6 files changed

+271
-3
lines changed

6 files changed

+271
-3
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"project": ["./tsconfig.json", "./tests/tsconfig.json", "./cz-adapter/tsconfig.json"],
2727
"sourceType": "module"
2828
},
29-
"ignorePatterns": ["build/", "coverage/", "lib/", "**/*.js"],
29+
"ignorePatterns": ["build/", "coverage/", "lib/", "**/*.js", "src/util/upstream/**/*"],
3030
"rules": {
3131
"@typescript-eslint/no-unnecessary-condition": "off",
3232
"import/no-relative-parent-imports": "error",

.nycrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"src/**/*"
55
],
66
"exclude": [
7-
"src/util/conditional-imports/**/*"
7+
"src/util/conditional-imports/**/*",
8+
"src/util/upstream/**/*"
89
],
910
"reporter": [
1011
"lcov",

src/util/rule.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { Node, Type } from "typescript";
66
import { shouldIgnore } from "~/common/ignore-options";
77
import { version } from "~/package.json";
88

9+
import { isTypeReadonly } from "./upstream/eslint-typescript/isTypeReadonly";
10+
911
export type BaseOptions = object;
1012

1113
// "url" will be set automatically.
@@ -150,7 +152,8 @@ export function isReadonly<Context extends RuleContext<string, BaseOptions>>(
150152

151153
const checker = parserServices.program.getTypeChecker();
152154
const type = getTypeOfNode(node, context);
153-
return ESLintUtils.isTypeReadonly(checker, type!);
155+
// return ESLintUtils.isTypeReadonly(checker, type!);
156+
return isTypeReadonly(checker, type!);
154157
}
155158

156159
/**
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// @ts-nocheck
2+
3+
import tsutils from "~/conditional-imports/tsutils";
4+
import ts from "~/conditional-imports/typescript";
5+
6+
import { nullThrows, NullThrowsReasons } from "./nullThrows";
7+
import { getTypeOfPropertyOfType } from "./propertyTypes";
8+
9+
const enum Readonlyness {
10+
/** the type cannot be handled by the function */
11+
UnknownType = 1,
12+
/** the type is mutable */
13+
Mutable = 2,
14+
/** the type is readonly */
15+
Readonly = 3,
16+
}
17+
18+
function isTypeReadonlyArrayOrTuple(
19+
checker: ts.TypeChecker,
20+
type: ts.Type,
21+
seenTypes: Set<ts.Type>
22+
): Readonlyness {
23+
function checkTypeArguments(arrayType: ts.TypeReference): Readonlyness {
24+
const typeArguments = checker.getTypeArguments
25+
? checker.getTypeArguments(arrayType)
26+
: arrayType.typeArguments ?? [];
27+
28+
// this shouldn't happen in reality as:
29+
// - tuples require at least 1 type argument
30+
// - ReadonlyArray requires at least 1 type argument
31+
/* istanbul ignore if */ if (typeArguments.length === 0) {
32+
return Readonlyness.Readonly;
33+
}
34+
35+
// validate the element types are also readonly
36+
if (
37+
typeArguments.some(
38+
(typeArg) =>
39+
isTypeReadonlyRecurser(checker, typeArg, seenTypes) ===
40+
Readonlyness.Mutable
41+
)
42+
) {
43+
return Readonlyness.Mutable;
44+
}
45+
return Readonlyness.Readonly;
46+
}
47+
48+
if (checker.isArrayType(type)) {
49+
const symbol = nullThrows(
50+
type.getSymbol(),
51+
NullThrowsReasons.MissingToken("symbol", "array type")
52+
);
53+
const escapedName = symbol.getEscapedName();
54+
if (escapedName === "Array") {
55+
return Readonlyness.Mutable;
56+
}
57+
58+
return checkTypeArguments(type);
59+
}
60+
61+
if (checker.isTupleType(type)) {
62+
if (!type.target.readonly) {
63+
return Readonlyness.Mutable;
64+
}
65+
66+
return checkTypeArguments(type);
67+
}
68+
69+
return Readonlyness.UnknownType;
70+
}
71+
72+
function isTypeReadonlyObject(
73+
checker: ts.TypeChecker,
74+
type: ts.Type,
75+
seenTypes: Set<ts.Type>
76+
): Readonlyness {
77+
function checkIndexSignature(kind: ts.IndexKind): Readonlyness {
78+
const indexInfo = checker.getIndexInfoOfType(type, kind);
79+
if (indexInfo) {
80+
return indexInfo.isReadonly
81+
? Readonlyness.Readonly
82+
: Readonlyness.Mutable;
83+
}
84+
85+
return Readonlyness.UnknownType;
86+
}
87+
88+
const properties = type.getProperties();
89+
if (properties.length) {
90+
// ensure the properties are marked as readonly
91+
for (const property of properties) {
92+
if (!tsutils.isPropertyReadonlyInType(type, property.getEscapedName(), checker)) {
93+
return Readonlyness.Mutable;
94+
}
95+
}
96+
97+
// all properties were readonly
98+
// now ensure that all of the values are readonly also.
99+
100+
// do this after checking property readonly-ness as a perf optimization,
101+
// as we might be able to bail out early due to a mutable property before
102+
// doing this deep, potentially expensive check.
103+
for (const property of properties) {
104+
const propertyType = nullThrows(
105+
getTypeOfPropertyOfType(checker, type, property),
106+
NullThrowsReasons.MissingToken(`property "${property.name}"`, "type")
107+
);
108+
109+
// handle recursive types.
110+
// we only need this simple check, because a mutable recursive type will break via the above prop readonly check
111+
if (seenTypes.has(propertyType)) {
112+
continue;
113+
}
114+
115+
if (
116+
isTypeReadonlyRecurser(checker, propertyType, seenTypes) ===
117+
Readonlyness.Mutable
118+
) {
119+
return Readonlyness.Mutable;
120+
}
121+
}
122+
}
123+
124+
const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String);
125+
if (isStringIndexSigReadonly === Readonlyness.Mutable) {
126+
return isStringIndexSigReadonly;
127+
}
128+
129+
const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number);
130+
if (isNumberIndexSigReadonly === Readonlyness.Mutable) {
131+
return isNumberIndexSigReadonly;
132+
}
133+
134+
return Readonlyness.Readonly;
135+
}
136+
137+
// a helper function to ensure the seenTypes map is always passed down, except by the external caller
138+
function isTypeReadonlyRecurser(
139+
checker: ts.TypeChecker,
140+
type: ts.Type,
141+
seenTypes: Set<ts.Type>
142+
): Readonlyness.Readonly | Readonlyness.Mutable {
143+
seenTypes.add(type);
144+
145+
if (tsutils.isUnionType(type)) {
146+
// all types in the union must be readonly
147+
const result = tsutils.unionTypeParts(type).every((t) =>
148+
isTypeReadonlyRecurser(checker, t, seenTypes)
149+
);
150+
const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable;
151+
return readonlyness;
152+
}
153+
154+
// all non-object, non-intersection types are readonly.
155+
// this should only be primitive types
156+
if (!tsutils.isObjectType(type) && !tsutils.isUnionOrIntersectionType(type)) {
157+
return Readonlyness.Readonly;
158+
}
159+
160+
// pure function types are readonly
161+
if (
162+
type.getCallSignatures().length > 0 &&
163+
type.getProperties().length === 0
164+
) {
165+
return Readonlyness.Readonly;
166+
}
167+
168+
const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type, seenTypes);
169+
if (isReadonlyArray !== Readonlyness.UnknownType) {
170+
return isReadonlyArray;
171+
}
172+
173+
const isReadonlyObject = isTypeReadonlyObject(checker, type, seenTypes);
174+
/* istanbul ignore else */ if (
175+
isReadonlyObject !== Readonlyness.UnknownType
176+
) {
177+
return isReadonlyObject;
178+
}
179+
180+
throw new Error("Unhandled type");
181+
}
182+
183+
/**
184+
* Checks if the given type is readonly
185+
*/
186+
function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean {
187+
if (ts === undefined) {
188+
throw new Error("TypeScript not found.");
189+
}
190+
if (tsutils === undefined) {
191+
throw new Error("tsutils not found.");
192+
}
193+
return (
194+
isTypeReadonlyRecurser(checker, type, new Set()) === Readonlyness.Readonly
195+
);
196+
}
197+
198+
export { isTypeReadonly };
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* A set of common reasons for calling nullThrows
3+
*/
4+
const NullThrowsReasons = {
5+
MissingParent: 'Expected node to have a parent.',
6+
MissingToken: (token: string, thing: string) =>
7+
`Expected to find a ${token} for the ${thing}.`,
8+
} as const;
9+
10+
/**
11+
* Assert that a value must not be null or undefined.
12+
* This is a nice explicit alternative to the non-null assertion operator.
13+
*/
14+
function nullThrows<T>(value: T | null | undefined, message: string): T {
15+
// this function is primarily used to keep types happy in a safe way
16+
// i.e. is used when we expect that a value is never nullish
17+
// this means that it's pretty much impossible to test the below if...
18+
19+
// so ignore it in coverage metrics.
20+
/* istanbul ignore if */
21+
if (value === null || value === undefined) {
22+
throw new Error(`Non-null Assertion Failed: ${message}`);
23+
}
24+
25+
return value;
26+
}
27+
28+
export { nullThrows, NullThrowsReasons };
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// @ts-nocheck
2+
3+
import * as ts from "typescript";
4+
5+
export function getTypeOfPropertyOfName(
6+
checker: ts.TypeChecker,
7+
type: ts.Type,
8+
name: string,
9+
escapedName?: ts.__String
10+
): ts.Type | undefined {
11+
// Most names are directly usable in the checker and aren't different from escaped names
12+
if (!escapedName || !name.startsWith("__")) {
13+
return checker.getTypeOfPropertyOfType(type, name);
14+
}
15+
16+
// Symbolic names may differ in their escaped name compared to their human-readable name
17+
// https://github.com/typescript-eslint/typescript-eslint/issues/2143
18+
const escapedProperty = type
19+
.getProperties()
20+
.find((property) => property.escapedName === escapedName);
21+
22+
return escapedProperty
23+
? checker.getDeclaredTypeOfSymbol(escapedProperty)
24+
: undefined;
25+
}
26+
27+
export function getTypeOfPropertyOfType(
28+
checker: ts.TypeChecker,
29+
type: ts.Type,
30+
property: ts.Symbol
31+
): ts.Type | undefined {
32+
return getTypeOfPropertyOfName(
33+
checker,
34+
type,
35+
property.getName(),
36+
property.getEscapedName()
37+
);
38+
}

0 commit comments

Comments
 (0)