Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c5f4881
commit cde59a5
Showing
6 changed files
with
268 additions
and
133 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
147 changes: 147 additions & 0 deletions
147
packages/eslint-plugin/src/util/isTypeReadonly/index.ts
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,147 @@ | ||
import { | ||
isObjectType, | ||
isUnionType, | ||
isUnionOrIntersectionType, | ||
unionTypeParts, | ||
} from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
import { nullThrows, NullThrowsReasons } from '..'; | ||
import { isReadonlySymbol } from './isReadonlySymbol'; | ||
|
||
/** | ||
* Returns: | ||
* - null if the type is not an array or tuple, | ||
* - true if the type is a readonly array or readonly tuple, | ||
* - false if the type is a mutable array or mutable tuple. | ||
*/ | ||
function isTypeReadonlyArrayOrTuple( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
): boolean | null { | ||
function checkTypeArguments(arrayType: ts.TypeReference): boolean { | ||
const typeArguments = checker.getTypeArguments(arrayType); | ||
if (typeArguments.length === 0) { | ||
// this shouldn't happen in reality as: | ||
// - tuples require at least 1 type argument | ||
// - ReadonlyArray requires at least 1 type argument | ||
return true; | ||
} | ||
|
||
// validate the element types are also readonly | ||
if (typeArguments.some(typeArg => !isTypeReadonly(checker, typeArg))) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
if (checker.isArrayType(type)) { | ||
const symbol = nullThrows( | ||
type.getSymbol(), | ||
NullThrowsReasons.MissingToken('symbol', 'array type'), | ||
); | ||
const escapedName = symbol.getEscapedName(); | ||
if (escapedName === 'Array' && escapedName !== 'ReadonlyArray') { | ||
return false; | ||
} | ||
|
||
return checkTypeArguments(type); | ||
} | ||
|
||
if (checker.isTupleType(type)) { | ||
if (!type.target.readonly) { | ||
return false; | ||
} | ||
|
||
return checkTypeArguments(type); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Returns: | ||
* - null if the type is not an object, | ||
* - true if the type is an object with only readonly props, | ||
* - false if the type is an object with at least one mutable prop. | ||
*/ | ||
function isTypeReadonlyObject( | ||
checker: ts.TypeChecker, | ||
type: ts.Type, | ||
): boolean | null { | ||
function checkIndexSignature(kind: ts.IndexKind): boolean | null { | ||
const indexInfo = checker.getIndexInfoOfType(type, kind); | ||
if (indexInfo) { | ||
return indexInfo.isReadonly ? true : false; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
const properties = type.getProperties(); | ||
if (properties.length) { | ||
for (const property of properties) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | ||
// @ts-ignore | ||
const x = checker.isReadonlySymbol(property); | ||
const y = isReadonlySymbol(property); | ||
if (x !== y) { | ||
throw new Error('FUCK'); | ||
} | ||
if (!isReadonlySymbol(property)) { | ||
return false; | ||
} | ||
} | ||
|
||
// all properties were readonly | ||
} | ||
|
||
const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String); | ||
if (isStringIndexSigReadonly === false) { | ||
return isStringIndexSigReadonly; | ||
} | ||
|
||
const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number); | ||
if (isNumberIndexSigReadonly === false) { | ||
return isNumberIndexSigReadonly; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Checks if the given type is readonly | ||
*/ | ||
function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean { | ||
if (isUnionType(type)) { | ||
// all types in the union must be readonly | ||
return unionTypeParts(type).every(t => isTypeReadonly(checker, t)); | ||
} | ||
|
||
// all non-object, non-intersection types are readonly. | ||
// this should only be primitive types | ||
if (!isObjectType(type) && !isUnionOrIntersectionType(type)) { | ||
return true; | ||
} | ||
|
||
// pure function types are readonly | ||
if ( | ||
type.getCallSignatures().length > 0 && | ||
type.getProperties().length === 0 | ||
) { | ||
return true; | ||
} | ||
|
||
const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type); | ||
if (isReadonlyArray !== null) { | ||
return isReadonlyArray; | ||
} | ||
|
||
const isReadonlyObject = isTypeReadonlyObject(checker, type); | ||
if (isReadonlyObject !== null) { | ||
return isReadonlyObject; | ||
} | ||
|
||
throw new Error('Unhandled type'); | ||
} | ||
|
||
export { isTypeReadonly }; |
82 changes: 82 additions & 0 deletions
82
packages/eslint-plugin/src/util/isTypeReadonly/isReadonlySymbol.ts
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,82 @@ | ||
// - this code is ported from typescript's type checker | ||
// Starting at https://github.com/Microsoft/TypeScript/blob/4212484ae18163df867f53dab19a8cc0c6000793/src/compiler/checker.ts#L26285 | ||
|
||
import * as ts from 'typescript'; | ||
|
||
// #region internal types used for isReadonlySymbol | ||
|
||
// we can't use module augmentation because typescript uses export = ts | ||
/* eslint-disable @typescript-eslint/ban-ts-ignore */ | ||
|
||
// CheckFlags is actually const enum | ||
// https://github.com/Microsoft/TypeScript/blob/236012e47b26fee210caa9cbd2e072ef9e99f9ae/src/compiler/types.ts#L4038 | ||
const enum CheckFlags { | ||
Readonly = 1 << 3, | ||
} | ||
type GetCheckFlags = (symbol: ts.Symbol) => CheckFlags; | ||
// @ts-ignore | ||
const getCheckFlags: GetCheckFlags = ts.getCheckFlags; | ||
|
||
type GetDeclarationModifierFlagsFromSymbol = (s: ts.Symbol) => ts.ModifierFlags; | ||
const getDeclarationModifierFlagsFromSymbol: GetDeclarationModifierFlagsFromSymbol = | ||
// @ts-ignore | ||
ts.getDeclarationModifierFlagsFromSymbol; | ||
|
||
/* eslint-enable @typescript-eslint/ban-ts-ignore */ | ||
|
||
// function getDeclarationNodeFlagsFromSymbol(s: ts.Symbol): ts.NodeFlags { | ||
// return s.valueDeclaration ? ts.getCombinedNodeFlags(s.valueDeclaration) : 0; | ||
// } | ||
|
||
// #endregion | ||
|
||
function isReadonlySymbol(symbol: ts.Symbol): boolean { | ||
// The following symbols are considered read-only: | ||
// Properties with a 'readonly' modifier | ||
// Variables declared with 'const' | ||
// Get accessors without matching set accessors | ||
// Enum members | ||
// Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation) | ||
|
||
// transient readonly property | ||
if (getCheckFlags(symbol) & CheckFlags.Readonly) { | ||
console.log('check flags is truthy'); | ||
return true; | ||
} | ||
|
||
// Properties with a 'readonly' modifier | ||
if ( | ||
symbol.flags & ts.SymbolFlags.Property && | ||
getDeclarationModifierFlagsFromSymbol(symbol) & ts.ModifierFlags.Readonly | ||
) { | ||
return true; | ||
} | ||
|
||
// Variables declared with 'const' | ||
// if ( | ||
// symbol.flags & ts.SymbolFlags.Variable && | ||
// getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const | ||
// ) { | ||
// return true; | ||
// } | ||
|
||
// Get accessors without matching set accessors | ||
if ( | ||
symbol.flags & ts.SymbolFlags.Accessor && | ||
!(symbol.flags & ts.SymbolFlags.SetAccessor) | ||
) { | ||
return true; | ||
} | ||
|
||
// Enum members | ||
if (symbol.flags & ts.SymbolFlags.EnumMember) { | ||
return true; | ||
} | ||
|
||
return false; | ||
|
||
// TODO - maybe add this check? | ||
// || symbol.declarations.some(isReadonlyAssignmentDeclaration) | ||
} | ||
|
||
export { isReadonlySymbol }; |
Oops, something went wrong.