Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.

Commit a15541d

Browse files
Josh Goldbergadidahiya
Josh Goldberg
authored andcommitted
Added allow-generics option to invalid-void rule (#4839)
1 parent 0f2a540 commit a15541d

File tree

9 files changed

+177
-12
lines changed

9 files changed

+177
-12
lines changed

src/rules/invalidVoidRule.ts

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,84 @@
1515
* limitations under the License.
1616
*/
1717

18+
import * as tsutils from "tsutils";
1819
import * as ts from "typescript";
1920

2021
import * as Lint from "../index";
2122

23+
const OPTION_ALLOW_GENERICS = "allow-generics";
24+
25+
interface Options {
26+
allowGenerics: boolean | Set<string>;
27+
}
28+
29+
type RawOptions =
30+
| undefined
31+
| {
32+
[OPTION_ALLOW_GENERICS]?: boolean | Set<string>;
33+
};
34+
35+
type GenericReference = ts.NewExpression | ts.TypeReferenceNode;
36+
2237
export class Rule extends Lint.Rules.AbstractRule {
2338
/* tslint:disable:object-literal-sort-keys */
2439
public static metadata: Lint.IRuleMetadata = {
2540
ruleName: "invalid-void",
2641
description: Lint.Utils.dedent`
27-
Disallows usage of \`void\` type outside of return type.
42+
Disallows usage of \`void\` type outside of generic or return types.
2843
If \`void\` is used as return type, it shouldn't be a part of intersection/union type.`,
2944
rationale: Lint.Utils.dedent`
3045
The \`void\` type means "nothing" or that a function does not return any value,
3146
in contra with implicit \`undefined\` type which means that a function returns a value \`undefined\`.
3247
So "nothing" cannot be mixed with any other types.
3348
If you need this - use \`undefined\` type instead.`,
3449
hasFix: false,
35-
optionsDescription: "Not configurable.",
36-
options: null,
37-
optionExamples: [true],
50+
optionsDescription: Lint.Utils.dedent`
51+
If \`${OPTION_ALLOW_GENERICS}\` is specified as \`false\`, then generic types will no longer be allowed to to be \`void\`.
52+
Alternately, provide an array of strings for \`${OPTION_ALLOW_GENERICS}\` to exclusively allow generic types by those names.`,
53+
options: {
54+
type: "object",
55+
properties: {
56+
[OPTION_ALLOW_GENERICS]: {
57+
oneOf: [
58+
{ type: "boolean" },
59+
{ type: "array", items: { type: "string" }, minLength: 1 },
60+
],
61+
},
62+
},
63+
additionalProperties: false,
64+
},
65+
optionExamples: [
66+
true,
67+
[true, { [OPTION_ALLOW_GENERICS]: false }],
68+
[true, { [OPTION_ALLOW_GENERICS]: ["Promise", "PromiseLike"] }],
69+
],
3870
type: "maintainability",
3971
typescriptOnly: true,
4072
};
4173
/* tslint:enable:object-literal-sort-keys */
4274

43-
public static FAILURE_STRING = "void is not a valid type other than return types";
75+
public static FAILURE_STRING_ALLOW_GENERICS =
76+
"void is only valid as a return type or generic type variable";
77+
public static FAILURE_STRING_NO_GENERICS = "void is only valid as a return type";
78+
public static FAILURE_WRONG_GENERIC = (genericName: string) =>
79+
`${genericName} may not have void as a type variable`;
4480

4581
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
46-
return this.applyWithFunction(sourceFile, walk);
82+
return this.applyWithFunction(sourceFile, walk, {
83+
// tslint:disable-next-line:no-object-literal-type-assertion
84+
allowGenerics: this.getAllowGenerics(this.ruleArguments[0] as RawOptions),
85+
});
86+
}
87+
88+
private getAllowGenerics(rawArgument: RawOptions) {
89+
if (rawArgument == undefined) {
90+
return true;
91+
}
92+
93+
const allowGenerics = rawArgument[OPTION_ALLOW_GENERICS];
94+
95+
return allowGenerics instanceof Array ? new Set(allowGenerics) : Boolean(allowGenerics);
4796
}
4897
}
4998

@@ -75,10 +124,60 @@ const failedKinds = new Set([
75124
ts.SyntaxKind.CallExpression,
76125
]);
77126

78-
function walk(ctx: Lint.WalkContext): void {
127+
function walk(ctx: Lint.WalkContext<Options>): void {
128+
const defaultFailureString = ctx.options.allowGenerics
129+
? Rule.FAILURE_STRING_ALLOW_GENERICS
130+
: Rule.FAILURE_STRING_NO_GENERICS;
131+
132+
const getGenericReferenceName = (node: GenericReference) => {
133+
const rawName = tsutils.isNewExpression(node) ? node.expression : node.typeName;
134+
135+
return tsutils.isIdentifier(rawName) ? rawName.text : rawName.getText(ctx.sourceFile);
136+
};
137+
138+
const getTypeReferenceFailure = (node: GenericReference) => {
139+
if (!(ctx.options.allowGenerics instanceof Set)) {
140+
return ctx.options.allowGenerics ? undefined : defaultFailureString;
141+
}
142+
143+
const genericName = getGenericReferenceName(node);
144+
145+
return ctx.options.allowGenerics.has(genericName)
146+
? undefined
147+
: Rule.FAILURE_WRONG_GENERIC(genericName);
148+
};
149+
150+
const checkTypeReference = (parent: GenericReference, node: ts.Node) => {
151+
const failure = getTypeReferenceFailure(parent);
152+
153+
if (failure !== undefined) {
154+
ctx.addFailureAtNode(node, failure);
155+
}
156+
};
157+
158+
const isParentGenericReference = (
159+
parent: ts.Node,
160+
node: ts.Node,
161+
): parent is GenericReference => {
162+
if (tsutils.isTypeReferenceNode(parent)) {
163+
return true;
164+
}
165+
166+
return (
167+
tsutils.isNewExpression(parent) &&
168+
parent.typeArguments !== undefined &&
169+
ts.isTypeNode(node) &&
170+
parent.typeArguments.indexOf(node) !== -1
171+
);
172+
};
173+
79174
ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node) {
80175
if (node.kind === ts.SyntaxKind.VoidKeyword && failedKinds.has(node.parent.kind)) {
81-
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
176+
if (isParentGenericReference(node.parent, node)) {
177+
checkTypeReference(node.parent, node);
178+
} else {
179+
ctx.addFailureAtNode(node, defaultFailureString);
180+
}
82181
}
83182

84183
ts.forEachChild(node, cb);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type Generic<T> = [T];
2+
type GenericVoid = Generic<void>;
3+
~~~~ [0]
4+
5+
function takeVoid(thing: void) { }
6+
~~~~ [0]
7+
8+
let voidPromise: Promise<void> = new Promise<void>(() => {});
9+
~~~~ [0]
10+
~~~~ [0]
11+
12+
let voidMap: Map<string, void> = new Map<string, void>();
13+
~~~~ [0]
14+
~~~~ [0]
15+
16+
[0]: void is only valid as a return type
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": {
3+
"invalid-void": [true, {
4+
"allow-generics": false
5+
}]
6+
}
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
type Generic<T> = [T];
2+
type GenericVoid = Generic<void>;
3+
4+
function takeVoid(thing: void) { }
5+
~~~~ [0]
6+
7+
let voidPromise: Promise<void> = new Promise<void>(() => {});
8+
9+
let voidMap: Map<string, void> = new Map<string, void>();
10+
11+
[0]: void is only valid as a return type or generic type variable
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": {
3+
"invalid-void": [true, {
4+
"allow-generics": true
5+
}]
6+
}
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
type Allowed<T> = [T];
2+
type AllowedVoid = Allowed<void>;
3+
4+
type Banned<T> = [T];
5+
type BannedVoid = Banned<void>;
6+
~~~~ [Generic % ('Banned')]
7+
8+
function takeVoid(thing: void) { }
9+
~~~~ [0]
10+
11+
[0]: void is only valid as a return type or generic type variable
12+
[Generic]: %s may not have void as a type variable
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": {
3+
"invalid-void": [true, {
4+
"allow-generics": ["Allowed"]
5+
}]
6+
}
7+
}

test/rules/invalid-void/test.ts.lint renamed to test/rules/invalid-void/default/test.ts.lint

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ class ClassName {
7575
~~~~ [0]
7676
}
7777

78-
let invalidMap: Map<string, void> = new Map<string, void>();
79-
~~~~ [0]
80-
~~~~ [0]
78+
let voidPromise: Promise<void> = new Promise<void>(() => {});
79+
80+
let voidMap: Map<string, void> = new Map<string, void>();
8181

8282
let letVoid: void;
8383
~~~~ [0]
@@ -99,6 +99,12 @@ type UnionType3 = string | (number & any | (string | void));
9999
type IntersectionType = string & number & void;
100100
~~~~ [0]
101101

102+
function returnsVoidPromiseDirectly(): Promise<void> {
103+
return Promise.resolve();
104+
}
105+
106+
async function returnsVoidPromiseAsync(): Promise<void> {}
107+
102108
#if typescript >= 2.8.0
103109
type MappedType<T> = {
104110
[K in keyof T]: void;
@@ -118,4 +124,4 @@ function foo(arr: readonly void[]) { }
118124
~~~~ [0]
119125
#endif
120126

121-
[0]: void is not a valid type other than return types
127+
[0]: void is only valid as a return type or generic type variable

0 commit comments

Comments
 (0)