Skip to content

Commit 7abd090

Browse files
feat(eslint-plugin): [prefer-readonly-parameter-types] add option treatMethodsAsReadonly
fix #1758
1 parent db78642 commit 7abd090

File tree

3 files changed

+96
-10
lines changed

3 files changed

+96
-10
lines changed

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Options = [
88
{
99
checkParameterProperties?: boolean;
1010
ignoreInferredTypes?: boolean;
11+
treatMethodsAsReadonly?: boolean;
1112
},
1213
];
1314
type MessageIds = 'shouldBeReadonly';
@@ -34,6 +35,9 @@ export default util.createRule<Options, MessageIds>({
3435
ignoreInferredTypes: {
3536
type: 'boolean',
3637
},
38+
treatMethodsAsReadonly: {
39+
type: 'boolean',
40+
},
3741
},
3842
},
3943
],
@@ -45,10 +49,13 @@ export default util.createRule<Options, MessageIds>({
4549
{
4650
checkParameterProperties: true,
4751
ignoreInferredTypes: false,
52+
treatMethodsAsReadonly: false,
4853
},
4954
],
5055
create(context, options) {
51-
const [{ checkParameterProperties, ignoreInferredTypes }] = options;
56+
const [
57+
{ checkParameterProperties, ignoreInferredTypes, treatMethodsAsReadonly },
58+
] = options;
5259
const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context);
5360
const checker = program.getTypeChecker();
5461

@@ -94,7 +101,9 @@ export default util.createRule<Options, MessageIds>({
94101

95102
const tsNode = esTreeNodeToTSNodeMap.get(actualParam);
96103
const type = checker.getTypeAtLocation(tsNode);
97-
const isReadOnly = util.isTypeReadonly(checker, type);
104+
const isReadOnly = util.isTypeReadonly(checker, type, {
105+
treatMethodsAsReadonly: treatMethodsAsReadonly!,
106+
});
98107

99108
if (!isReadOnly) {
100109
context.report({

Diff for: packages/eslint-plugin/src/util/isTypeReadonly.ts

+46-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isUnionOrIntersectionType,
55
unionTypeParts,
66
isPropertyReadonlyInType,
7+
isSymbolFlagSet,
78
} from 'tsutils';
89
import * as ts from 'typescript';
910
import { getTypeOfPropertyOfType, nullThrows, NullThrowsReasons } from '.';
@@ -17,9 +18,18 @@ const enum Readonlyness {
1718
Readonly = 3,
1819
}
1920

21+
export interface ReadonlynessOptions {
22+
readonly treatMethodsAsReadonly: boolean;
23+
}
24+
25+
function hasSymbol(node: ts.Node): node is ts.Node & { symbol: ts.Symbol } {
26+
return Object.prototype.hasOwnProperty.call(node, 'symbol');
27+
}
28+
2029
function isTypeReadonlyArrayOrTuple(
2130
checker: ts.TypeChecker,
2231
type: ts.Type,
32+
options: ReadonlynessOptions,
2333
seenTypes: Set<ts.Type>,
2434
): Readonlyness {
2535
function checkTypeArguments(arrayType: ts.TypeReference): Readonlyness {
@@ -35,7 +45,7 @@ function isTypeReadonlyArrayOrTuple(
3545
if (
3646
typeArguments.some(
3747
typeArg =>
38-
isTypeReadonlyRecurser(checker, typeArg, seenTypes) ===
48+
isTypeReadonlyRecurser(checker, typeArg, options, seenTypes) ===
3949
Readonlyness.Mutable,
4050
)
4151
) {
@@ -71,6 +81,7 @@ function isTypeReadonlyArrayOrTuple(
7181
function isTypeReadonlyObject(
7282
checker: ts.TypeChecker,
7383
type: ts.Type,
84+
options: ReadonlynessOptions,
7485
seenTypes: Set<ts.Type>,
7586
): Readonlyness {
7687
function checkIndexSignature(kind: ts.IndexKind): Readonlyness {
@@ -88,7 +99,18 @@ function isTypeReadonlyObject(
8899
if (properties.length) {
89100
// ensure the properties are marked as readonly
90101
for (const property of properties) {
91-
if (!isPropertyReadonlyInType(type, property.getEscapedName(), checker)) {
102+
if (
103+
!(
104+
isPropertyReadonlyInType(type, property.getEscapedName(), checker) ||
105+
(options.treatMethodsAsReadonly &&
106+
property.valueDeclaration !== undefined &&
107+
hasSymbol(property.valueDeclaration) &&
108+
isSymbolFlagSet(
109+
property.valueDeclaration.symbol,
110+
ts.SymbolFlags.Method,
111+
))
112+
)
113+
) {
92114
return Readonlyness.Mutable;
93115
}
94116
}
@@ -112,7 +134,7 @@ function isTypeReadonlyObject(
112134
}
113135

114136
if (
115-
isTypeReadonlyRecurser(checker, propertyType, seenTypes) ===
137+
isTypeReadonlyRecurser(checker, propertyType, options, seenTypes) ===
116138
Readonlyness.Mutable
117139
) {
118140
return Readonlyness.Mutable;
@@ -137,14 +159,15 @@ function isTypeReadonlyObject(
137159
function isTypeReadonlyRecurser(
138160
checker: ts.TypeChecker,
139161
type: ts.Type,
162+
options: ReadonlynessOptions,
140163
seenTypes: Set<ts.Type>,
141164
): Readonlyness.Readonly | Readonlyness.Mutable {
142165
seenTypes.add(type);
143166

144167
if (isUnionType(type)) {
145168
// all types in the union must be readonly
146169
const result = unionTypeParts(type).every(t =>
147-
isTypeReadonlyRecurser(checker, t, seenTypes),
170+
isTypeReadonlyRecurser(checker, t, options, seenTypes),
148171
);
149172
const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable;
150173
return readonlyness;
@@ -164,12 +187,22 @@ function isTypeReadonlyRecurser(
164187
return Readonlyness.Readonly;
165188
}
166189

167-
const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type, seenTypes);
190+
const isReadonlyArray = isTypeReadonlyArrayOrTuple(
191+
checker,
192+
type,
193+
options,
194+
seenTypes,
195+
);
168196
if (isReadonlyArray !== Readonlyness.UnknownType) {
169197
return isReadonlyArray;
170198
}
171199

172-
const isReadonlyObject = isTypeReadonlyObject(checker, type, seenTypes);
200+
const isReadonlyObject = isTypeReadonlyObject(
201+
checker,
202+
type,
203+
options,
204+
seenTypes,
205+
);
173206
/* istanbul ignore else */ if (
174207
isReadonlyObject !== Readonlyness.UnknownType
175208
) {
@@ -182,9 +215,14 @@ function isTypeReadonlyRecurser(
182215
/**
183216
* Checks if the given type is readonly
184217
*/
185-
function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean {
218+
function isTypeReadonly(
219+
checker: ts.TypeChecker,
220+
type: ts.Type,
221+
options: ReadonlynessOptions,
222+
): boolean {
186223
return (
187-
isTypeReadonlyRecurser(checker, type, new Set()) === Readonlyness.Readonly
224+
isTypeReadonlyRecurser(checker, type, options, new Set()) ===
225+
Readonlyness.Readonly
188226
);
189227
}
190228

Diff for: packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts

+39
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,45 @@ ruleTester.run('prefer-readonly-parameter-types', rule, {
154154
}
155155
function foo(arg: Readonly<Foo>) {}
156156
`,
157+
// methods treated as readonly
158+
{
159+
code: `
160+
class Foo {
161+
method() {}
162+
}
163+
function foo(arg: Foo) {}
164+
`,
165+
options: [
166+
{
167+
treatMethodsAsReadonly: true,
168+
},
169+
],
170+
},
171+
{
172+
code: `
173+
interface Foo {
174+
method(): void;
175+
}
176+
function foo(arg: Foo) {}
177+
`,
178+
options: [
179+
{
180+
treatMethodsAsReadonly: true,
181+
},
182+
],
183+
},
184+
// ReadonlySet and ReadonlyMap are seen as readonly when methods are treated as readonly
185+
{
186+
code: `
187+
function foo(arg: ReadonlySet) {}
188+
function bar(arg: ReadonlyMap) {}
189+
`,
190+
options: [
191+
{
192+
treatMethodsAsReadonly: true,
193+
},
194+
],
195+
},
157196

158197
// parameter properties should work fine
159198
{

0 commit comments

Comments
 (0)