Skip to content

Commit a974191

Browse files
fix(eslint-plugin): [prefer-readonly-parameter-types] ignore tagged primitives (#11660)
* fix(eslint-plugin): [prefer-readonly-parameter-types] ignore tagged primitives * A lot more testing, and no more length === 2 * fix lint complaitns * Fix more reports * git checkout main -- .vscode/launch.json * Suggestions on isTypeBrandedLiteralLike * Switch to .every
1 parent 02e0278 commit a974191

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { TypeOrValueSpecifier } from '../util';
77
import {
88
createRule,
99
getParserServices,
10+
isTypeBrandedLiteralLike,
1011
isTypeReadonly,
1112
readonlynessOptionsDefaults,
1213
readonlynessOptionsSchema,
@@ -129,7 +130,7 @@ export default createRule<Options, MessageIds>({
129130
treatMethodsAsReadonly: !!treatMethodsAsReadonly,
130131
});
131132

132-
if (!isReadOnly) {
133+
if (!isReadOnly && !isTypeBrandedLiteralLike(type)) {
133134
context.report({
134135
node: actualParam,
135136
messageId: 'shouldBeReadonly',

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,114 @@ function foo(arg: Test) {}
320320
321321
const willNotCrash = (foo: Readonly<WithSymbol>) => {};
322322
`,
323+
`
324+
type TaggedBigInt = bigint & {
325+
readonly __tag: unique symbol;
326+
};
327+
function custom1(arg: TaggedBigInt) {}
328+
`,
329+
`
330+
type TaggedNumber = number & {
331+
readonly __tag: unique symbol;
332+
};
333+
function custom1(arg: TaggedNumber) {}
334+
`,
335+
`
336+
type TaggedString = string & {
337+
readonly __tag: unique symbol;
338+
};
339+
function custom1(arg: TaggedString) {}
340+
`,
341+
`
342+
type TaggedString = string & {
343+
readonly __tagA: unique symbol;
344+
readonly __tagB: unique symbol;
345+
};
346+
function custom1(arg: TaggedString) {}
347+
`,
348+
`
349+
type TaggedString = string & {
350+
readonly __tag: unique symbol;
351+
};
352+
353+
type OtherSpecialString = string & {
354+
readonly ' __other_tag': unique symbol;
355+
};
356+
357+
function custom1(arg: TaggedString | OtherSpecialString) {}
358+
`,
359+
`
360+
type TaggedTemplateLiteral = \`\${string}-\${string}\` & {
361+
readonly __tag: unique symbol;
362+
};
363+
function custom1(arg: TaggedTemplateLiteral) {}
364+
`,
365+
`
366+
type TaggedNumber = 1 & {
367+
readonly __tag: unique symbol;
368+
};
369+
370+
function custom1(arg: TaggedNumber) {}
371+
`,
372+
`
373+
type TaggedNumber = (1 | 2) & {
374+
readonly __tag: unique symbol;
375+
};
376+
377+
function custom1(arg: TaggedNumber) {}
378+
`,
379+
`
380+
type TaggedString = ('a' | 'b') & {
381+
readonly __tag: unique symbol;
382+
};
383+
384+
function custom1(arg: TaggedString) {}
385+
`,
386+
`
387+
type Strings = 'one' | 'two' | 'three';
388+
389+
type TaggedString = Strings & {
390+
readonly __tag: unique symbol;
391+
};
392+
393+
function custom1(arg: TaggedString) {}
394+
`,
395+
`
396+
type Strings = 'one' | 'two' | 'three';
397+
398+
type TaggedString = Strings & {
399+
__tag: unique symbol;
400+
};
401+
402+
function custom1(arg: TaggedString) {}
403+
`,
404+
`
405+
type TaggedString = string & {
406+
__tag: unique symbol;
407+
} & {
408+
__tag: unique symbol;
409+
};
410+
function custom1(arg: TaggedString) {}
411+
`,
412+
`
413+
type TaggedString = string & {
414+
__tagA: unique symbol;
415+
} & {
416+
__tagB: unique symbol;
417+
};
418+
function custom1(arg: TaggedString) {}
419+
`,
420+
`
421+
type TaggedString = string &
422+
({ __tag: unique symbol } | { __tag: unique symbol });
423+
function custom1(arg: TaggedString) {}
424+
`,
425+
`
426+
type TaggedFunction = (() => void) & {
427+
readonly __tag: unique symbol;
428+
};
429+
function custom1(arg: TaggedFunction) {}
430+
`,
323431
{
324432
code: `
325433
type Callback<T> = (options: T) => void;
@@ -909,6 +1017,23 @@ function foo(arg: Test) {}
9091017
},
9101018
],
9111019
},
1020+
{
1021+
code: `
1022+
class ClassExample {}
1023+
type Test = typeof ClassExample & {
1024+
readonly property: boolean;
1025+
};
1026+
function foo(arg: Test) {}
1027+
`,
1028+
errors: [
1029+
{
1030+
column: 14,
1031+
endColumn: 23,
1032+
line: 6,
1033+
messageId: 'shouldBeReadonly',
1034+
},
1035+
],
1036+
},
9121037
{
9131038
code: `
9141039
const sym = Symbol('sym');

packages/type-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './getDeclaration';
66
export * from './getSourceFileOfNode';
77
export * from './getTypeName';
88
export * from './isSymbolFromDefaultLibrary';
9+
export * from './isTypeBrandedLiteralLike';
910
export * from './isTypeReadonly';
1011
export * from './isUnsafeAssignment';
1112
export * from './predicates';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as tsutils from 'ts-api-utils';
2+
import * as ts from 'typescript';
3+
4+
function isLiteralOrTaggablePrimitiveLike(type: ts.Type): boolean {
5+
return (
6+
type.isLiteral() ||
7+
tsutils.isTypeFlagSet(
8+
type,
9+
ts.TypeFlags.BigInt |
10+
ts.TypeFlags.Number |
11+
ts.TypeFlags.String |
12+
ts.TypeFlags.TemplateLiteral,
13+
)
14+
);
15+
}
16+
17+
function isObjectLiteralLike(type: ts.Type): boolean {
18+
return (
19+
!type.getCallSignatures().length &&
20+
!type.getConstructSignatures().length &&
21+
tsutils.isObjectType(type)
22+
);
23+
}
24+
25+
function isTypeBrandedLiteral(type: ts.Type): boolean {
26+
if (!type.isIntersection()) {
27+
return false;
28+
}
29+
30+
let hadObjectLike = false;
31+
let hadPrimitiveLike = false;
32+
33+
for (const constituent of type.types) {
34+
if (isObjectLiteralLike(constituent)) {
35+
hadPrimitiveLike = true;
36+
} else if (isLiteralOrTaggablePrimitiveLike(constituent)) {
37+
hadObjectLike = true;
38+
} else {
39+
return false;
40+
}
41+
}
42+
43+
return hadPrimitiveLike && hadObjectLike;
44+
}
45+
46+
export function isTypeBrandedLiteralLike(type: ts.Type): boolean {
47+
return type.isUnion()
48+
? type.types.every(isTypeBrandedLiteral)
49+
: isTypeBrandedLiteral(type);
50+
}

0 commit comments

Comments
 (0)