Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [array-type] detect Readonly<string[]> case #8752

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion packages/eslint-plugin/docs/rules/array-type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ const y: readonly string[] = ['a', 'b'];

### `"generic"`

Always use `Array<T>` or `ReadonlyArray<T>` for all array types.
Always use `Array<T>`, `ReadonlyArray<T>`, or `Readonly<Array<T>>` for all array types.
`readonly T[]` will be modified to `ReadonlyArray<T>` and `Readonly<T[]>` will be modified to `Readonly<Array<T>`.

<Tabs>
<TabItem value="❌ Incorrect">

```ts option='{ "default": "generic" }'
const x: string[] = ['a', 'b'];
const y: readonly string[] = ['a', 'b'];
const z: Readonly<string[]> = ['a', 'b'];
```

</TabItem>
Expand All @@ -58,6 +60,7 @@ const y: readonly string[] = ['a', 'b'];
```ts option='{ "default": "generic" }'
const x: Array<string> = ['a', 'b'];
const y: ReadonlyArray<string> = ['a', 'b'];
const z: Readonly<Array<string>> = ['a', 'b'];
```

</TabItem>
Expand Down
28 changes: 21 additions & 7 deletions packages/eslint-plugin/src/rules/array-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ type MessageIds =
| 'errorStringArray'
| 'errorStringArraySimple'
| 'errorStringGeneric'
| 'errorStringGenericSimple';
| 'errorStringGenericSimple'
| 'errorStringArrayReadonly';

export default createRule<Options, MessageIds>({
name: 'array-type',
Expand All @@ -99,6 +100,8 @@ export default createRule<Options, MessageIds>({
"Array type using '{{readonlyPrefix}}{{type}}[]' is forbidden. Use '{{className}}<{{type}}>' instead.",
errorStringArray:
"Array type using '{{className}}<{{type}}>' is forbidden. Use '{{readonlyPrefix}}{{type}}[]' instead.",
errorStringArrayReadonly:
"Array type using '{{className}}<{{type}}>' is forbidden. Use '{{readonlyPrefix}}{{type}}' instead.",
errorStringArraySimple:
"Array type using '{{className}}<{{type}}>' is forbidden for simple types. Use '{{readonlyPrefix}}{{type}}[]' instead.",
errorStringGenericSimple:
Expand Down Expand Up @@ -199,13 +202,22 @@ export default createRule<Options, MessageIds>({
node.typeName.type !== AST_NODE_TYPES.Identifier ||
!(
node.typeName.name === 'Array' ||
node.typeName.name === 'ReadonlyArray'
)
node.typeName.name === 'ReadonlyArray' ||
node.typeName.name === 'Readonly'
) ||
(node.typeName.name === 'Readonly' &&
node.typeArguments?.params[0].type !== AST_NODE_TYPES.TSArrayType)
) {
return;
}

const isReadonlyArrayType = node.typeName.name === 'ReadonlyArray';
const isReadonlyWithGenericArrayType =
node.typeName.name === 'Readonly' &&
node.typeArguments?.params[0].type === AST_NODE_TYPES.TSArrayType;
Comment on lines +214 to +216
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] If the type name is Readonly and first type argument is not an array, we should return from the function. Because otherwise any Readonly<...> type will be reported.

const x: Readonly<string> = 'a'

playground

const isReadonlyArrayType =
node.typeName.name === 'ReadonlyArray' ||
isReadonlyWithGenericArrayType;

const currentOption = isReadonlyArrayType
? readonlyOption
: defaultOption;
Expand All @@ -218,7 +230,9 @@ export default createRule<Options, MessageIds>({
const typeParams = node.typeArguments?.params;
const messageId =
currentOption === 'array'
? 'errorStringArray'
? isReadonlyWithGenericArrayType
? 'errorStringArrayReadonly'
: 'errorStringArray'
: 'errorStringArraySimple';

if (!typeParams || typeParams.length === 0) {
Expand Down Expand Up @@ -256,13 +270,13 @@ export default createRule<Options, MessageIds>({
const start = `${parentParens ? '(' : ''}${readonlyPrefix}${
typeParens ? '(' : ''
}`;
const end = `${typeParens ? ')' : ''}[]${parentParens ? ')' : ''}`;
const end = `${typeParens ? ')' : ''}${isReadonlyWithGenericArrayType ? '' : `[]`}${parentParens ? ')' : ''}`;

context.report({
node,
messageId,
data: {
className: isReadonlyArrayType ? 'ReadonlyArray' : 'Array',
className: isReadonlyArrayType ? node.typeName.name : 'Array',
readonlyPrefix,
type: getMessageType(type),
},
Expand Down
23 changes: 23 additions & 0 deletions packages/eslint-plugin/tests/rules/array-type.test.ts
developer-bandi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ function bazFunction(baz: Arr<ArrayClass<String>>) {
code: 'let a: readonly Array<number>[] = [[]];',
options: [{ default: 'generic', readonly: 'array' }],
},
{
code: 'let a: Readonly = [];',
options: [{ default: 'generic', readonly: 'array' }],
},
{
code: "const x: Readonly<string> = 'a';",
options: [{ default: 'array' }],
},
],
invalid: [
// Base cases from https://github.com/typescript-eslint/typescript-eslint/issues/2323#issuecomment-663977655
Expand Down Expand Up @@ -1918,6 +1926,21 @@ interface FooInterface {
},
],
},
{
code: "const x: Readonly<string[]> = ['a', 'b'];",
output: "const x: readonly string[] = ['a', 'b'];",
options: [{ default: 'array' }],
errors: [
{
messageId: 'errorStringArrayReadonly',
data: {
className: 'Readonly',
readonlyPrefix: 'readonly ',
type: 'string[]',
},
},
],
},
],
});

Expand Down