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