Skip to content

Commit 7e70479

Browse files
feat(prefer-readonly-type-alias): add support for interfaces
1 parent 06f9c78 commit 7e70479

File tree

5 files changed

+424
-97
lines changed

5 files changed

+424
-97
lines changed

src/rules/prefer-readonly-type-alias.ts

Lines changed: 108 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import type { JSONSchema4 } from "json-schema";
33

44
import type { RuleContext, RuleMetaData, RuleResult } from "~/util/rule";
55
import { createRule, isReadonly } from "~/util/rule";
6-
import { getParentTypeAliasDeclaration } from "~/util/tree";
6+
import { getAncestorOfType } from "~/util/tree";
77
import {
88
isIdentifier,
99
isTSArrayType,
10+
isTSIndexSignature,
11+
isTSInterfaceDeclaration,
1012
isTSParameterProperty,
1113
isTSTupleType,
14+
isTSTypeAliasDeclaration,
1215
isTSTypeOperator,
1316
} from "~/util/typeguard";
1417

@@ -32,6 +35,7 @@ type Options = {
3235
readonly requireOthersToBeReadonly: boolean;
3336
};
3437
readonly blacklist: ReadonlyArray<string>;
38+
readonly ignoreInterface: boolean;
3539
};
3640

3741
// The schema for the rule options.
@@ -75,6 +79,9 @@ const schema: JSONSchema4 = [
7579
type: "string",
7680
},
7781
},
82+
ignoreInterface: {
83+
type: "boolean",
84+
},
7885
},
7986
additionalProperties: false,
8087
},
@@ -83,14 +90,15 @@ const schema: JSONSchema4 = [
8390
// The default options for the rule.
8491
const defaultOptions: Options = {
8592
mustBeReadonly: {
86-
pattern: "^Readonly",
93+
pattern: "^(I?)Readonly",
8794
requireOthersToBeMutable: false,
8895
},
8996
mustBeMutable: {
90-
pattern: "^Mutable",
97+
pattern: "^(I?)Mutable",
9198
requireOthersToBeReadonly: true,
9299
},
93100
blacklist: ["^Mutable$"],
101+
ignoreInterface: false,
94102
};
95103

96104
// The possible error messages.
@@ -130,7 +138,7 @@ const mutableToImmutableTypes: ReadonlyMap<string, string> = new Map<
130138
["Set", "ReadonlySet"],
131139
]);
132140

133-
enum TypeAliasDeclarationDetails {
141+
enum TypeReadonlynessDetails {
134142
ERROR_MUTABLE_READONLY,
135143
NEEDS_EXPLICIT_MARKING,
136144
IGNORE,
@@ -140,37 +148,51 @@ enum TypeAliasDeclarationDetails {
140148
READONLY_NOT_OK,
141149
}
142150

143-
const cachedTypeAliasDeclarationsDetails = new WeakMap<
144-
TSESTree.TSTypeAliasDeclaration,
145-
TypeAliasDeclarationDetails
151+
const cachedDetails = new WeakMap<
152+
TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
153+
TypeReadonlynessDetails
146154
>();
147155

148156
/**
149157
* Get the details for the given type alias.
150158
*/
151159
function getTypeAliasDeclarationDetails(
152-
node: TSESTree.TSTypeAliasDeclaration,
160+
node: TSESTree.Node,
153161
context: RuleContext<keyof typeof errorMessages, Options>,
154162
options: Options
155-
): TypeAliasDeclarationDetails {
156-
const cached = cachedTypeAliasDeclarationsDetails.get(node);
163+
): TypeReadonlynessDetails {
164+
const typeDeclaration = getTypeDeclaration(node);
165+
if (typeDeclaration === null) {
166+
return TypeReadonlynessDetails.IGNORE;
167+
}
168+
169+
const indexSignature = getParentIndexSignature(node);
170+
if (indexSignature !== null && getTypeDeclaration(indexSignature) !== null) {
171+
return TypeReadonlynessDetails.IGNORE;
172+
}
173+
174+
const cached = cachedDetails.get(typeDeclaration);
157175
if (cached !== undefined) {
158176
return cached;
159177
}
160178

161-
const result = getTypeAliasDeclarationDetailsInternal(node, context, options);
162-
cachedTypeAliasDeclarationsDetails.set(node, result);
179+
const result = getTypeAliasDeclarationDetailsInternal(
180+
typeDeclaration,
181+
context,
182+
options
183+
);
184+
cachedDetails.set(typeDeclaration, result);
163185
return result;
164186
}
165187

166188
/**
167189
* Get the details for the given type alias.
168190
*/
169191
function getTypeAliasDeclarationDetailsInternal(
170-
node: TSESTree.TSTypeAliasDeclaration,
192+
node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
171193
context: RuleContext<keyof typeof errorMessages, Options>,
172194
options: Options
173-
): TypeAliasDeclarationDetails {
195+
): TypeReadonlynessDetails {
174196
const blacklistPatterns = (
175197
Array.isArray(options.blacklist) ? options.blacklist : [options.blacklist]
176198
).map((pattern) => new RegExp(pattern, "u"));
@@ -180,7 +202,7 @@ function getTypeAliasDeclarationDetailsInternal(
180202
);
181203

182204
if (blacklisted) {
183-
return TypeAliasDeclarationDetails.IGNORE;
205+
return TypeReadonlynessDetails.IGNORE;
184206
}
185207

186208
const mustBeReadonlyPatterns = (
@@ -203,7 +225,7 @@ function getTypeAliasDeclarationDetailsInternal(
203225
);
204226

205227
if (patternStatesReadonly && patternStatesMutable) {
206-
return TypeAliasDeclarationDetails.ERROR_MUTABLE_READONLY;
228+
return TypeReadonlynessDetails.ERROR_MUTABLE_READONLY;
207229
}
208230

209231
if (
@@ -212,7 +234,7 @@ function getTypeAliasDeclarationDetailsInternal(
212234
options.mustBeReadonly.requireOthersToBeMutable &&
213235
options.mustBeMutable.requireOthersToBeReadonly
214236
) {
215-
return TypeAliasDeclarationDetails.NEEDS_EXPLICIT_MARKING;
237+
return TypeReadonlynessDetails.NEEDS_EXPLICIT_MARKING;
216238
}
217239

218240
const requiredReadonlyness =
@@ -226,34 +248,44 @@ function getTypeAliasDeclarationDetailsInternal(
226248
: RequiredReadonlyness.EITHER;
227249

228250
if (requiredReadonlyness === RequiredReadonlyness.EITHER) {
229-
return TypeAliasDeclarationDetails.IGNORE;
251+
return TypeReadonlynessDetails.IGNORE;
230252
}
231253

232-
const readonly = isReadonly(node.typeAnnotation, context);
254+
const readonly = isReadonly(
255+
isTSTypeAliasDeclaration(node) ? node.typeAnnotation : node.body,
256+
context
257+
);
233258

234259
if (requiredReadonlyness === RequiredReadonlyness.MUTABLE) {
235260
return readonly
236-
? TypeAliasDeclarationDetails.MUTABLE_NOT_OK
237-
: TypeAliasDeclarationDetails.MUTABLE_OK;
261+
? TypeReadonlynessDetails.MUTABLE_NOT_OK
262+
: TypeReadonlynessDetails.MUTABLE_OK;
238263
}
239264

240265
return readonly
241-
? TypeAliasDeclarationDetails.READONLY_OK
242-
: TypeAliasDeclarationDetails.READONLY_NOT_OK;
266+
? TypeReadonlynessDetails.READONLY_OK
267+
: TypeReadonlynessDetails.READONLY_NOT_OK;
243268
}
244269

245270
/**
246271
* Check if the given TypeReference violates this rule.
247272
*/
248273
function checkTypeAliasDeclaration(
249-
node: TSESTree.TSTypeAliasDeclaration,
274+
node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
250275
context: RuleContext<keyof typeof errorMessages, Options>,
251276
options: Options
252277
): RuleResult<keyof typeof errorMessages, Options> {
278+
if (options.ignoreInterface && isTSInterfaceDeclaration(node)) {
279+
return {
280+
context,
281+
descriptors: [],
282+
};
283+
}
284+
253285
const details = getTypeAliasDeclarationDetails(node, context, options);
254286

255287
switch (details) {
256-
case TypeAliasDeclarationDetails.NEEDS_EXPLICIT_MARKING: {
288+
case TypeReadonlynessDetails.NEEDS_EXPLICIT_MARKING: {
257289
return {
258290
context,
259291
descriptors: [
@@ -264,7 +296,7 @@ function checkTypeAliasDeclaration(
264296
],
265297
};
266298
}
267-
case TypeAliasDeclarationDetails.ERROR_MUTABLE_READONLY: {
299+
case TypeReadonlynessDetails.ERROR_MUTABLE_READONLY: {
268300
return {
269301
context,
270302
descriptors: [
@@ -275,7 +307,7 @@ function checkTypeAliasDeclaration(
275307
],
276308
};
277309
}
278-
case TypeAliasDeclarationDetails.MUTABLE_NOT_OK: {
310+
case TypeReadonlynessDetails.MUTABLE_NOT_OK: {
279311
return {
280312
context,
281313
descriptors: [
@@ -286,7 +318,7 @@ function checkTypeAliasDeclaration(
286318
],
287319
};
288320
}
289-
case TypeAliasDeclarationDetails.READONLY_NOT_OK: {
321+
case TypeReadonlynessDetails.READONLY_NOT_OK: {
290322
return {
291323
context,
292324
descriptors: [
@@ -314,19 +346,10 @@ function checkArrayOrTupleType(
314346
context: RuleContext<keyof typeof errorMessages, Options>,
315347
options: Options
316348
): RuleResult<keyof typeof errorMessages, Options> {
317-
const typeAlias = getParentTypeAliasDeclaration(node);
318-
319-
if (typeAlias === null) {
320-
return {
321-
context,
322-
descriptors: [],
323-
};
324-
}
325-
326-
const details = getTypeAliasDeclarationDetails(typeAlias, context, options);
349+
const details = getTypeAliasDeclarationDetails(node, context, options);
327350

328351
switch (details) {
329-
case TypeAliasDeclarationDetails.READONLY_NOT_OK: {
352+
case TypeReadonlynessDetails.READONLY_NOT_OK: {
330353
return {
331354
context,
332355
descriptors:
@@ -368,19 +391,10 @@ function checkMappedType(
368391
context: RuleContext<keyof typeof errorMessages, Options>,
369392
options: Options
370393
): RuleResult<keyof typeof errorMessages, Options> {
371-
const typeAlias = getParentTypeAliasDeclaration(node);
372-
373-
if (typeAlias === null) {
374-
return {
375-
context,
376-
descriptors: [],
377-
};
378-
}
379-
380-
const details = getTypeAliasDeclarationDetails(typeAlias, context, options);
394+
const details = getTypeAliasDeclarationDetails(node, context, options);
381395

382396
switch (details) {
383-
case TypeAliasDeclarationDetails.READONLY_NOT_OK: {
397+
case TypeReadonlynessDetails.READONLY_NOT_OK: {
384398
return {
385399
context,
386400
descriptors:
@@ -423,19 +437,10 @@ function checkTypeReference(
423437
};
424438
}
425439

426-
const typeAlias = getParentTypeAliasDeclaration(node);
427-
428-
if (typeAlias === null) {
429-
return {
430-
context,
431-
descriptors: [],
432-
};
433-
}
434-
435-
const details = getTypeAliasDeclarationDetails(typeAlias, context, options);
440+
const details = getTypeAliasDeclarationDetails(node, context, options);
436441

437442
switch (details) {
438-
case TypeAliasDeclarationDetails.READONLY_NOT_OK: {
443+
case TypeReadonlynessDetails.READONLY_NOT_OK: {
439444
const immutableType = mutableToImmutableTypes.get(node.typeName.name);
440445

441446
return {
@@ -473,19 +478,10 @@ function checkProperty(
473478
context: RuleContext<keyof typeof errorMessages, Options>,
474479
options: Options
475480
): RuleResult<keyof typeof errorMessages, Options> {
476-
const typeAlias = getParentTypeAliasDeclaration(node);
477-
478-
if (typeAlias === null) {
479-
return {
480-
context,
481-
descriptors: [],
482-
};
483-
}
484-
485-
const details = getTypeAliasDeclarationDetails(typeAlias, context, options);
481+
const details = getTypeAliasDeclarationDetails(node, context, options);
486482

487483
switch (details) {
488-
case TypeAliasDeclarationDetails.READONLY_NOT_OK: {
484+
case TypeReadonlynessDetails.READONLY_NOT_OK: {
489485
return {
490486
context,
491487
descriptors:
@@ -512,6 +508,45 @@ function checkProperty(
512508
}
513509
}
514510

511+
/**
512+
* Get the type alias or interface that the given node is in.
513+
*/
514+
function getTypeDeclaration(
515+
node: TSESTree.Node
516+
): TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration | null {
517+
if (isTSTypeAliasDeclaration(node) || isTSInterfaceDeclaration(node)) {
518+
return node;
519+
}
520+
521+
return (getAncestorOfType(
522+
(n): n is TSESTree.Node =>
523+
n.parent !== undefined &&
524+
n.parent !== null &&
525+
((isTSTypeAliasDeclaration(n.parent) && n.parent.typeAnnotation === n) ||
526+
(isTSInterfaceDeclaration(n.parent) && n.parent.body === n)),
527+
node
528+
)?.parent ?? null) as
529+
| TSESTree.TSInterfaceDeclaration
530+
| TSESTree.TSTypeAliasDeclaration
531+
| null;
532+
}
533+
534+
/**
535+
* Get the parent Index Signature that the given node is in.
536+
*/
537+
function getParentIndexSignature(
538+
node: TSESTree.Node
539+
): TSESTree.TSIndexSignature | null {
540+
return (getAncestorOfType(
541+
(n): n is TSESTree.Node =>
542+
n.parent !== undefined &&
543+
n.parent !== null &&
544+
isTSIndexSignature(n.parent) &&
545+
n.parent.typeAnnotation === n,
546+
node
547+
)?.parent ?? null) as TSESTree.TSIndexSignature | null;
548+
}
549+
515550
// Create the rule.
516551
export const rule = createRule<keyof typeof errorMessages, Options>(
517552
name,
@@ -520,6 +555,7 @@ export const rule = createRule<keyof typeof errorMessages, Options>(
520555
{
521556
TSArrayType: checkArrayOrTupleType,
522557
TSIndexSignature: checkProperty,
558+
TSInterfaceDeclaration: checkTypeAliasDeclaration,
523559
TSMappedType: checkMappedType,
524560
TSParameterProperty: checkProperty,
525561
TSPropertySignature: checkProperty,

0 commit comments

Comments
 (0)