Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e77d977

Browse files
committed
wip
1 parent 7dad3bf commit e77d977

File tree

6 files changed

+268
-133
lines changed

6 files changed

+268
-133
lines changed

packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts

Lines changed: 1 addition & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import {
22
TSESTree,
33
AST_NODE_TYPES,
44
} from '@typescript-eslint/experimental-utils';
5-
import { isObjectType, isUnionType, unionTypeParts } from 'tsutils';
6-
import * as ts from 'typescript';
75
import * as util from '../util';
86

97
export default util.createRule({
@@ -31,134 +29,6 @@ export default util.createRule({
3129
const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context);
3230
const checker = program.getTypeChecker();
3331

34-
/**
35-
* Returns:
36-
* - null if the type is not an array or tuple,
37-
* - true if the type is a readonly array or readonly tuple,
38-
* - false if the type is a mutable array or mutable tuple.
39-
*/
40-
function isTypeReadonlyArrayOrTuple(type: ts.Type): boolean | null {
41-
function checkTypeArguments(arrayType: ts.TypeReference): boolean {
42-
const typeArguments = checker.getTypeArguments(arrayType);
43-
if (typeArguments.length === 0) {
44-
// this shouldn't happen in reality as:
45-
// - tuples require at least 1 type argument
46-
// - ReadonlyArray requires at least 1 type argument
47-
return true;
48-
}
49-
50-
// validate the element types are also readonly
51-
if (typeArguments.some(typeArg => !isTypeReadonly(typeArg))) {
52-
return false;
53-
}
54-
return true;
55-
}
56-
57-
if (checker.isArrayType(type)) {
58-
const symbol = util.nullThrows(
59-
type.getSymbol(),
60-
util.NullThrowsReasons.MissingToken('symbol', 'array type'),
61-
);
62-
const escapedName = symbol.getEscapedName();
63-
if (escapedName === 'Array' && escapedName !== 'ReadonlyArray') {
64-
return false;
65-
}
66-
67-
return checkTypeArguments(type);
68-
}
69-
70-
if (checker.isTupleType(type)) {
71-
if (!type.target.readonly) {
72-
return false;
73-
}
74-
75-
return checkTypeArguments(type);
76-
}
77-
78-
return null;
79-
}
80-
81-
/**
82-
* Returns:
83-
* - null if the type is not an object,
84-
* - true if the type is an object with only readonly props,
85-
* - false if the type is an object with at least one mutable prop.
86-
*/
87-
function isTypeReadonlyObject(type: ts.Type): boolean | null {
88-
function checkIndex(kind: ts.IndexKind): boolean | null {
89-
const indexInfo = checker.getIndexInfoOfType(type, kind);
90-
if (indexInfo) {
91-
return indexInfo.isReadonly ? true : false;
92-
}
93-
94-
return null;
95-
}
96-
97-
const isStringIndexReadonly = checkIndex(ts.IndexKind.String);
98-
if (isStringIndexReadonly !== null) {
99-
return isStringIndexReadonly;
100-
}
101-
102-
const isNumberIndexReadonly = checkIndex(ts.IndexKind.Number);
103-
if (isNumberIndexReadonly !== null) {
104-
return isNumberIndexReadonly;
105-
}
106-
107-
const properties = type.getProperties();
108-
if (properties.length) {
109-
// NOTES:
110-
// - port isReadonlySymbol - https://github.com/Microsoft/TypeScript/blob/4212484ae18163df867f53dab19a8cc0c6000793/src/compiler/checker.ts#L26285
111-
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
112-
// @ts-ignore
113-
declare function isReadonlySymbol(symbol: ts.Symbol): boolean;
114-
115-
for (const property of properties) {
116-
if (!isReadonlySymbol(property)) {
117-
return false;
118-
}
119-
}
120-
121-
// all properties were readonly
122-
return true;
123-
}
124-
125-
return false;
126-
}
127-
128-
/**
129-
* Checks if the given type is readonly
130-
*/
131-
function isTypeReadonly(type: ts.Type): boolean {
132-
if (isUnionType(type)) {
133-
return unionTypeParts(type).every(t => isTypeReadonly(t));
134-
}
135-
136-
// all non-object types are readonly
137-
if (!isObjectType(type)) {
138-
return true;
139-
}
140-
141-
// pure function types are readonly
142-
if (
143-
type.getCallSignatures().length > 0 &&
144-
type.getProperties().length === 0
145-
) {
146-
return true;
147-
}
148-
149-
const isReadonlyArray = isTypeReadonlyArrayOrTuple(type);
150-
if (isReadonlyArray !== null) {
151-
return isReadonlyArray;
152-
}
153-
154-
const isReadonlyObject = isTypeReadonlyObject(type);
155-
if (isReadonlyObject !== null) {
156-
return isReadonlyObject;
157-
}
158-
159-
throw new Error('Unhandled type');
160-
}
161-
16232
return {
16333
'ArrowFunctionExpression, FunctionDeclaration, FunctionExpression, TSEmptyBodyFunctionExpression'(
16434
node:
@@ -174,7 +44,7 @@ export default util.createRule({
17444
: param;
17545
const tsNode = esTreeNodeToTSNodeMap.get(actualParam);
17646
const type = checker.getTypeAtLocation(tsNode);
177-
const isReadOnly = isTypeReadonly(type);
47+
const isReadOnly = util.isTypeReadonly(checker, type);
17848

17949
if (!isReadOnly) {
18050
return context.report({

packages/eslint-plugin/src/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils';
22

33
export * from './astUtils';
44
export * from './createRule';
5+
export * from './isTypeReadonly';
56
export * from './misc';
67
export * from './nullThrows';
78
export * from './types';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
isObjectType,
3+
isUnionType,
4+
isUnionOrIntersectionType,
5+
unionTypeParts,
6+
} from 'tsutils';
7+
import * as ts from 'typescript';
8+
import { nullThrows, NullThrowsReasons } from '..';
9+
import { isReadonlySymbol } from './isReadonlySymbol';
10+
11+
/**
12+
* Returns:
13+
* - null if the type is not an array or tuple,
14+
* - true if the type is a readonly array or readonly tuple,
15+
* - false if the type is a mutable array or mutable tuple.
16+
*/
17+
function isTypeReadonlyArrayOrTuple(
18+
checker: ts.TypeChecker,
19+
type: ts.Type,
20+
): boolean | null {
21+
function checkTypeArguments(arrayType: ts.TypeReference): boolean {
22+
const typeArguments = checker.getTypeArguments(arrayType);
23+
if (typeArguments.length === 0) {
24+
// this shouldn't happen in reality as:
25+
// - tuples require at least 1 type argument
26+
// - ReadonlyArray requires at least 1 type argument
27+
return true;
28+
}
29+
30+
// validate the element types are also readonly
31+
if (typeArguments.some(typeArg => !isTypeReadonly(checker, typeArg))) {
32+
return false;
33+
}
34+
return true;
35+
}
36+
37+
if (checker.isArrayType(type)) {
38+
const symbol = nullThrows(
39+
type.getSymbol(),
40+
NullThrowsReasons.MissingToken('symbol', 'array type'),
41+
);
42+
const escapedName = symbol.getEscapedName();
43+
if (escapedName === 'Array' && escapedName !== 'ReadonlyArray') {
44+
return false;
45+
}
46+
47+
return checkTypeArguments(type);
48+
}
49+
50+
if (checker.isTupleType(type)) {
51+
if (!type.target.readonly) {
52+
return false;
53+
}
54+
55+
return checkTypeArguments(type);
56+
}
57+
58+
return null;
59+
}
60+
61+
/**
62+
* Returns:
63+
* - null if the type is not an object,
64+
* - true if the type is an object with only readonly props,
65+
* - false if the type is an object with at least one mutable prop.
66+
*/
67+
function isTypeReadonlyObject(
68+
checker: ts.TypeChecker,
69+
type: ts.Type,
70+
): boolean | null {
71+
function checkIndexSignature(kind: ts.IndexKind): boolean | null {
72+
const indexInfo = checker.getIndexInfoOfType(type, kind);
73+
if (indexInfo) {
74+
return indexInfo.isReadonly ? true : false;
75+
}
76+
77+
return null;
78+
}
79+
80+
const properties = type.getProperties();
81+
if (properties.length) {
82+
for (const property of properties) {
83+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
84+
// @ts-ignore
85+
const x = checker.isReadonlySymbol(property);
86+
const y = isReadonlySymbol(property);
87+
if (x !== y) {
88+
throw new Error('FUCK');
89+
}
90+
if (!isReadonlySymbol(property)) {
91+
return false;
92+
}
93+
}
94+
95+
// all properties were readonly
96+
}
97+
98+
const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String);
99+
if (isStringIndexSigReadonly === false) {
100+
return isStringIndexSigReadonly;
101+
}
102+
103+
const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number);
104+
if (isNumberIndexSigReadonly === false) {
105+
return isNumberIndexSigReadonly;
106+
}
107+
108+
return true;
109+
}
110+
111+
/**
112+
* Checks if the given type is readonly
113+
*/
114+
function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean {
115+
if (isUnionType(type)) {
116+
// all types in the union must be readonly
117+
return unionTypeParts(type).every(t => isTypeReadonly(checker, t));
118+
}
119+
120+
// all non-object, non-intersection types are readonly.
121+
// this should only be primitive types
122+
if (!isObjectType(type) && !isUnionOrIntersectionType(type)) {
123+
return true;
124+
}
125+
126+
// pure function types are readonly
127+
if (
128+
type.getCallSignatures().length > 0 &&
129+
type.getProperties().length === 0
130+
) {
131+
return true;
132+
}
133+
134+
const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type);
135+
if (isReadonlyArray !== null) {
136+
return isReadonlyArray;
137+
}
138+
139+
const isReadonlyObject = isTypeReadonlyObject(checker, type);
140+
if (isReadonlyObject !== null) {
141+
return isReadonlyObject;
142+
}
143+
144+
throw new Error('Unhandled type');
145+
}
146+
147+
export { isTypeReadonly };
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// - this code is ported from typescript's type checker
2+
// Starting at https://github.com/Microsoft/TypeScript/blob/4212484ae18163df867f53dab19a8cc0c6000793/src/compiler/checker.ts#L26285
3+
4+
import * as ts from 'typescript';
5+
6+
// #region internal types used for isReadonlySymbol
7+
8+
// we can't use module augmentation because typescript uses export = ts
9+
/* eslint-disable @typescript-eslint/ban-ts-ignore */
10+
11+
// CheckFlags is actually const enum
12+
// https://github.com/Microsoft/TypeScript/blob/236012e47b26fee210caa9cbd2e072ef9e99f9ae/src/compiler/types.ts#L4038
13+
const enum CheckFlags {
14+
Readonly = 1 << 3,
15+
}
16+
type GetCheckFlags = (symbol: ts.Symbol) => CheckFlags;
17+
// @ts-ignore
18+
const getCheckFlags: GetCheckFlags = ts.getCheckFlags;
19+
20+
type GetDeclarationModifierFlagsFromSymbol = (s: ts.Symbol) => ts.ModifierFlags;
21+
const getDeclarationModifierFlagsFromSymbol: GetDeclarationModifierFlagsFromSymbol =
22+
// @ts-ignore
23+
ts.getDeclarationModifierFlagsFromSymbol;
24+
25+
/* eslint-enable @typescript-eslint/ban-ts-ignore */
26+
27+
// function getDeclarationNodeFlagsFromSymbol(s: ts.Symbol): ts.NodeFlags {
28+
// return s.valueDeclaration ? ts.getCombinedNodeFlags(s.valueDeclaration) : 0;
29+
// }
30+
31+
// #endregion
32+
33+
function isReadonlySymbol(symbol: ts.Symbol): boolean {
34+
// The following symbols are considered read-only:
35+
// Properties with a 'readonly' modifier
36+
// Variables declared with 'const'
37+
// Get accessors without matching set accessors
38+
// Enum members
39+
// Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation)
40+
41+
// transient readonly property
42+
if (getCheckFlags(symbol) & CheckFlags.Readonly) {
43+
console.log('check flags is truthy');
44+
return true;
45+
}
46+
47+
// Properties with a 'readonly' modifier
48+
if (
49+
symbol.flags & ts.SymbolFlags.Property &&
50+
getDeclarationModifierFlagsFromSymbol(symbol) & ts.ModifierFlags.Readonly
51+
) {
52+
return true;
53+
}
54+
55+
// Variables declared with 'const'
56+
// if (
57+
// symbol.flags & ts.SymbolFlags.Variable &&
58+
// getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const
59+
// ) {
60+
// return true;
61+
// }
62+
63+
// Get accessors without matching set accessors
64+
if (
65+
symbol.flags & ts.SymbolFlags.Accessor &&
66+
!(symbol.flags & ts.SymbolFlags.SetAccessor)
67+
) {
68+
return true;
69+
}
70+
71+
// Enum members
72+
if (symbol.flags & ts.SymbolFlags.EnumMember) {
73+
return true;
74+
}
75+
76+
return false;
77+
78+
// TODO - maybe add this check?
79+
// || symbol.declarations.some(isReadonlyAssignmentDeclaration)
80+
}
81+
82+
export { isReadonlySymbol };

0 commit comments

Comments
 (0)