Skip to content

Commit 5833576

Browse files
armano2JamesHenry
authored andcommitted
[FEAT] [array-type] Add rule (typescript-eslint#211)
1 parent c2a54d7 commit 5833576

File tree

6 files changed

+1147
-0
lines changed

6 files changed

+1147
-0
lines changed

packages/eslint-plugin-typescript/.editorconfig

+4
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ insert_final_newline = true
1010
# yarn / npm uses 2 spaces when it dumps the file after a change
1111
[package.json]
1212
indent_size = 2
13+
14+
# yml uses 2 spaces
15+
[*.yml]
16+
indent_size = 2

packages/eslint-plugin-typescript/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ This guarantees 100% compatibility between the plugin and the parser.
4949
<!-- Please run `npm run docs` to update this section -->
5050
<!-- begin rule list -->
5151
* [`typescript/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) — Require that member overloads be consecutive
52+
* [`typescript/array-type`](./docs/rules/array-type.md) — Requires using either `T[]` or `Array<T>` for arrays (`array-type` from TSLint)
5253
* [`typescript/ban-types`](./docs/rules/ban-types.md) — Enforces that types will not to be used (`ban-types` from TSLint)
5354
* [`typescript/camelcase`](./docs/rules/camelcase.md) — Enforce camelCase naming convention
5455
* [`typescript/class-name-casing`](./docs/rules/class-name-casing.md) — Require PascalCased class and interface names (`class-name` from TSLint)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Requires using either `T[]` or `Array<T>` for arrays (array-type)
2+
3+
```ts
4+
class Foo<T = Array<Array<Bar>>> extends Bar<T, Array<T>> implements Baz<Array<T>> {
5+
private s: Array<T>
6+
7+
constructor (p: Array<T>) {
8+
return new Array()
9+
}
10+
}
11+
```
12+
13+
## Rule Details
14+
15+
This rule aims to standardise usage of array.
16+
17+
## Options
18+
19+
Default config:
20+
21+
```JSON
22+
{
23+
"array-type": ["error", "array"]
24+
}
25+
```
26+
27+
- `array` enforces use of `T[]` for all types `T`.
28+
- `generic` enforces use of `Array<T>` for all types `T`.
29+
- `array-simple` enforces use of `T[]` if `T` is a simple type.
30+
31+
## Related to
32+
33+
* TSLint: [array-type](https://palantir.github.io/tslint/rules/array-type/)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* @fileoverview Requires using either `T[]` or `Array<T>` for arrays.
3+
* @author Mackie Underdown
4+
* @author Armano <https://github.com/armano2>
5+
*/
6+
"use strict";
7+
8+
const util = require("../util");
9+
10+
/**
11+
* Check whatever node can be considered as simple
12+
* @param {ASTNode} node the node to be evaluated.
13+
* @returns {*} true or false
14+
*/
15+
function isSimpleType(node) {
16+
switch (node.type) {
17+
case "Identifier":
18+
case "TSAnyKeyword":
19+
case "TSBooleanKeyword":
20+
case "TSNeverKeyword":
21+
case "TSNumberKeyword":
22+
case "TSObjectKeyword":
23+
case "TSStringKeyword":
24+
case "TSSymbolKeyword":
25+
case "TSUnknownKeyword":
26+
case "TSVoidKeyword":
27+
case "TSNullKeyword":
28+
case "TSArrayType":
29+
case "TSUndefinedKeyword":
30+
case "TSThisType":
31+
case "TSQualifiedName":
32+
return true;
33+
case "TSTypeReference":
34+
if (
35+
node.typeName &&
36+
node.typeName.type === "Identifier" &&
37+
node.typeName.name === "Array"
38+
) {
39+
if (!node.typeParameters) {
40+
return true;
41+
}
42+
if (node.typeParameters.params.length === 1) {
43+
return isSimpleType(node.typeParameters.params[0]);
44+
}
45+
} else {
46+
if (node.typeParameters) {
47+
return false;
48+
}
49+
return isSimpleType(node.typeName);
50+
}
51+
return false;
52+
default:
53+
return false;
54+
}
55+
}
56+
57+
/**
58+
* Check if node needs parentheses
59+
* @param {ASTNode} node the node to be evaluated.
60+
* @returns {*} true or false
61+
*/
62+
function typeNeedsParentheses(node) {
63+
if (node.type === "TSTypeReference") {
64+
switch (node.typeName.type) {
65+
case "TSUnionType":
66+
case "TSFunctionType":
67+
case "TSIntersectionType":
68+
case "TSTypeOperator":
69+
return true;
70+
default:
71+
return false;
72+
}
73+
}
74+
return false;
75+
}
76+
77+
//------------------------------------------------------------------------------
78+
// Rule Definition
79+
//------------------------------------------------------------------------------
80+
81+
module.exports = {
82+
meta: {
83+
docs: {
84+
description: "Requires using either `T[]` or `Array<T>` for arrays",
85+
extraDescription: [util.tslintRule("array-type")],
86+
category: "TypeScript",
87+
url:
88+
"https://github.com/bradzacher/eslint-plugin-typescript/blob/master/docs/rules/array-type.md",
89+
},
90+
fixable: "code",
91+
messages: {
92+
errorStringGeneric:
93+
"Array type using '{{type}}[]' is forbidden. Use 'Array<{{type}}>' instead.",
94+
errorStringGenericSimple:
95+
"Array type using '{{type}}[]' is forbidden for non-simple types. Use 'Array<{{type}}>' instead.",
96+
errorStringArray:
97+
"Array type using 'Array<{{type}}>' is forbidden. Use '{{type}}[]' instead.",
98+
errorStringArraySimple:
99+
"Array type using 'Array<{{type}}>' is forbidden for simple types. Use '{{type}}[]' instead.",
100+
},
101+
schema: [
102+
{
103+
enum: ["array", "generic", "array-simple"],
104+
},
105+
],
106+
},
107+
create(context) {
108+
const option = context.options[0] || "array";
109+
const sourceCode = context.getSourceCode();
110+
111+
/**
112+
* Check if whitespace is needed before this node
113+
* @param {ASTNode} node the node to be evaluated.
114+
* @returns {boolean} true of false
115+
*/
116+
function requireWhitespaceBefore(node) {
117+
const prevToken = sourceCode.getTokenBefore(node);
118+
119+
if (node.range[0] - prevToken.range[1] > 0) {
120+
return false;
121+
}
122+
123+
return prevToken.type === "Identifier";
124+
}
125+
126+
/**
127+
* @param {ASTNode} node the node to be evaluated.
128+
* @returns {string} Type used in message
129+
*/
130+
function getMessageType(node) {
131+
if (node) {
132+
if (node.type === "TSParenthesizedType") {
133+
return getMessageType(node.typeAnnotation);
134+
}
135+
if (isSimpleType(node)) {
136+
return sourceCode.getText(node);
137+
}
138+
}
139+
return "T";
140+
}
141+
142+
//----------------------------------------------------------------------
143+
// Public
144+
//----------------------------------------------------------------------
145+
146+
return {
147+
TSArrayType(node) {
148+
if (
149+
option === "array" ||
150+
(option === "array-simple" &&
151+
isSimpleType(node.elementType))
152+
) {
153+
return;
154+
}
155+
const messageId =
156+
option === "generic"
157+
? "errorStringGeneric"
158+
: "errorStringGenericSimple";
159+
160+
context.report({
161+
node,
162+
messageId,
163+
data: {
164+
type: getMessageType(node.elementType),
165+
},
166+
fix(fixer) {
167+
const startText = requireWhitespaceBefore(node);
168+
const toFix = [
169+
fixer.replaceTextRange(
170+
[node.range[1] - 2, node.range[1]],
171+
">"
172+
),
173+
fixer.insertTextBefore(
174+
node,
175+
`${startText ? " " : ""}Array<`
176+
),
177+
];
178+
179+
if (node.elementType.type === "TSParenthesizedType") {
180+
toFix.push(
181+
fixer.remove(
182+
sourceCode.getFirstToken(node.elementType)
183+
)
184+
);
185+
toFix.push(
186+
fixer.remove(
187+
sourceCode.getLastToken(node.elementType)
188+
)
189+
);
190+
}
191+
192+
return toFix;
193+
},
194+
});
195+
},
196+
TSTypeReference(node) {
197+
if (
198+
option === "generic" ||
199+
node.typeName.type !== "Identifier" ||
200+
node.typeName.name !== "Array"
201+
) {
202+
return;
203+
}
204+
const messageId =
205+
option === "array"
206+
? "errorStringArray"
207+
: "errorStringArraySimple";
208+
209+
const typeParams =
210+
node.typeParameters && node.typeParameters.params;
211+
212+
if (!typeParams || typeParams.length === 0) {
213+
// Create an 'any' array
214+
context.report({
215+
node,
216+
messageId,
217+
data: {
218+
type: "any",
219+
},
220+
fix(fixer) {
221+
return fixer.replaceText(node, "any[]");
222+
},
223+
});
224+
return;
225+
}
226+
227+
if (
228+
typeParams.length !== 1 ||
229+
(option === "array-simple" && !isSimpleType(typeParams[0]))
230+
) {
231+
return;
232+
}
233+
234+
const type = typeParams[0];
235+
const parens = typeNeedsParentheses(type);
236+
237+
context.report({
238+
node,
239+
messageId,
240+
data: {
241+
type: getMessageType(type),
242+
},
243+
fix(fixer) {
244+
return [
245+
fixer.replaceTextRange(
246+
[node.range[0], type.range[0]],
247+
parens ? "(" : ""
248+
),
249+
fixer.replaceTextRange(
250+
[type.range[1], node.range[1]],
251+
parens ? ")[]" : "[]"
252+
),
253+
];
254+
},
255+
});
256+
},
257+
};
258+
},
259+
};
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
env:
2+
mocha: true
13
rules:
24
node/no-unpublished-require: off # we’re using devDeps here.

0 commit comments

Comments
 (0)