Skip to content

Commit

Permalink
New: no-unsafe-optional-chaining rule (fixes #13431)
Browse files Browse the repository at this point in the history
  • Loading branch information
yeonjuan committed Nov 17, 2020
1 parent 37a06d6 commit c95f015
Show file tree
Hide file tree
Showing 5 changed files with 586 additions and 0 deletions.
63 changes: 63 additions & 0 deletions docs/rules/no-unsafe-optional-chaining.md
@@ -0,0 +1,63 @@
# disallow optional chaining that possibly errors (no-unsafe-optional-chaining)

The optional chaining(`?.`) expression can short-circuit with `undefined`. Therefore, treating an evaluated optional chaining expression as a function, object, number, etc., can cause TypeError or unexpected results.

## Rule Details

This rule disallows some cases that might be an TypeError.

Examples of **incorrect** code for this rule:

```js
/*eslint no-unsafe-optional-chaining: "error"*/

(obj?.foo)();

(obj?.foo).bar;

(obj?.foo)`template`;

new (obj?.foo)();

[...obj?.foo];

bar(...obj?.foo);
```
Examples of **correct** code for this rule:
```js
/*eslint no-unsafe-optional-chaining: "error"*/

(obj?.foo)?.();

obj?.foo?.bar;

(obj?.foo ?? bar)`template`;

new (obj?.foo ?? bar)();

var baz = {...obj.?foo};
```
## Options
This rule has an object option:
- `disallowArithmeticOperators`: Disallow arithmetic operation on optional chaining expression (Default `false`). If this is `true`, this rule warns arithmetic operations on optional chaining expression which possibly result in `NaN`.
### disallowArithmeticOperators
Examples of additional **incorrect** code for this rule with the `{ "disallowArithmeticOperators": true }` option:
```js
/*eslint no-unsafe-optional-chaining: ["error", { "disallowArithmeticOperators": true }]*/

obj?.foo + bar;

obj?.foo * bar;

+obj?.foo;

baz += obj?.foo;
```
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -217,6 +217,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
"no-unreachable-loop": () => require("./no-unreachable-loop"),
"no-unsafe-finally": () => require("./no-unsafe-finally"),
"no-unsafe-negation": () => require("./no-unsafe-negation"),
"no-unsafe-optional-chaining": () => require("./no-unsafe-optional-chaining"),
"no-unused-expressions": () => require("./no-unused-expressions"),
"no-unused-labels": () => require("./no-unused-labels"),
"no-unused-vars": () => require("./no-unused-vars"),
Expand Down
131 changes: 131 additions & 0 deletions lib/rules/no-unsafe-optional-chaining.js
@@ -0,0 +1,131 @@
/**
* @fileoverview Rule to disallow unsafe optional chaining
* @author Yeon JuAn
*/

"use strict";

const ARITHMETIC_OPERATORS = ["+", "-", "/", "*", "%", "**", "+=", "-=", "/=", "*=", "%=", "**="];

/**
* Checks whether a node is an arithmetic expression or not
* @param {ASTNode} node node to check
* @returns {boolean} `true` if a node is an arithmetic expression, otherwise `false`
*/
function isArithmeticExpression(node) {
return (
node.type === "BinaryExpression" ||
node.type === "UnaryExpression" ||
node.type === "AssignmentExpression"
) && ARITHMETIC_OPERATORS.includes(node.operator);
}

/**
* Checks whether a node is a destructuring pattern or not
* @param {ASTNode} node node to check
* @returns {boolean} `true` if a node is a destructuring pattern, otherwise `false`
*/
function isDestructuringPattern(node) {
return node.type === "ObjectPattern" || node.type === "ArrayPattern";
}

/**
* Checks whether a ChainExpression make an runtime error or not
* @param {ASTNode} chainExp a ChainExpression node.
* @returns {boolean} `true` if it can be a runtime error, otherwise `false`
*/
function isPossiblyMakeRuntimeError(chainExp) {
const parent = chainExp.parent;

switch (parent.type) {
case "CallExpression":
case "NewExpression":
return parent.callee === chainExp && parent.parent.type !== "ChainExpression";
case "MemberExpression":
return parent.object === chainExp && parent.parent.type !== "ChainExpression";
case "TaggedTemplateExpression":
return parent.tag === chainExp;
case "ClassDeclaration":
return parent.superClass === chainExp;
case "VariableDeclarator":
return isDestructuringPattern(parent.id) && parent.init === chainExp;
case "AssignmentExpression":
return isDestructuringPattern(parent.left) && parent.right === chainExp;
case "SpreadElement":
return parent.parent.type !== "ObjectExpression";
default:
return false;
}
}

module.exports = {
meta: {
type: "suggestion",

docs: {
description: "disallow using unsafe-optional-chaining.",
category: "Possible Errors",
recommended: false,
url: "https://eslint.org/docs/rules/no-unsafe-optional-chaining"
},
schema: [{
type: "object",
properties: {
disallowArithmeticOperators: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],
fixable: null,
messages: {
unsafeOptionalChain: "Unsafe usage of {{node}}.",
unsafeArithmetic: "Unsafe arithmetic operation on {{node}}. It can result in NaN."
}
},

create(context) {
const options = context.options[0] || {};
const disallowArithmeticOperators = (options.disallowArithmeticOperators) || false;

/**
* Reports an error for unsafe optional chaining usage.
* @param {ASTNode} node node to report
* @returns {void}
*/
function reportUnsafeOptionalChain(node) {
context.report({
messageId: "unsafeOptionalChain",
node
});
}

/**
* Reports an error for unsafe arithmetic operations on optional chaining.
* @param {ASTNode} node node to report
* @returns {void}
*/
function reportUnsafeArithmetic(node) {
context.report({
messageId: "unsafeArithmetic",
node
});
}

return {
ChainExpression(node) {
if (
disallowArithmeticOperators &&
node.parent &&
isArithmeticExpression(node.parent)
) {
reportUnsafeArithmetic(node);
}
if (isPossiblyMakeRuntimeError(node)) {
reportUnsafeOptionalChain(node);
}
}
};
}
};

0 comments on commit c95f015

Please sign in to comment.