diff --git a/src/configs/all.ts b/src/configs/all.ts index 8c3a6ff35e5..cc6303fc945 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -163,6 +163,7 @@ export const rules = { "cyclomatic-complexity": true, eofline: true, indent: { options: ["spaces"] }, + "invalid-void": true, "linebreak-style": { options: "LF" }, "max-classes-per-file": { options: 1 }, "max-file-line-count": { options: 1000 }, diff --git a/src/language/rule/abstractRule.ts b/src/language/rule/abstractRule.ts index 53dc9156f72..8e1d5bf5e75 100644 --- a/src/language/rule/abstractRule.ts +++ b/src/language/rule/abstractRule.ts @@ -67,7 +67,7 @@ export abstract class AbstractRule implements IRule { ): RuleFailure[]; protected applyWithFunction( sourceFile: ts.SourceFile, - walkFn: (ctx: WalkContext, programOrChecker?: U) => void, + walkFn: (ctx: WalkContext, programOrChecker?: U) => void, options?: T, programOrChecker?: U, ): RuleFailure[] { diff --git a/src/language/walker/walkContext.ts b/src/language/walker/walkContext.ts index 522af77174d..3bbdea62709 100644 --- a/src/language/walker/walkContext.ts +++ b/src/language/walker/walkContext.ts @@ -19,7 +19,7 @@ import * as ts from "typescript"; import { Fix, RuleFailure } from "../rule/rule"; -export class WalkContext { +export class WalkContext { public readonly failures: RuleFailure[] = []; constructor( diff --git a/src/language/walker/walker.ts b/src/language/walker/walker.ts index de92391e82e..41a1a75ade0 100644 --- a/src/language/walker/walker.ts +++ b/src/language/walker/walker.ts @@ -27,7 +27,7 @@ export interface IWalker { getFailures(): RuleFailure[]; } -export abstract class AbstractWalker extends WalkContext implements IWalker { +export abstract class AbstractWalker extends WalkContext implements IWalker { public abstract walk(sourceFile: ts.SourceFile): void; public getSourceFile() { diff --git a/src/rules/invalidVoidRule.ts b/src/rules/invalidVoidRule.ts new file mode 100644 index 00000000000..2baec30a50f --- /dev/null +++ b/src/rules/invalidVoidRule.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2019 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "invalid-void", + description: Lint.Utils.dedent` + Disallows usage of \`void\` type outside of return type. + If \`void\` is used as return type, it shouldn't be a part of intersection/union type.`, + rationale: Lint.Utils.dedent` + The \`void\` type means "nothing" or that a function does not return any value, + in contra with implicit \`undefined\` type which means that a function returns a value \`undefined\`. + So "nothing" cannot be mixed with any other types. + If you need this - use \`undefined\` type instead.`, + hasFix: false, + optionsDescription: "Not configurable.", + options: null, + optionExamples: [true], + type: "maintainability", + typescriptOnly: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "void is not a valid type other than return types"; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +const failedKinds = new Set([ + ts.SyntaxKind.PropertySignature, + ts.SyntaxKind.PropertyDeclaration, + + ts.SyntaxKind.VariableDeclaration, + ts.SyntaxKind.TypeAliasDeclaration, + + ts.SyntaxKind.IntersectionType, + ts.SyntaxKind.UnionType, + + ts.SyntaxKind.Parameter, + ts.SyntaxKind.TypeParameter, + + ts.SyntaxKind.AsExpression, + ts.SyntaxKind.TypeAssertionExpression, + + ts.SyntaxKind.TypeOperator, + ts.SyntaxKind.ArrayType, + + ts.SyntaxKind.MappedType, + ts.SyntaxKind.ConditionalType, + + ts.SyntaxKind.TypeReference, + + ts.SyntaxKind.NewExpression, + ts.SyntaxKind.CallExpression, +]); + +function walk(ctx: Lint.WalkContext): void { + ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node) { + if (node.kind === ts.SyntaxKind.VoidKeyword && failedKinds.has(node.parent.kind)) { + ctx.addFailureAtNode(node, Rule.FAILURE_STRING); + } + + ts.forEachChild(node, cb); + }); +} diff --git a/test/rules/invalid-void/test.ts.lint b/test/rules/invalid-void/test.ts.lint new file mode 100644 index 00000000000..27c065770b1 --- /dev/null +++ b/test/rules/invalid-void/test.ts.lint @@ -0,0 +1,121 @@ +function func(): void {} + +type NormalType = () => void; + +let normalArrow = (): void => { } + +let ughThisThing = void 0; + +function takeThing(thing: undefined) { } +takeThing(void 0); + +function takeVoid(thing: void) { } + ~~~~ [0] + + +const arrowGeneric = (arg: T) => { } + ~~~~ [0] +#if typescript >= 2.3.0 +const arrowGeneric1 = (arg: T) => { } + ~~~~ [0] +const arrowGeneric2 = (arg: T) => { } + ~~~~ [0] + ~~~~ [0] +#endif + +#if typescript >= 2.3.0 +function functionGeneric(arg: T) {} + ~~~~ [0] + +function functionGeneric1(arg: T) {} + ~~~~ [0] +function functionGeneric2(arg: T) {} + ~~~~ [0] + ~~~~ [0] +#endif + +declare function functionDeclaration(arg: T): void; + ~~~~ [0] +#if typescript >= 2.3.0 +declare function functionDeclaration1(arg: T): void; + ~~~~ [0] +declare function functionDeclaration2(arg: T): void; + ~~~~ [0] + ~~~~ [0] +#endif + + +functionGeneric(undefined); + ~~~~ [0] + +declare function voidArray(args: void[]): void[]; + ~~~~ [0] + ~~~~ [0] + +let value = undefined as void; + ~~~~ [0] + +let value = undefined; + ~~~~ [0] + +function takesThings(...things: void[]): void { } + ~~~~ [0] + +type KeyofVoid = keyof void; + ~~~~[0] + +interface Interface { + lambda: () => void; + voidProp: void; + ~~~~ [0] +} + +class ClassName { + private readonly propName: void; + ~~~~ [0] +} + +let invalidMap: Map = new Map(); + ~~~~ [0] + ~~~~ [0] + +let letVoid: void; + ~~~~ [0] + +type VoidType = void; + ~~~~ [0] + +class OtherClassName { + private propName: VoidType; +} + +type UnionType = string | number; +type UnionType2 = string | number | void; + ~~~~ [0] + +type UnionType3 = string | (number & any | (string | void)); + ~~~~ [0] + +type IntersectionType = string & number & void; + ~~~~ [0] + +#if typescript >= 2.8.0 +type MappedType = { + [K in keyof T]: void; + ~~~~ [0] +} +type ConditionalType = { + [K in keyof T]: T[K] extends string ? void : string; + ~~~~ [0] +} +#endif + +#if typescript >= 3.4.0 +type ManyVoid = readonly void[]; + ~~~~ [0] + +function foo(arr: readonly void[]) { } + ~~~~ [0] +#endif + +[0]: void is not a valid type other than return types diff --git a/test/rules/invalid-void/tslint.json b/test/rules/invalid-void/tslint.json new file mode 100644 index 00000000000..79da7276673 --- /dev/null +++ b/test/rules/invalid-void/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "invalid-void": true + } +}