Skip to content

Commit

Permalink
Add no-typeof-undefined rule (#1966)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fisker and sindresorhus committed Nov 20, 2022
1 parent e8c5156 commit d7f7341
Show file tree
Hide file tree
Showing 9 changed files with 743 additions and 2 deletions.
1 change: 1 addition & 0 deletions configs/recommended.js
Expand Up @@ -52,6 +52,7 @@ module.exports = {
'unicorn/no-static-only-class': 'error',
'unicorn/no-thenable': 'error',
'unicorn/no-this-assignment': 'error',
'unicorn/no-typeof-undefined': 'error',
'unicorn/no-unnecessary-await': 'error',
'unicorn/no-unreadable-array-destructuring': 'error',
'unicorn/no-unreadable-iife': 'error',
Expand Down
59 changes: 59 additions & 0 deletions docs/rules/no-typeof-undefined.md
@@ -0,0 +1,59 @@
# Disallow comparing `undefined` using `typeof`

✅ This rule is enabled in the `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs).

🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

Checking if a value is `undefined` by using `typeof value === 'undefined'` is needlessly verbose. It's generally better to compare against `undefined` directly. The only time `typeof` is needed is when a global variable potentially does not exists, in which case, using `globalThis.value === undefined` may be better.

## Fail

```js
function foo(bar) {
if (typeof bar === 'undefined') {}
}
```

```js
import foo from './foo.js';

if (typeof foo.bar !== 'undefined') {}
```

## Pass

```js
function foo(bar) {
if (foo === undefined) {}
}
```

```js
import foo from './foo.js';

if (foo.bar !== undefined) {}
```

## Options

### checkGlobalVariables

Type: `boolean`\
Default: `false`

The rule ignores variables not defined in the file by default.

Set it to `true` to check all variables.

```js
// eslint unicorn/no-typeof-undefined: ["error", {"checkGlobalVariables": true}]
if (typeof undefinedVariable === 'undefined') {} // Fails
```

```js
// eslint unicorn/no-typeof-undefined: ["error", {"checkGlobalVariables": true}]
if (typeof Array === 'undefined') {} // Fails
```
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -91,6 +91,7 @@ Use a [preset config](#preset-configs) or configure each rules in `package.json`
| [no-static-only-class](docs/rules/no-static-only-class.md) | Disallow classes that only have static members. || 🔧 | |
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. || | |
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. || | |
| [no-typeof-undefined](docs/rules/no-typeof-undefined.md) | Disallow comparing `undefined` using `typeof`. || 🔧 | 💡 |
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. || 🔧 | |
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. || 🔧 | |
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. || | |
Expand Down
2 changes: 1 addition & 1 deletion rules/no-static-only-class.js
Expand Up @@ -49,7 +49,7 @@ function isStaticMember(node) {
if (
isDeclare
|| isReadonly
|| typeof accessibility !== 'undefined'
|| accessibility !== undefined
|| (Array.isArray(decorators) && decorators.length > 0)
// TODO: Remove this when we drop support for `@typescript-eslint/parser` v4
|| key.type === 'TSPrivateIdentifier'
Expand Down
140 changes: 140 additions & 0 deletions rules/no-typeof-undefined.js
@@ -0,0 +1,140 @@
'use strict';
const isShadowed = require('./utils/is-shadowed.js');
const {matches} = require('./selectors/index.js');
const {
addParenthesizesToReturnOrThrowExpression,
} = require('./fix/index.js');
const {removeSpacesAfter} = require('./fix/index.js');
const isOnSameLine = require('./utils/is-on-same-line.js');
const needsSemicolon = require('./utils/needs-semicolon.js');
const {
isParenthesized,
} = require('./utils/parentheses.js');

const MESSAGE_ID_ERROR = 'no-typeof-undefined/error';
const MESSAGE_ID_SUGGESTION = 'no-typeof-undefined/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Compare with `undefined` directly instead of using `typeof`.',
[MESSAGE_ID_SUGGESTION]: 'Switch to `… {{operator}} undefined`.',
};

const selector = [
'BinaryExpression',
matches(['===', '!==', '==', '!='].map(operator => `[operator="${operator}"]`)),
'[left.type="UnaryExpression"]',
'[left.operator="typeof"]',
'[left.prefix]',
'[right.type="Literal"]',
].join('');

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {
checkGlobalVariables,
} = {
checkGlobalVariables: false,
...context.options[0],
};

return {
[selector](binaryExpression) {
const {left: typeofNode, right: undefinedString, operator} = binaryExpression;
if (undefinedString.value !== 'undefined') {
return;
}

const valueNode = typeofNode.argument;
const isGlobalVariable = valueNode.type === 'Identifier'
&& !isShadowed(context.getScope(), valueNode);

if (!checkGlobalVariables && isGlobalVariable) {
return;
}

const sourceCode = context.getSourceCode();
const [typeofToken, secondToken] = sourceCode.getFirstTokens(typeofNode, 2);

const fix = function * (fixer) {
// Change `==`/`!=` to `===`/`!==`
if (operator === '==' || operator === '!=') {
const operatorToken = sourceCode.getTokenAfter(
typeofNode,
token => token.type === 'Punctuator' && token.value === operator,
);

yield fixer.insertTextAfter(operatorToken, '=');
}

yield fixer.replaceText(undefinedString, 'undefined');

yield fixer.remove(typeofToken);
yield removeSpacesAfter(typeofToken, sourceCode, fixer);

const {parent} = binaryExpression;
if (
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
&& parent.argument === binaryExpression
&& !isOnSameLine(typeofToken, secondToken)
&& !isParenthesized(binaryExpression, sourceCode)
&& !isParenthesized(typeofNode, sourceCode)
) {
yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode);
return;
}

const tokenBefore = sourceCode.getTokenBefore(binaryExpression);
if (needsSemicolon(tokenBefore, sourceCode, secondToken.value)) {
yield fixer.insertTextBefore(binaryExpression, ';');
}
};

const problem = {
node: binaryExpression,
loc: typeofToken.loc,
messageId: MESSAGE_ID_ERROR,
};

if (isGlobalVariable) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
data: {operator: operator.startsWith('!') ? '!==' : '==='},
fix,
},
];
} else {
problem.fix = fix;
}

return problem;
},
};
};

const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
checkGlobalVariables: {
type: 'boolean',
default: false,
},
},
},
];

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow comparing `undefined` using `typeof`.',
},
fixable: 'code',
hasSuggestions: true,
schema,
messages,
},
};
2 changes: 1 addition & 1 deletion rules/utils/is-same-reference.js
Expand Up @@ -145,7 +145,7 @@ function isSameReference(left, right) {
const nameA = getStaticPropertyName(left);

// `x.y = x["y"]`
if (typeof nameA !== 'undefined') {
if (nameA !== undefined) {
return (
isSameReference(left.object, right.object)
&& nameA === getStaticPropertyName(right)
Expand Down
90 changes: 90 additions & 0 deletions test/no-typeof-undefined.mjs
@@ -0,0 +1,90 @@
import outdent from 'outdent';
import {getTester} from './utils/test.mjs';

const {test} = getTester(import.meta);

test.snapshot({
valid: [
'typeof a.b',
'typeof a.b > "undefined"',
'a.b === "undefined"',
'void a.b === "undefined"',
'+a.b === "undefined"',
'++a.b === "undefined"',
'a.b++ === "undefined"',
'foo === undefined',
'typeof a.b === "string"',
'typeof foo === "undefined"',
'foo = 2; typeof foo === "undefined"',
'/* globals foo: readonly */ typeof foo === "undefined"',
'/* globals globalThis: readonly */ typeof globalThis === "undefined"',
// Cases we are not checking
'"undefined" === typeof a.b',
'const UNDEFINED = "undefined"; typeof a.b === UNDEFINED',
'typeof a.b === `undefined`',
],
invalid: [
'typeof a.b === "undefined"',
'typeof a.b !== "undefined"',
'typeof a.b == "undefined"',
'typeof a.b != "undefined"',
'typeof a.b == \'undefined\'',
'let foo; typeof foo === "undefined"',
'const foo = 1; typeof foo === "undefined"',
'var foo; typeof foo === "undefined"',
'var foo; var foo; typeof foo === "undefined"',
'for (const foo of bar) typeof foo === "undefined";',
outdent`
let foo;
function bar() {
typeof foo === "undefined";
}
`,
'function foo() {typeof foo === "undefined"}',
'function foo(bar) {typeof bar === "undefined"}',
'function foo({bar}) {typeof bar === "undefined"}',
'function foo([bar]) {typeof bar === "undefined"}',
'typeof foo.bar === "undefined"',
outdent`
import foo from 'foo';
typeof foo.bar === "undefined"
`,
// ASI
outdent`
foo
typeof [] === "undefined";
`,
outdent`
foo
typeof (a ? b : c) === "undefined";
`,
outdent`
function a() {
return typeof // comment
a.b === 'undefined';
}
`,
outdent`
function a() {
return (typeof // ReturnStatement argument is parenthesized
a.b === 'undefined');
}
`,
outdent`
function a() {
return (typeof // UnaryExpression is parenthesized
a.b) === 'undefined';
}
`,
],
});

// `checkGlobalVariables: true`
test.snapshot({
valid: [
],
invalid: [
'typeof undefinedVariableIdentifier === "undefined"',
'typeof Array !== "undefined"',
].map(code => ({code, options: [{checkGlobalVariables: true}]})),
});

0 comments on commit d7f7341

Please sign in to comment.