• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2  AST_NODE_TYPES,
3  TSESTree,
4} from '@typescript-eslint/experimental-utils';
5import * as util from '../util';
6
7/**
8 * Check whatever node can be considered as simple
9 * @param node the node to be evaluated.
10 */
11function isSimpleType(node: TSESTree.Node): boolean {
12  switch (node.type) {
13    case AST_NODE_TYPES.Identifier:
14    case AST_NODE_TYPES.TSAnyKeyword:
15    case AST_NODE_TYPES.TSBooleanKeyword:
16    case AST_NODE_TYPES.TSNeverKeyword:
17    case AST_NODE_TYPES.TSNumberKeyword:
18    case AST_NODE_TYPES.TSObjectKeyword:
19    case AST_NODE_TYPES.TSStringKeyword:
20    case AST_NODE_TYPES.TSSymbolKeyword:
21    case AST_NODE_TYPES.TSUnknownKeyword:
22    case AST_NODE_TYPES.TSVoidKeyword:
23    case AST_NODE_TYPES.TSNullKeyword:
24    case AST_NODE_TYPES.TSArrayType:
25    case AST_NODE_TYPES.TSUndefinedKeyword:
26    case AST_NODE_TYPES.TSThisType:
27    case AST_NODE_TYPES.TSQualifiedName:
28      return true;
29    case AST_NODE_TYPES.TSTypeReference:
30      if (
31        node.typeName &&
32        node.typeName.type === AST_NODE_TYPES.Identifier &&
33        node.typeName.name === 'Array'
34      ) {
35        if (!node.typeParameters) {
36          return true;
37        }
38        if (node.typeParameters.params.length === 1) {
39          return isSimpleType(node.typeParameters.params[0]);
40        }
41      } else {
42        if (node.typeParameters) {
43          return false;
44        }
45        return isSimpleType(node.typeName);
46      }
47      return false;
48    default:
49      return false;
50  }
51}
52
53/**
54 * Check if node needs parentheses
55 * @param node the node to be evaluated.
56 */
57function typeNeedsParentheses(node: TSESTree.Node): boolean {
58  switch (node.type) {
59    case AST_NODE_TYPES.TSTypeReference:
60      return typeNeedsParentheses(node.typeName);
61    case AST_NODE_TYPES.TSUnionType:
62    case AST_NODE_TYPES.TSFunctionType:
63    case AST_NODE_TYPES.TSIntersectionType:
64    case AST_NODE_TYPES.TSTypeOperator:
65    case AST_NODE_TYPES.TSInferType:
66      return true;
67    case AST_NODE_TYPES.Identifier:
68      return node.name === 'ReadonlyArray';
69    default:
70      return false;
71  }
72}
73
74export type OptionString = 'array' | 'generic' | 'array-simple';
75type Options = [
76  {
77    default: OptionString;
78    readonly?: OptionString;
79  },
80];
81type MessageIds =
82  | 'errorStringGeneric'
83  | 'errorStringGenericSimple'
84  | 'errorStringArray'
85  | 'errorStringArraySimple';
86
87const arrayOption = { enum: ['array', 'generic', 'array-simple'] };
88
89export default util.createRule<Options, MessageIds>({
90  name: 'array-type',
91  meta: {
92    type: 'suggestion',
93    docs: {
94      description: 'Requires using either `T[]` or `Array<T>` for arrays',
95      category: 'Stylistic Issues',
96      // too opinionated to be recommended
97      recommended: false,
98    },
99    fixable: 'code',
100    messages: {
101      errorStringGeneric:
102        "Array type using '{{type}}[]' is forbidden. Use 'Array<{{type}}>' instead.",
103      errorStringGenericSimple:
104        "Array type using '{{type}}[]' is forbidden for non-simple types. Use 'Array<{{type}}>' instead.",
105      errorStringArray:
106        "Array type using 'Array<{{type}}>' is forbidden. Use '{{type}}[]' instead.",
107      errorStringArraySimple:
108        "Array type using 'Array<{{type}}>' is forbidden for simple types. Use '{{type}}[]' instead.",
109    },
110    schema: [
111      {
112        type: 'object',
113        properties: {
114          default: arrayOption,
115          readonly: arrayOption,
116        },
117      },
118    ],
119  },
120  defaultOptions: [
121    {
122      default: 'array',
123    },
124  ],
125  create(context, [options]) {
126    const sourceCode = context.getSourceCode();
127
128    const defaultOption = options.default;
129    const readonlyOption = options.readonly ?? defaultOption;
130
131    /**
132     * @param node the node to be evaluated.
133     */
134    function getMessageType(node: TSESTree.Node): string {
135      if (node) {
136        if (node.type === AST_NODE_TYPES.TSParenthesizedType) {
137          return getMessageType(node.typeAnnotation);
138        }
139        if (isSimpleType(node)) {
140          return sourceCode.getText(node);
141        }
142      }
143      return 'T';
144    }
145
146    return {
147      TSArrayType(node): void {
148        const isReadonly =
149          node.parent &&
150          node.parent.type === AST_NODE_TYPES.TSTypeOperator &&
151          node.parent.operator === 'readonly';
152
153        const currentOption = isReadonly ? readonlyOption : defaultOption;
154
155        if (
156          currentOption === 'array' ||
157          (currentOption === 'array-simple' && isSimpleType(node.elementType))
158        ) {
159          return;
160        }
161
162        const messageId =
163          currentOption === 'generic'
164            ? 'errorStringGeneric'
165            : 'errorStringGenericSimple';
166        const errorNode = isReadonly ? node.parent! : node;
167
168        context.report({
169          node: errorNode,
170          messageId,
171          data: {
172            type: getMessageType(node.elementType),
173          },
174          fix(fixer) {
175            const typeNode =
176              node.elementType.type === AST_NODE_TYPES.TSParenthesizedType
177                ? node.elementType.typeAnnotation
178                : node.elementType;
179
180            const arrayType = isReadonly ? 'ReadonlyArray' : 'Array';
181
182            return [
183              fixer.replaceTextRange(
184                [errorNode.range[0], typeNode.range[0]],
185                `${arrayType}<`,
186              ),
187              fixer.replaceTextRange(
188                [typeNode.range[1], errorNode.range[1]],
189                '>',
190              ),
191            ];
192          },
193        });
194      },
195
196      TSTypeReference(node): void {
197        if (
198          node.typeName.type !== AST_NODE_TYPES.Identifier ||
199          !(
200            node.typeName.name === 'Array' ||
201            node.typeName.name === 'ReadonlyArray'
202          )
203        ) {
204          return;
205        }
206
207        const isReadonlyArrayType = node.typeName.name === 'ReadonlyArray';
208        const currentOption = isReadonlyArrayType
209          ? readonlyOption
210          : defaultOption;
211
212        if (currentOption === 'generic') {
213          return;
214        }
215
216        const readonlyPrefix = isReadonlyArrayType ? 'readonly ' : '';
217        const typeParams = node.typeParameters?.params;
218        const messageId =
219          currentOption === 'array'
220            ? 'errorStringArray'
221            : 'errorStringArraySimple';
222
223        if (!typeParams || typeParams.length === 0) {
224          // Create an 'any' array
225          context.report({
226            node,
227            messageId,
228            data: {
229              type: 'any',
230            },
231            fix(fixer) {
232              return fixer.replaceText(node, `${readonlyPrefix}any[]`);
233            },
234          });
235
236          return;
237        }
238
239        if (
240          typeParams.length !== 1 ||
241          (currentOption === 'array-simple' && !isSimpleType(typeParams[0]))
242        ) {
243          return;
244        }
245
246        const type = typeParams[0];
247        const typeParens = typeNeedsParentheses(type);
248        const parentParens =
249          readonlyPrefix && node.parent?.type === AST_NODE_TYPES.TSArrayType;
250
251        const start = `${parentParens ? '(' : ''}${readonlyPrefix}${
252          typeParens ? '(' : ''
253        }`;
254        const end = `${typeParens ? ')' : ''}[]${parentParens ? ')' : ''}`;
255
256        context.report({
257          node,
258          messageId,
259          data: {
260            type: getMessageType(type),
261          },
262          fix(fixer) {
263            return [
264              fixer.replaceTextRange([node.range[0], type.range[0]], start),
265              fixer.replaceTextRange([type.range[1], node.range[1]], end),
266            ];
267          },
268        });
269      },
270    };
271  },
272});
273