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

Add "no-null-undefined-union" rule. #4589

Merged
merged 6 commits into from Mar 27, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/configs/all.ts
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/configuration.ts
Expand Up @@ -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();
Expand Down Expand Up @@ -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<IOptions> {
Expand Down Expand Up @@ -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
Expand Down
105 changes: 105 additions & 0 deletions 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: "True if the rule should be enabled.",
nrathi marked this conversation as resolved.
Show resolved Hide resolved
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<void>, 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 {
// NOTE: This is a comprehensive intersection between `HasType` and has property `name`.
nrathi marked this conversation as resolved.
Show resolved Hide resolved
// 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;
}
2 changes: 1 addition & 1 deletion src/rules/noUnusedVariableRule.ts
Expand Up @@ -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);
Expand Down
55 changes: 55 additions & 0 deletions 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<number | null | undefined> {} // 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]
nrathi marked this conversation as resolved.
Show resolved Hide resolved

[0]: Union type cannot include both 'null' and 'undefined'.
19 changes: 19 additions & 0 deletions 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"
}
}
5 changes: 5 additions & 0 deletions test/rules/no-null-undefined-union/tslint.json
@@ -0,0 +1,5 @@
{
"rules": {
"no-null-undefined-union": true
}
}