diff --git a/src/configs/all.ts b/src/configs/all.ts index 3845aff90ea..fb98cc7f242 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -126,6 +126,7 @@ export const rules = { // "no-invalid-this": Won't this be deprecated? "no-misused-new": true, "no-null-keyword": true, + "no-null-undefined-union": true, "no-object-literal-type-assertion": true, "no-return-await": true, "no-shadowed-variable": true, diff --git a/src/configuration.ts b/src/configuration.ts index 486761c38b7..6639ab0f1ef 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -378,6 +378,7 @@ export function extendConfigurationFile( * * @deprecated use `path.resolve` instead */ +// tslint:disable-next-line no-null-undefined-union export function getRelativePath(directory?: string | null, relativeTo?: string) { if (directory != undefined) { const basePath = relativeTo !== undefined ? relativeTo : process.cwd(); @@ -428,6 +429,7 @@ export function getRulesDirectories( * @param ruleConfigValue The raw option setting of a rule */ function parseRuleOptions( + // tslint:disable-next-line no-null-undefined-union ruleConfigValue: RawRuleConfig, rawDefaultRuleSeverity: string | undefined, ): Partial { @@ -506,8 +508,11 @@ export interface RawConfigFile { jsRules?: RawRulesConfig | boolean; } export interface RawRulesConfig { + // tslint:disable-next-line no-null-undefined-union [key: string]: RawRuleConfig; } + +// tslint:disable-next-line no-null-undefined-union export type RawRuleConfig = | null | undefined diff --git a/src/rules/noNullUndefinedUnionRule.ts b/src/rules/noNullUndefinedUnionRule.ts new file mode 100644 index 00000000000..d543594b2c4 --- /dev/null +++ b/src/rules/noNullUndefinedUnionRule.ts @@ -0,0 +1,105 @@ +/** + * @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 { + isParameterDeclaration, + isPropertyDeclaration, + isPropertySignature, + isSignatureDeclaration, + isTypeAliasDeclaration, + isTypeReference, + isUnionType, + isVariableDeclaration, +} from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-null-undefined-union", + description: "Disallows union types with both `null` and `undefined` as members.", + rationale: Lint.Utils.dedent` + A union type that includes both \`null\` and \`undefined\` is either redundant or fragile. + Enforcing the choice between the two allows the \`triple-equals\` rule to exist without + exceptions, and is essentially a more flexible version of the \`no-null-keyword\` rule. + `, + optionsDescription: "Not configurable.", + options: null, + optionExamples: [true], + type: "functionality", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Union type cannot include both 'null' and 'undefined'."; + + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker()); + } +} + +function walk(ctx: Lint.WalkContext, tc: ts.TypeChecker): void { + return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { + const type = getType(node, tc); + if (type !== undefined && isNullUndefinedUnion(type)) { + ctx.addFailureAtNode(node, Rule.FAILURE_STRING); + } + return ts.forEachChild(node, cb); + }); +} + +function getType(node: ts.Node, tc: ts.TypeChecker): ts.Type | undefined { + // This is a comprehensive intersection between `HasType` and has property `name`. + // The node name kind must be identifier, or else this rule will throw errors while descending. + if ( + (isVariableDeclaration(node) || + isParameterDeclaration(node) || + isPropertySignature(node) || + isPropertyDeclaration(node) || + isTypeAliasDeclaration(node)) && + node.name.kind === ts.SyntaxKind.Identifier + ) { + return tc.getTypeAtLocation(node); + } else if (isSignatureDeclaration(node)) { + const signature = tc.getSignatureFromDeclaration(node); + return signature === undefined ? undefined : signature.getReturnType(); + } else { + return undefined; + } +} + +function isNullUndefinedUnion(type: ts.Type): boolean { + if (isTypeReference(type) && type.typeArguments !== undefined) { + return type.typeArguments.some(isNullUndefinedUnion); + } + + if (isUnionType(type)) { + let hasNull = false; + let hasUndefined = false; + for (const subType of type.types) { + hasNull = hasNull || subType.getFlags() === ts.TypeFlags.Null; + hasUndefined = hasUndefined || subType.getFlags() === ts.TypeFlags.Undefined; + if (hasNull && hasUndefined) { + return true; + } + } + } + return false; +} diff --git a/src/rules/noUnusedVariableRule.ts b/src/rules/noUnusedVariableRule.ts index 75f222a250b..e77f8745431 100644 --- a/src/rules/noUnusedVariableRule.ts +++ b/src/rules/noUnusedVariableRule.ts @@ -94,7 +94,7 @@ function parseOptions(options: any[]): Options { let ignorePattern: RegExp | undefined; for (const o of options) { if (typeof o === "object") { - // tslint:disable-next-line no-unsafe-any + // tslint:disable-next-line no-unsafe-any no-null-undefined-union const ignore = o[OPTION_IGNORE_PATTERN] as string | null | undefined; if (ignore != undefined) { ignorePattern = new RegExp(ignore); diff --git a/test/rules/no-null-undefined-union/test.ts.lint b/test/rules/no-null-undefined-union/test.ts.lint new file mode 100644 index 00000000000..27604370ea3 --- /dev/null +++ b/test/rules/no-null-undefined-union/test.ts.lint @@ -0,0 +1,55 @@ +[typescript]: >= 2.4.0 + +interface someInterface { + a: number | undefined | null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + b: boolean; +} + +const c: string | null | undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +export type SomeType = +~~~~~~~~~~~~~~~~~~~~~~ + | null +~~~~~~~~~~ + | undefined +~~~~~~~~~~~~~~~ + | boolean; +~~~~~~~~~~~~~~ [0] + +const someFunc = (): string | undefined | null => {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +const someFunc = (foo: null | string | undefined, bar: boolean) => {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +function someFunc(): number | undefined | null {} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +function someFunc(): Promise {} // error +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +function someFunc(bar: boolean, foo: null | number | undefined) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +function someFunc() { +~~~~~~~~~~~~~~~~~~~~~ + const somePredicate = (): boolean => true; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + const someFunc = (): string | null => null; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + let foo; +~~~~~~~~~~~~ + if (somePredicate()) { +~~~~~~~~~~~~~~~~~~~~~~~~~~ + foo = someFunc(); +~~~~~~~~~~~~~~~~~~~~~~~~~ + } +~~~~~ + return foo; +~~~~~~~~~~~~~~~ +} +~ [0] + +[0]: Union type cannot include both 'null' and 'undefined'. diff --git a/test/rules/no-null-undefined-union/tsconfig.json b/test/rules/no-null-undefined-union/tsconfig.json new file mode 100644 index 00000000000..d81c1202adb --- /dev/null +++ b/test/rules/no-null-undefined-union/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "downlevelIteration": true, + "experimentalDecorators": true, + "importHelpers": true, + "jsx": "react", + "lib": [ + "es5", + "es2015", + "es2016", + "es2017" + ], + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "stripInternal": true, + "target": "es5" + } +} diff --git a/test/rules/no-null-undefined-union/tslint.json b/test/rules/no-null-undefined-union/tslint.json new file mode 100644 index 00000000000..f41c08c7b72 --- /dev/null +++ b/test/rules/no-null-undefined-union/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-null-undefined-union": true + } +}