-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(prefer-readonly-type-declaration): add upstream utils to this p…
…roject Once typescript-eslint/typescript-eslint#3658 is merged, this commit can be reverted
- Loading branch information
1 parent
57ff8e0
commit fd0d84e
Showing
6 changed files
with
271 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
// @ts-nocheck | ||
|
||
import tsutils from "~/conditional-imports/tsutils"; | ||
import ts from "~/conditional-imports/typescript"; | ||
|
||
import { nullThrows, NullThrowsReasons } from "./nullThrows"; | ||
import { getTypeOfPropertyOfType } from "./propertyTypes"; | ||
|
||
const enum Readonlyness { | ||
/** the type cannot be handled by the function */ | ||
UnknownType = 1, | ||
/** the type is mutable */ | ||
Mutable = 2, | ||
/** the type is readonly */ | ||
Readonly = 3, | ||
} | ||
|
||
function isTypeReadonlyArrayOrTuple( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
seenTypes: Set<ts.Type> | ||
): Readonlyness { | ||
function checkTypeArguments(arrayType: ts.TypeReference): Readonlyness { | ||
const typeArguments = checker.getTypeArguments | ||
? checker.getTypeArguments(arrayType) | ||
: arrayType.typeArguments ?? []; | ||
|
||
// this shouldn't happen in reality as: | ||
// - tuples require at least 1 type argument | ||
// - ReadonlyArray requires at least 1 type argument | ||
/* istanbul ignore if */ if (typeArguments.length === 0) { | ||
return Readonlyness.Readonly; | ||
} | ||
|
||
// validate the element types are also readonly | ||
if ( | ||
typeArguments.some( | ||
(typeArg) => | ||
isTypeReadonlyRecurser(checker, typeArg, seenTypes) === | ||
Readonlyness.Mutable | ||
) | ||
) { | ||
return Readonlyness.Mutable; | ||
} | ||
return Readonlyness.Readonly; | ||
} | ||
|
||
if (checker.isArrayType(type)) { | ||
const symbol = nullThrows( | ||
type.getSymbol(), | ||
NullThrowsReasons.MissingToken("symbol", "array type") | ||
); | ||
const escapedName = symbol.getEscapedName(); | ||
if (escapedName === "Array") { | ||
return Readonlyness.Mutable; | ||
} | ||
|
||
return checkTypeArguments(type); | ||
} | ||
|
||
if (checker.isTupleType(type)) { | ||
if (!type.target.readonly) { | ||
return Readonlyness.Mutable; | ||
} | ||
|
||
return checkTypeArguments(type); | ||
} | ||
|
||
return Readonlyness.UnknownType; | ||
} | ||
|
||
function isTypeReadonlyObject( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
seenTypes: Set<ts.Type> | ||
): Readonlyness { | ||
function checkIndexSignature(kind: ts.IndexKind): Readonlyness { | ||
const indexInfo = checker.getIndexInfoOfType(type, kind); | ||
if (indexInfo) { | ||
return indexInfo.isReadonly | ||
? Readonlyness.Readonly | ||
: Readonlyness.Mutable; | ||
} | ||
|
||
return Readonlyness.UnknownType; | ||
} | ||
|
||
const properties = type.getProperties(); | ||
if (properties.length) { | ||
// ensure the properties are marked as readonly | ||
for (const property of properties) { | ||
if (!tsutils.isPropertyReadonlyInType(type, property.getEscapedName(), checker)) { | ||
return Readonlyness.Mutable; | ||
} | ||
} | ||
|
||
// all properties were readonly | ||
// now ensure that all of the values are readonly also. | ||
|
||
// do this after checking property readonly-ness as a perf optimization, | ||
// as we might be able to bail out early due to a mutable property before | ||
// doing this deep, potentially expensive check. | ||
for (const property of properties) { | ||
const propertyType = nullThrows( | ||
getTypeOfPropertyOfType(checker, type, property), | ||
NullThrowsReasons.MissingToken(`property "${property.name}"`, "type") | ||
); | ||
|
||
// handle recursive types. | ||
// we only need this simple check, because a mutable recursive type will break via the above prop readonly check | ||
if (seenTypes.has(propertyType)) { | ||
continue; | ||
} | ||
|
||
if ( | ||
isTypeReadonlyRecurser(checker, propertyType, seenTypes) === | ||
Readonlyness.Mutable | ||
) { | ||
return Readonlyness.Mutable; | ||
} | ||
} | ||
} | ||
|
||
const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String); | ||
if (isStringIndexSigReadonly === Readonlyness.Mutable) { | ||
return isStringIndexSigReadonly; | ||
} | ||
|
||
const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number); | ||
if (isNumberIndexSigReadonly === Readonlyness.Mutable) { | ||
return isNumberIndexSigReadonly; | ||
} | ||
|
||
return Readonlyness.Readonly; | ||
} | ||
|
||
// a helper function to ensure the seenTypes map is always passed down, except by the external caller | ||
function isTypeReadonlyRecurser( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
seenTypes: Set<ts.Type> | ||
): Readonlyness.Readonly | Readonlyness.Mutable { | ||
seenTypes.add(type); | ||
|
||
if (tsutils.isUnionType(type)) { | ||
// all types in the union must be readonly | ||
const result = tsutils.unionTypeParts(type).every((t) => | ||
isTypeReadonlyRecurser(checker, t, seenTypes) | ||
); | ||
const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable; | ||
return readonlyness; | ||
} | ||
|
||
// all non-object, non-intersection types are readonly. | ||
// this should only be primitive types | ||
if (!tsutils.isObjectType(type) && !tsutils.isUnionOrIntersectionType(type)) { | ||
return Readonlyness.Readonly; | ||
} | ||
|
||
// pure function types are readonly | ||
if ( | ||
type.getCallSignatures().length > 0 && | ||
type.getProperties().length === 0 | ||
) { | ||
return Readonlyness.Readonly; | ||
} | ||
|
||
const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type, seenTypes); | ||
if (isReadonlyArray !== Readonlyness.UnknownType) { | ||
return isReadonlyArray; | ||
} | ||
|
||
const isReadonlyObject = isTypeReadonlyObject(checker, type, seenTypes); | ||
/* istanbul ignore else */ if ( | ||
isReadonlyObject !== Readonlyness.UnknownType | ||
) { | ||
return isReadonlyObject; | ||
} | ||
|
||
throw new Error("Unhandled type"); | ||
} | ||
|
||
/** | ||
* Checks if the given type is readonly | ||
*/ | ||
function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean { | ||
if (ts === undefined) { | ||
throw new Error("TypeScript not found."); | ||
} | ||
if (tsutils === undefined) { | ||
throw new Error("tsutils not found."); | ||
} | ||
return ( | ||
isTypeReadonlyRecurser(checker, type, new Set()) === Readonlyness.Readonly | ||
); | ||
} | ||
|
||
export { isTypeReadonly }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/** | ||
* A set of common reasons for calling nullThrows | ||
*/ | ||
const NullThrowsReasons = { | ||
MissingParent: 'Expected node to have a parent.', | ||
MissingToken: (token: string, thing: string) => | ||
`Expected to find a ${token} for the ${thing}.`, | ||
} as const; | ||
|
||
/** | ||
* Assert that a value must not be null or undefined. | ||
* This is a nice explicit alternative to the non-null assertion operator. | ||
*/ | ||
function nullThrows<T>(value: T | null | undefined, message: string): T { | ||
// this function is primarily used to keep types happy in a safe way | ||
// i.e. is used when we expect that a value is never nullish | ||
// this means that it's pretty much impossible to test the below if... | ||
|
||
// so ignore it in coverage metrics. | ||
/* istanbul ignore if */ | ||
if (value === null || value === undefined) { | ||
throw new Error(`Non-null Assertion Failed: ${message}`); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
export { nullThrows, NullThrowsReasons }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// @ts-nocheck | ||
|
||
import * as ts from "typescript"; | ||
|
||
export function getTypeOfPropertyOfName( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
name: string, | ||
escapedName?: ts.__String | ||
): ts.Type | undefined { | ||
// Most names are directly usable in the checker and aren't different from escaped names | ||
if (!escapedName || !name.startsWith("__")) { | ||
return checker.getTypeOfPropertyOfType(type, name); | ||
} | ||
|
||
// Symbolic names may differ in their escaped name compared to their human-readable name | ||
// https://github.com/typescript-eslint/typescript-eslint/issues/2143 | ||
const escapedProperty = type | ||
.getProperties() | ||
.find((property) => property.escapedName === escapedName); | ||
|
||
return escapedProperty | ||
? checker.getDeclaredTypeOfSymbol(escapedProperty) | ||
: undefined; | ||
} | ||
|
||
export function getTypeOfPropertyOfType( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
property: ts.Symbol | ||
): ts.Type | undefined { | ||
return getTypeOfPropertyOfName( | ||
checker, | ||
type, | ||
property.getName(), | ||
property.getEscapedName() | ||
); | ||
} |