From c95f015b7db93b8d4ba7e117d3ba0a95c792980f Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Tue, 17 Nov 2020 23:30:48 +0900 Subject: [PATCH 01/20] New: `no-unsafe-optional-chaining` rule (fixes #13431) --- docs/rules/no-unsafe-optional-chaining.md | 63 +++ lib/rules/index.js | 1 + lib/rules/no-unsafe-optional-chaining.js | 131 ++++++ .../lib/rules/no-unsafe-optional-chaining.js | 390 ++++++++++++++++++ tools/rule-types.json | 1 + 5 files changed, 586 insertions(+) create mode 100644 docs/rules/no-unsafe-optional-chaining.md create mode 100644 lib/rules/no-unsafe-optional-chaining.js create mode 100644 tests/lib/rules/no-unsafe-optional-chaining.js diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md new file mode 100644 index 00000000000..f3d622c561d --- /dev/null +++ b/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; +``` diff --git a/lib/rules/index.js b/lib/rules/index.js index 3cf26e51bc8..bace6558354 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -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"), diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js new file mode 100644 index 00000000000..fcbc21c5088 --- /dev/null +++ b/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); + } + } + }; + } +}; diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js new file mode 100644 index 00000000000..e198ad63acf --- /dev/null +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -0,0 +1,390 @@ +/** + * @fileoverview Tests for no-unsafe-optional-chaining rule. + * @author Yeon JuAn + */ + +"use strict"; + +const rule = require("../../../lib/rules/no-unsafe-optional-chaining"); + +const { RuleTester } = require("../../../lib/rule-tester"); + +const parserOptions = { + ecmaVersion: 2021, + sourceType: "module" +}; + +const ruleTester = new RuleTester({ parserOptions }); + +ruleTester.run("no-unsafe-optional-chaining", rule, { + valid: [ + "obj?.foo();", + "obj?.foo?.();", + "(obj?.foo ?? bar)();", + "(obj?.foo)?.()", + "(obj.foo)?.();", + "obj?.foo.bar;", + "obj?.foo?.bar;", + "(obj?.foo)?.bar;", + "(obj?.foo ?? bar).baz;", + "(obj?.foo ?? val)`template`", + "new (obj?.foo ?? val)()", + "obj?.foo?.()();", + "const {foo} = obj?.baz || {};", + "bar(...obj?.foo ?? []);", + + "var bar = {...foo?.bar};", + + // The default value option disallowArithmeticOperators is false + "obj?.foo - bar;", + "obj?.foo + bar;", + "obj?.foo * bar;", + "obj?.foo / bar;", + "obj?.foo % bar;", + "obj?.foo ** bar;", + + { + code: "(obj?.foo || baz) + bar;", + options: [{ + disallowArithmeticOperators: true + }] + }, + { + code: "(obj?.foo ?? baz) + bar;", + options: [{ + disallowArithmeticOperators: true + }] + }, + { + code: "bar += obj?.foo ?? val", + options: [{ + disallowArithmeticOperators: true + }] + } + ], + + invalid: [ + { + code: "(obj?.foo)();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "(obj?.foo?.())();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "(obj?.foo).bar", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "(obj?.foo)`template`", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "new (obj?.foo)();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 6 + } + ] + }, + { + code: "new (obj?.foo?.())()", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 6 + } + ] + }, + { + code: "[...obj?.foo];", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 5 + } + ] + }, + { + code: "bar(...obj?.foo);", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 8 + } + ] + }, + { + code: "new Bar(...obj?.foo);", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 12 + } + ] + }, + { + code: "const {foo} = obj?.bar;", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 15 + } + ] + }, + { + code: "const {foo} = obj?.bar();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 15 + } + ] + }, + { + code: "const [foo] = obj?.bar;", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 15 + } + ] + }, + { + code: "const [foo] = obj?.bar?.();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 15 + } + ] + }, + { + code: "class A extends obj?.foo {}", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 17 + } + ] + }, + { + code: "obj?.foo + bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 1 + } + ] + }, + { + code: "obj?.foo - bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 1 + } + ] + }, + { + code: "obj?.foo * bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 1 + } + ] + }, + { + code: "obj?.foo / bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 1 + } + ] + }, + { + code: "obj?.foo % bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 1 + } + ] + }, + { + code: "obj?.foo ** bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 1 + } + ] + }, + { + code: "+obj?.foo;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "-obj?.foo;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "bar += obj?.foo;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 8 + } + ] + }, + { + code: "bar -= obj?.foo;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 8 + } + ] + }, + { + code: "bar %= obj?.foo;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 8 + } + ] + }, + { + code: "bar **= obj?.foo;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 9 + } + ] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 84700de70d0..158e09a7d49 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -204,6 +204,7 @@ "no-unreachable-loop": "problem", "no-unsafe-finally": "problem", "no-unsafe-negation": "problem", + "no-unsafe-optional-chaining": "problem", "no-unused-expressions": "suggestion", "no-unused-labels": "suggestion", "no-unused-vars": "problem", From 97b2a103d921f4c1ba769ba95dbb3f7c74a9f929 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 18 Nov 2020 00:14:56 +0900 Subject: [PATCH 02/20] add checking 'in' --- docs/rules/no-unsafe-optional-chaining.md | 2 ++ lib/rules/no-unsafe-optional-chaining.js | 10 +++--- .../lib/rules/no-unsafe-optional-chaining.js | 35 ++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index f3d622c561d..eea583510b3 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -22,6 +22,8 @@ new (obj?.foo)(); [...obj?.foo]; bar(...obj?.foo); + +1 in obj?.foo; ``` Examples of **correct** code for this rule: diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index fcbc21c5088..a64f2de475a 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -34,7 +34,7 @@ function isDestructuringPattern(node) { * @param {ASTNode} chainExp a ChainExpression node. * @returns {boolean} `true` if it can be a runtime error, otherwise `false` */ -function isPossiblyMakeRuntimeError(chainExp) { +function isPossiblyError(chainExp) { const parent = chainExp.parent; switch (parent.type) { @@ -53,6 +53,8 @@ function isPossiblyMakeRuntimeError(chainExp) { return isDestructuringPattern(parent.left) && parent.right === chainExp; case "SpreadElement": return parent.parent.type !== "ObjectExpression"; + case "BinaryExpression": + return parent.operator === "in" && parent.right === chainExp; default: return false; } @@ -80,8 +82,8 @@ module.exports = { }], fixable: null, messages: { - unsafeOptionalChain: "Unsafe usage of {{node}}.", - unsafeArithmetic: "Unsafe arithmetic operation on {{node}}. It can result in NaN." + unsafeOptionalChain: "Unsafe usage of optional chaining. If it short-circuits with 'undefined' the evaluation will throw TypeError.", + unsafeArithmetic: "Unsafe arithmetic operation on optional chaining. It can result in NaN." } }, @@ -122,7 +124,7 @@ module.exports = { ) { reportUnsafeArithmetic(node); } - if (isPossiblyMakeRuntimeError(node)) { + if (isPossiblyError(node)) { reportUnsafeOptionalChain(node); } } diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index e198ad63acf..cd998b016fc 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -32,8 +32,8 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "obj?.foo?.()();", "const {foo} = obj?.baz || {};", "bar(...obj?.foo ?? []);", - "var bar = {...foo?.bar};", + "foo?.bar in {};", // The default value option disallowArithmeticOperators is false "obj?.foo - bar;", @@ -97,6 +97,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "(obj?.foo)[1];", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, { code: "(obj?.foo)`template`", errors: [ @@ -163,6 +174,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "1 in foo?.bar;", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 6 + } + ] + }, { code: "const {foo} = obj?.bar;", errors: [ @@ -196,6 +218,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "([foo] = obj?.bar);", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 10 + } + ] + }, { code: "const [foo] = obj?.bar?.();", errors: [ From 8778363f5834934b5ce9fc472d6ed34f5fd2198e Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 18 Nov 2020 00:28:01 +0900 Subject: [PATCH 03/20] fix type & add example --- docs/rules/no-unsafe-optional-chaining.md | 7 ++++++- lib/rules/no-unsafe-optional-chaining.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index eea583510b3..19aa2553109 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -1,6 +1,11 @@ # 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. +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 result. For example: + +```js +var obj = {}; +(obj?.foo)(); // TypeError: obj?.foo is not a function +``` ## Rule Details diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index a64f2de475a..7d3f93cb761 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -62,7 +62,7 @@ function isPossiblyError(chainExp) { module.exports = { meta: { - type: "suggestion", + type: "problem", docs: { description: "disallow using unsafe-optional-chaining.", From d6bcfc09f1e62f480584318d0476d14b1e44b072 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 18 Nov 2020 20:25:26 +0900 Subject: [PATCH 04/20] add check --- lib/rules/no-unsafe-optional-chaining.js | 42 +++----- .../lib/rules/no-unsafe-optional-chaining.js | 96 +++++++++++++++++-- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index 7d3f93cb761..43d6b4ccf3c 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -6,6 +6,7 @@ "use strict"; const ARITHMETIC_OPERATORS = ["+", "-", "/", "*", "%", "**", "+=", "-=", "/=", "*=", "%=", "**="]; +const UNSAFE_RELATIONAL_OPERATORS = ["in", "instanceof"]; /** * Checks whether a node is an arithmetic expression or not @@ -50,11 +51,16 @@ function isPossiblyError(chainExp) { case "VariableDeclarator": return isDestructuringPattern(parent.id) && parent.init === chainExp; case "AssignmentExpression": + case "AssignmentPattern": return isDestructuringPattern(parent.left) && parent.right === chainExp; case "SpreadElement": return parent.parent.type !== "ObjectExpression"; case "BinaryExpression": - return parent.operator === "in" && parent.right === chainExp; + return UNSAFE_RELATIONAL_OPERATORS.includes(parent.operator) && parent.right === chainExp; + case "ForOfStatement": + return parent.right === chainExp; + case "WithStatement": + return true; default: return false; } @@ -91,30 +97,6 @@ module.exports = { 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 ( @@ -122,10 +104,16 @@ module.exports = { node.parent && isArithmeticExpression(node.parent) ) { - reportUnsafeArithmetic(node); + context.report({ + messageId: "unsafeArithmetic", + node + }); } if (isPossiblyError(node)) { - reportUnsafeOptionalChain(node); + context.report({ + messageId: "unsafeOptionalChain", + node + }); } } }; diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index cd998b016fc..787a59a5dfd 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -34,6 +34,7 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "bar(...obj?.foo ?? []);", "var bar = {...foo?.bar};", "foo?.bar in {};", + "[foo = obj?.bar] = [];", // The default value option disallowArithmeticOperators is false "obj?.foo - bar;", @@ -175,18 +176,18 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { ] }, { - code: "1 in foo?.bar;", + code: "const {foo} = obj?.bar;", errors: [ { messageId: "unsafeOptionalChain", type: "ChainExpression", line: 1, - column: 6 + column: 15 } ] }, { - code: "const {foo} = obj?.bar;", + code: "const {foo} = obj?.bar();", errors: [ { messageId: "unsafeOptionalChain", @@ -197,7 +198,7 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { ] }, { - code: "const {foo} = obj?.bar();", + code: "const [foo] = obj?.bar;", errors: [ { messageId: "unsafeOptionalChain", @@ -208,7 +209,18 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { ] }, { - code: "const [foo] = obj?.bar;", + code: "([foo] = obj?.bar);", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 10 + } + ] + }, + { + code: "const [foo] = obj?.bar?.();", errors: [ { messageId: "unsafeOptionalChain", @@ -219,24 +231,35 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { ] }, { - code: "([foo] = obj?.bar);", + code: "[{ foo } = obj?.bar] = [];", errors: [ { messageId: "unsafeOptionalChain", type: "ChainExpression", line: 1, - column: 10 + column: 12 } ] }, { - code: "const [foo] = obj?.bar?.();", + code: "({bar: [ foo ] = obj?.prop} = {});", errors: [ { messageId: "unsafeOptionalChain", type: "ChainExpression", line: 1, - column: 15 + column: 18 + } + ] + }, + { + code: "[[ foo ] = obj?.bar] = [];", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 12 } ] }, @@ -251,6 +274,61 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + + // unsafe relational operations + { + code: "foo instanceof obj?.prop", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 16 + } + ] + }, + { + code: "1 in foo?.bar;", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 6 + } + ] + }, + + // unsafe `for...of` + { + code: "for (foo of obj?.bar);", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 13 + } + ] + }, + + // unsafe `with` + { + code: "with (obj?.foo) {};", + parserOptions: { + sourceType: "script" + }, + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 7 + } + ] + }, + + // unsafe arithmetic operations { code: "obj?.foo + bar;", options: [{ From bf6386486f6932dcee7262df3716a823f41bc7a3 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 18 Nov 2020 20:38:25 +0900 Subject: [PATCH 05/20] add examples --- docs/rules/no-unsafe-optional-chaining.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 19aa2553109..9a002cd6a26 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -29,6 +29,14 @@ new (obj?.foo)(); bar(...obj?.foo); 1 in obj?.foo; + +bar instanceof obj?.foo; + +for (bar of obj?.foo); + +[{ bar } = obj?.foo] = []; + +with (obj?.foo); ``` Examples of **correct** code for this rule: From d8f5c2159d7c9a4b234fba71fa7b633e3b218999 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 19 Nov 2020 00:36:55 +0900 Subject: [PATCH 06/20] make it handle logical expression --- docs/rules/no-unsafe-optional-chaining.md | 4 + lib/rules/no-unsafe-optional-chaining.js | 188 ++++++++++++------ .../lib/rules/no-unsafe-optional-chaining.js | 139 +++++++++++++ 3 files changed, 265 insertions(+), 66 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 9a002cd6a26..23723d3c6fc 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -18,6 +18,8 @@ Examples of **incorrect** code for this rule: (obj?.foo)(); +(obj?.foo ?? obj?.bar)(); + (obj?.foo).bar; (obj?.foo)`template`; @@ -46,6 +48,8 @@ Examples of **correct** code for this rule: (obj?.foo)?.(); +(obj?.foo ?? bar).(); + obj?.foo?.bar; (obj?.foo ?? bar)`template`; diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index 43d6b4ccf3c..bd2acb11983 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -5,65 +5,17 @@ "use strict"; -const ARITHMETIC_OPERATORS = ["+", "-", "/", "*", "%", "**", "+=", "-=", "/=", "*=", "%=", "**="]; +const UNSAFE_ARITHMETIC_OPERATORS = ["+", "-", "/", "*", "%", "**"]; +const UNSAFE_ASSIGNMENT_OPERATORS = ["+=", "-=", "/=", "*=", "%=", "**="]; const UNSAFE_RELATIONAL_OPERATORS = ["in", "instanceof"]; -/** - * 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 isPossiblyError(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": - case "AssignmentPattern": - return isDestructuringPattern(parent.left) && parent.right === chainExp; - case "SpreadElement": - return parent.parent.type !== "ObjectExpression"; - case "BinaryExpression": - return UNSAFE_RELATIONAL_OPERATORS.includes(parent.operator) && parent.right === chainExp; - case "ForOfStatement": - return parent.right === chainExp; - case "WithStatement": - return true; - default: - return false; - } + return node && node.type === "ObjectPattern" || node.type === "ArrayPattern"; } module.exports = { @@ -97,23 +49,127 @@ module.exports = { const options = context.options[0] || {}; const disallowArithmeticOperators = (options.disallowArithmeticOperators) || false; + /** + * Reports unsafe usafe of optional chaining + * @param {ASTNode} node node to report + * @returns {void} + */ + function reportUnsafeUsage(node) { + context.report({ + messageId: "unsafeOptionalChain", + node + }); + } + + /** + * Reports unsage arithmetic operation on optional chaining + * @param {ASTNode} node node to report + * @returns {void} + */ + function reportUnsafeArithmetic(node) { + context.report({ + messageId: "unsafeArithmetic", + node + }); + } + + /** + * Checks and reports if a node can short-circuit with `undefined` by optional chaining. + * @param {ASTNode} node node to check + * @param {Function} reportFunc report function + * @returns {void} + */ + function checkUndefinedShourtCircuit(node, reportFunc) { + if (!node) { + return; + } + if (node.type === "LogicalExpression") { + if (node.operator === "||" || node.operator === "??") { + checkUndefinedShourtCircuit(node.right, reportFunc); + } else if (node.operator === "&&") { + checkUndefinedShourtCircuit(node.left, reportFunc); + checkUndefinedShourtCircuit(node.right, reportFunc); + } + } else if (node.type === "ChainExpression") { + reportFunc(node); + } + } + + /** + * Checks unsafe usage of optional chaining + * @param {ASTNode} node node to check + * @returns {void} + */ + function checkUnsafeUsage(node) { + checkUndefinedShourtCircuit(node, reportUnsafeUsage); + } + + /** + * Checks unsafe arithmetic operaions on optional chaining + * @param {ASTNode} node node to check + * @returns {void} + */ + function checkUnsafeArithmetic(node) { + if (disallowArithmeticOperators) { + checkUndefinedShourtCircuit(node, reportUnsafeArithmetic); + } + } + return { - ChainExpression(node) { - if ( - disallowArithmeticOperators && - node.parent && - isArithmeticExpression(node.parent) - ) { - context.report({ - messageId: "unsafeArithmetic", - node - }); + "CallExpression, NewExpression"(node) { + if (node.parent.type !== "ChainExpression") { + checkUnsafeUsage(node.callee); } - if (isPossiblyError(node)) { - context.report({ - messageId: "unsafeOptionalChain", - node - }); + }, + "AssignmentExpression, AssignmentPattern"(node) { + if (isDestructuringPattern(node.left)) { + checkUnsafeUsage(node.right); + } + }, + VariableDeclarator(node) { + if (isDestructuringPattern(node.id)) { + checkUnsafeUsage(node.init); + } + }, + MemberExpression(node) { + if (node.parent && node.parent.type !== "ChainExpression") { + checkUnsafeUsage(node.object); + } + }, + TaggedTemplateExpression(node) { + checkUnsafeUsage(node.tag); + }, + ClassDeclaration(node) { + checkUnsafeUsage(node.superClass); + }, + ForOfStatement(node) { + checkUnsafeUsage(node.right); + }, + SpreadElement(node) { + if (node.parent && node.parent.type !== "ObjectExpression") { + checkUnsafeUsage(node.argument); + } + }, + BinaryExpression(node) { + if (UNSAFE_RELATIONAL_OPERATORS.includes(node.operator)) { + checkUnsafeUsage(node.right); + } + if (UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator)) { + checkUnsafeArithmetic(node.right); + checkUnsafeArithmetic(node.left); + } + }, + WithStatement(node) { + checkUnsafeUsage(node.object); + }, + UnaryExpression(node) { + if (UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator)) { + checkUnsafeArithmetic(node.argument); + } + }, + AssignmentExpression(node) { + if (UNSAFE_ASSIGNMENT_OPERATORS.includes(node.operator)) { + checkUnsafeArithmetic(node.right); } } }; diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index 787a59a5dfd..8b0823f7fcf 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -18,10 +18,15 @@ const ruleTester = new RuleTester({ parserOptions }); ruleTester.run("no-unsafe-optional-chaining", rule, { valid: [ + "var foo;", + "class Foo {}", + "!!obj?.foo", + "obj?.foo();", "obj?.foo?.();", "(obj?.foo ?? bar)();", "(obj?.foo)?.()", + "(obj?.foo ?? bar?.baz)?.()", "(obj.foo)?.();", "obj?.foo.bar;", "obj?.foo?.bar;", @@ -36,6 +41,12 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "foo?.bar in {};", "[foo = obj?.bar] = [];", + // logical operations + "(obj?.foo ?? bar?.baz ?? qux)();", + "((obj?.foo ?? bar?.baz) || qux)();", + "((obj?.foo || bar?.baz) || qux)();", + "((obj?.foo && bar?.baz) || qux)();", + // The default value option disallowArithmeticOperators is false "obj?.foo - bar;", "obj?.foo + bar;", @@ -76,6 +87,50 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "(obj?.foo ?? bar?.baz)();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 14 + } + ] + }, + { + code: "(obj?.foo || bar?.baz)();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 14 + } + ] + }, + { + code: "(obj?.foo && bar)();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + } + ] + }, + { + code: "(bar && obj?.foo)();", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 9 + } + ] + }, { code: "(obj?.foo?.())();", errors: [ @@ -98,6 +153,23 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "(obj?.foo && obj?.baz).bar", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 2 + }, + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 14 + } + ] + }, { code: "(obj?.foo)[1];", errors: [ @@ -142,6 +214,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "new (obj?.foo?.() || obj?.bar)()", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 22 + } + ] + }, { code: "[...obj?.foo];", errors: [ @@ -153,6 +236,23 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "[...(obj?.foo && obj?.bar)];", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 6 + }, + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 18 + } + ] + }, { code: "bar(...obj?.foo);", errors: [ @@ -208,6 +308,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "const [foo] = obj?.bar || obj?.foo;", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 27 + } + ] + }, { code: "([foo] = obj?.bar);", errors: [ @@ -343,6 +454,34 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "(foo || obj?.foo) + bar;", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 9 + } + ] + }, + { + code: "bar + (foo || obj?.foo);", + options: [{ + disallowArithmeticOperators: true + }], + errors: [ + { + messageId: "unsafeArithmetic", + type: "ChainExpression", + line: 1, + column: 15 + } + ] + }, { code: "obj?.foo - bar;", options: [{ From 5fe64b830f8797b4fae873409df8f76edc22f7c8 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 19 Nov 2020 00:53:56 +0900 Subject: [PATCH 07/20] fix logical error --- lib/rules/no-unsafe-optional-chaining.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index bd2acb11983..e6d94613607 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -15,7 +15,7 @@ const UNSAFE_RELATIONAL_OPERATORS = ["in", "instanceof"]; * @returns {boolean} `true` if a node is a destructuring pattern, otherwise `false` */ function isDestructuringPattern(node) { - return node && node.type === "ObjectPattern" || node.type === "ArrayPattern"; + return node && (node.type === "ObjectPattern" || node.type === "ArrayPattern"); } module.exports = { From f40c36f3760663f20680e4b6c9bad8bc88507984 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 19 Nov 2020 02:51:20 +0900 Subject: [PATCH 08/20] fix typo --- lib/rules/no-unsafe-optional-chaining.js | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index e6d94613607..2330d7a411d 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -79,16 +79,16 @@ module.exports = { * @param {Function} reportFunc report function * @returns {void} */ - function checkUndefinedShourtCircuit(node, reportFunc) { + function checkUndefinedShortCircuit(node, reportFunc) { if (!node) { return; } if (node.type === "LogicalExpression") { if (node.operator === "||" || node.operator === "??") { - checkUndefinedShourtCircuit(node.right, reportFunc); + checkUndefinedShortCircuit(node.right, reportFunc); } else if (node.operator === "&&") { - checkUndefinedShourtCircuit(node.left, reportFunc); - checkUndefinedShourtCircuit(node.right, reportFunc); + checkUndefinedShortCircuit(node.left, reportFunc); + checkUndefinedShortCircuit(node.right, reportFunc); } } else if (node.type === "ChainExpression") { reportFunc(node); @@ -101,7 +101,7 @@ module.exports = { * @returns {void} */ function checkUnsafeUsage(node) { - checkUndefinedShourtCircuit(node, reportUnsafeUsage); + checkUndefinedShortCircuit(node, reportUnsafeUsage); } /** @@ -110,9 +110,7 @@ module.exports = { * @returns {void} */ function checkUnsafeArithmetic(node) { - if (disallowArithmeticOperators) { - checkUndefinedShourtCircuit(node, reportUnsafeArithmetic); - } + checkUndefinedShortCircuit(node, reportUnsafeArithmetic); } return { @@ -154,7 +152,10 @@ module.exports = { if (UNSAFE_RELATIONAL_OPERATORS.includes(node.operator)) { checkUnsafeUsage(node.right); } - if (UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator)) { + if ( + disallowArithmeticOperators && + UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator) + ) { checkUnsafeArithmetic(node.right); checkUnsafeArithmetic(node.left); } @@ -163,12 +164,18 @@ module.exports = { checkUnsafeUsage(node.object); }, UnaryExpression(node) { - if (UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator)) { + if ( + disallowArithmeticOperators && + UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator) + ) { checkUnsafeArithmetic(node.argument); } }, AssignmentExpression(node) { - if (UNSAFE_ASSIGNMENT_OPERATORS.includes(node.operator)) { + if ( + disallowArithmeticOperators && + UNSAFE_ASSIGNMENT_OPERATORS.includes(node.operator) + ) { checkUnsafeArithmetic(node.right); } } From bb2949a3c93fe0f672c013326e3c2bdde2e3c427 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 19 Nov 2020 02:52:10 +0900 Subject: [PATCH 09/20] fix typo --- lib/rules/no-unsafe-optional-chaining.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index 2330d7a411d..0e537436697 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -23,7 +23,7 @@ module.exports = { type: "problem", docs: { - description: "disallow using unsafe-optional-chaining.", + description: "disallow using unsafe optional chaining.", category: "Possible Errors", recommended: false, url: "https://eslint.org/docs/rules/no-unsafe-optional-chaining" From fd10bb8979c1598d0b3837419047462787c0677f Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Sat, 21 Nov 2020 17:09:12 +0900 Subject: [PATCH 10/20] fix tests format and false positive --- lib/rules/no-unsafe-optional-chaining.js | 21 +- .../lib/rules/no-unsafe-optional-chaining.js | 599 ++---------------- 2 files changed, 81 insertions(+), 539 deletions(-) diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index 0e537436697..e8f32c2a56d 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -114,32 +114,35 @@ module.exports = { } return { - "CallExpression, NewExpression"(node) { - if (node.parent.type !== "ChainExpression") { - checkUnsafeUsage(node.callee); - } - }, "AssignmentExpression, AssignmentPattern"(node) { if (isDestructuringPattern(node.left)) { checkUnsafeUsage(node.right); } }, + "ClassDeclaration, ClassExpression"(node) { + checkUnsafeUsage(node.superClass); + }, + CallExpression(node) { + if (!node.optional) { + checkUnsafeUsage(node.callee); + } + }, + NewExpression(node) { + checkUnsafeUsage(node.callee); + }, VariableDeclarator(node) { if (isDestructuringPattern(node.id)) { checkUnsafeUsage(node.init); } }, MemberExpression(node) { - if (node.parent && node.parent.type !== "ChainExpression") { + if (!node.optional) { checkUnsafeUsage(node.object); } }, TaggedTemplateExpression(node) { checkUnsafeUsage(node.tag); }, - ClassDeclaration(node) { - checkUnsafeUsage(node.superClass); - }, ForOfStatement(node) { checkUnsafeUsage(node.right); }, diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index 8b0823f7fcf..aed24a33ead 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -21,7 +21,6 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "var foo;", "class Foo {}", "!!obj?.foo", - "obj?.foo();", "obj?.foo?.();", "(obj?.foo ?? bar)();", @@ -31,9 +30,12 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "obj?.foo.bar;", "obj?.foo?.bar;", "(obj?.foo)?.bar;", + "(obj?.foo)?.bar.baz;", + "(obj?.foo)?.().bar", "(obj?.foo ?? bar).baz;", "(obj?.foo ?? val)`template`", "new (obj?.foo ?? val)()", + "new bar();", "obj?.foo?.()();", "const {foo} = obj?.baz || {};", "bar(...obj?.foo ?? []);", @@ -76,83 +78,52 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { ], invalid: [ - { - code: "(obj?.foo)();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "(obj?.foo ?? bar?.baz)();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 14 - } - ] - }, - { - code: "(obj?.foo || bar?.baz)();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 14 - } - ] - }, - { - code: "(obj?.foo && bar)();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "(bar && obj?.foo)();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 9 - } - ] - }, - { - code: "(obj?.foo?.())();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "(obj?.foo).bar", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, + ...[ + "(obj?.foo)();", + "(obj?.foo ?? bar?.baz)();", + "(obj?.foo || bar?.baz)();", + "(obj?.foo && bar)();", + "(bar && obj?.foo)();", + "(obj?.foo?.())();", + "(obj?.foo).bar", + "(obj?.foo)[1];", + "(obj?.foo)`template`", + "new (obj?.foo)();", + "new (obj?.foo?.())()", + "new (obj?.foo?.() || obj?.bar)()", + + // spread + "[...obj?.foo];", + "bar(...obj?.foo);", + "new Bar(...obj?.foo);", + + // destructuring + "const {foo} = obj?.bar;", + "const {foo} = obj?.bar();", + "const [foo] = obj?.bar;", + "const [foo] = obj?.bar || obj?.foo;", + "([foo] = obj?.bar);", + "const [foo] = obj?.bar?.();", + "[{ foo } = obj?.bar] = [];", + "({bar: [ foo ] = obj?.prop} = {});", + "[[ foo ] = obj?.bar] = [];", + + // class declaration + "class A extends obj?.foo {}", + + // class expression + "var a = class A extends obj?.foo {}", + + // relational operations + "foo instanceof obj?.prop", + "1 in foo?.bar;", + + // for...of + "for (foo of obj?.bar);" + ].map(code => ({ + code, + errors: [{ messageId: "unsafeOptionalChain", type: "ChainExpression" }] + })), { code: "(obj?.foo && obj?.baz).bar", errors: [ @@ -170,260 +141,6 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, - { - code: "(obj?.foo)[1];", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "(obj?.foo)`template`", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "new (obj?.foo)();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 6 - } - ] - }, - { - code: "new (obj?.foo?.())()", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 6 - } - ] - }, - { - code: "new (obj?.foo?.() || obj?.bar)()", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 22 - } - ] - }, - { - code: "[...obj?.foo];", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 5 - } - ] - }, - { - code: "[...(obj?.foo && obj?.bar)];", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 6 - }, - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 18 - } - ] - }, - { - code: "bar(...obj?.foo);", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 8 - } - ] - }, - { - code: "new Bar(...obj?.foo);", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 12 - } - ] - }, - { - code: "const {foo} = obj?.bar;", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 15 - } - ] - }, - { - code: "const {foo} = obj?.bar();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 15 - } - ] - }, - { - code: "const [foo] = obj?.bar;", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 15 - } - ] - }, - { - code: "const [foo] = obj?.bar || obj?.foo;", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 27 - } - ] - }, - { - code: "([foo] = obj?.bar);", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 10 - } - ] - }, - { - code: "const [foo] = obj?.bar?.();", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 15 - } - ] - }, - { - code: "[{ foo } = obj?.bar] = [];", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 12 - } - ] - }, - { - code: "({bar: [ foo ] = obj?.prop} = {});", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 18 - } - ] - }, - { - code: "[[ foo ] = obj?.bar] = [];", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 12 - } - ] - }, - { - code: "class A extends obj?.foo {}", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 17 - } - ] - }, - - // unsafe relational operations - { - code: "foo instanceof obj?.prop", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 16 - } - ] - }, - { - code: "1 in foo?.bar;", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 6 - } - ] - }, - - // unsafe `for...of` - { - code: "for (foo of obj?.bar);", - errors: [ - { - messageId: "unsafeOptionalChain", - type: "ChainExpression", - line: 1, - column: 13 - } - ] - }, - - // unsafe `with` { code: "with (obj?.foo) {};", parserOptions: { @@ -438,203 +155,25 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, - - // unsafe arithmetic operations - { - code: "obj?.foo + bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 1 - } - ] - }, - { - code: "(foo || obj?.foo) + bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 9 - } - ] - }, - { - code: "bar + (foo || obj?.foo);", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 15 - } - ] - }, - { - code: "obj?.foo - bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 1 - } - ] - }, - { - code: "obj?.foo * bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 1 - } - ] - }, - { - code: "obj?.foo / bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 1 - } - ] - }, - { - code: "obj?.foo % bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 1 - } - ] - }, - { - code: "obj?.foo ** bar;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 1 - } - ] - }, - { - code: "+obj?.foo;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "-obj?.foo;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 2 - } - ] - }, - { - code: "bar += obj?.foo;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 8 - } - ] - }, - { - code: "bar -= obj?.foo;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 8 - } - ] - }, - { - code: "bar %= obj?.foo;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 8 - } - ] - }, - { - code: "bar **= obj?.foo;", - options: [{ - disallowArithmeticOperators: true - }], - errors: [ - { - messageId: "unsafeArithmetic", - type: "ChainExpression", - line: 1, - column: 9 - } - ] - } + ...[ + "obj?.foo + bar;", + "(foo || obj?.foo) + bar;", + "bar + (foo || obj?.foo);", + "obj?.foo - bar;", + "obj?.foo * bar;", + "obj?.foo / bar;", + "obj?.foo % bar;", + "obj?.foo ** bar;", + "+obj?.foo;", + "-obj?.foo;", + "bar += obj?.foo;", + "bar -= obj?.foo;", + "bar %= obj?.foo;", + "bar **= obj?.foo;" + ].map(code => ({ + code, + options: [{ disallowArithmeticOperators: true }], + errors: [{ messageId: "unsafeArithmetic", type: "ChainExpression" }] + })) ] }); From 1c21aff450336e37b38cc64b7fbf5fc8df23312e Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Sun, 29 Nov 2020 20:49:40 +0900 Subject: [PATCH 11/20] fix review - add test cases (*=, /=) - improve Rule Details doc - fix wrong example - add unary, assignment operation test cases - edit test cases - move optional chain to right side - change to use Set - fix jsdoc typo and type - add examples on docs - handle conditional, sequence expressions - remove useless check - fix doc - add test cases - fix typo --- docs/rules/no-unsafe-optional-chaining.md | 58 +++++++++++++++--- lib/rules/no-unsafe-optional-chaining.js | 57 +++++++++++------- .../lib/rules/no-unsafe-optional-chaining.js | 60 +++++++++++++++++-- 3 files changed, 141 insertions(+), 34 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 23723d3c6fc..6e3aae09631 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -1,15 +1,28 @@ # 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 result. For example: +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. For example: ```js -var obj = {}; +var obj = undefined; + +1 in obj?.foo; // TypeError +with (obj?.foo); // TypeError +for (bar of obj?.foo); // TypeError +bar instanceof obj?.foo; // TypeError +const { bar } = obj?.foo; // TypeError +``` + +Also, the parentheses around optional chaining can cause unexpected TypeError because it stop short-circuiting. For example: + +```js +var obj = undefined; + (obj?.foo)(); // TypeError: obj?.foo is not a function ``` ## Rule Details -This rule disallows some cases that might be an TypeError. +This rule aims to detect some cases where the use of optional chaining doesn't prevent runtime errors. In particular, it flags optional chains in positions where short-circuiting to `undefined` causes throwing a TypeError afterward. Examples of **incorrect** code for this rule: @@ -20,8 +33,12 @@ Examples of **incorrect** code for this rule: (obj?.foo ?? obj?.bar)(); +(foo ? obj?.foo : bar)(); + (obj?.foo).bar; +(foo, obj?.bar).baz; + (obj?.foo)`template`; new (obj?.foo)(); @@ -36,9 +53,15 @@ bar instanceof obj?.foo; for (bar of obj?.foo); +const { bar } = obj?.foo; + [{ bar } = obj?.foo] = []; with (obj?.foo); + +class A extends obj?.foo {}; + +var a = class A extends obj?.foo {} ``` Examples of **correct** code for this rule: @@ -48,7 +71,7 @@ Examples of **correct** code for this rule: (obj?.foo)?.(); -(obj?.foo ?? bar).(); +(obj?.foo ?? bar)(); obj?.foo?.bar; @@ -56,27 +79,44 @@ obj?.foo?.bar; new (obj?.foo ?? bar)(); -var baz = {...obj.?foo}; +var baz = {...obj?.foo}; + +const { bar } = obj?.foo || baz; ``` ## 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`: 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 +With this option set to `true` the rule is enforced for: + +- Unary operators: `-`, `+` +- Arithmetic operators: `+`, `-`, `/`, `*`, `%`, `**` +- Assignment operators: `+=`, `-=`, `/=`, `*=`, `%=`, `**=` + 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; +-obj?.foo; +obj?.foo + bar; +obj?.foo - bar; +obj?.foo / bar; obj?.foo * bar; - -+obj?.foo; +obj?.foo % bar; +obj?.foo ** bar; baz += obj?.foo; +baz -= obj?.foo; +baz /= obj?.foo; +baz *= obj?.foo; +baz %= obj?.foo; +baz **= obj?.foo; ``` diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index e8f32c2a56d..f92f612bc4e 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -5,9 +5,9 @@ "use strict"; -const UNSAFE_ARITHMETIC_OPERATORS = ["+", "-", "/", "*", "%", "**"]; -const UNSAFE_ASSIGNMENT_OPERATORS = ["+=", "-=", "/=", "*=", "%=", "**="]; -const UNSAFE_RELATIONAL_OPERATORS = ["in", "instanceof"]; +const UNSAFE_ARITHMETIC_OPERATORS = new Set(["+", "-", "/", "*", "%", "**"]); +const UNSAFE_ASSIGNMENT_OPERATORS = new Set(["+=", "-=", "/=", "*=", "%=", "**="]); +const UNSAFE_RELATIONAL_OPERATORS = new Set(["in", "instanceof"]); /** * Checks whether a node is a destructuring pattern or not @@ -15,7 +15,7 @@ const UNSAFE_RELATIONAL_OPERATORS = ["in", "instanceof"]; * @returns {boolean} `true` if a node is a destructuring pattern, otherwise `false` */ function isDestructuringPattern(node) { - return node && (node.type === "ObjectPattern" || node.type === "ArrayPattern"); + return node.type === "ObjectPattern" || node.type === "ArrayPattern"; } module.exports = { @@ -23,7 +23,7 @@ module.exports = { type: "problem", docs: { - description: "disallow using unsafe optional chaining.", + description: "disallow use of optional chaining in contexts where the `undefined` value is not allowed", category: "Possible Errors", recommended: false, url: "https://eslint.org/docs/rules/no-unsafe-optional-chaining" @@ -50,7 +50,7 @@ module.exports = { const disallowArithmeticOperators = (options.disallowArithmeticOperators) || false; /** - * Reports unsafe usafe of optional chaining + * Reports unsafe usage of optional chaining * @param {ASTNode} node node to report * @returns {void} */ @@ -62,7 +62,7 @@ module.exports = { } /** - * Reports unsage arithmetic operation on optional chaining + * Reports unsafe arithmetic operation on optional chaining * @param {ASTNode} node node to report * @returns {void} */ @@ -75,7 +75,7 @@ module.exports = { /** * Checks and reports if a node can short-circuit with `undefined` by optional chaining. - * @param {ASTNode} node node to check + * @param {ASTNode} [node] node to check * @param {Function} reportFunc report function * @returns {void} */ @@ -83,15 +83,30 @@ module.exports = { if (!node) { return; } - if (node.type === "LogicalExpression") { - if (node.operator === "||" || node.operator === "??") { - checkUndefinedShortCircuit(node.right, reportFunc); - } else if (node.operator === "&&") { - checkUndefinedShortCircuit(node.left, reportFunc); - checkUndefinedShortCircuit(node.right, reportFunc); - } - } else if (node.type === "ChainExpression") { - reportFunc(node); + switch (node.type) { + case "LogicalExpression": + if (node.operator === "||" || node.operator === "??") { + checkUndefinedShortCircuit(node.right, reportFunc); + } else if (node.operator === "&&") { + checkUndefinedShortCircuit(node.left, reportFunc); + checkUndefinedShortCircuit(node.right, reportFunc); + } + break; + case "SequenceExpression": + checkUndefinedShortCircuit( + node.expressions[node.expressions.length - 1], + reportFunc + ); + break; + case "ConditionalExpression": + checkUndefinedShortCircuit(node.consequent, reportFunc); + checkUndefinedShortCircuit(node.alternate, reportFunc); + break; + case "ChainExpression": + reportFunc(node); + break; + default: + break; } } @@ -152,12 +167,12 @@ module.exports = { } }, BinaryExpression(node) { - if (UNSAFE_RELATIONAL_OPERATORS.includes(node.operator)) { + if (UNSAFE_RELATIONAL_OPERATORS.has(node.operator)) { checkUnsafeUsage(node.right); } if ( disallowArithmeticOperators && - UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator) + UNSAFE_ARITHMETIC_OPERATORS.has(node.operator) ) { checkUnsafeArithmetic(node.right); checkUnsafeArithmetic(node.left); @@ -169,7 +184,7 @@ module.exports = { UnaryExpression(node) { if ( disallowArithmeticOperators && - UNSAFE_ARITHMETIC_OPERATORS.includes(node.operator) + UNSAFE_ARITHMETIC_OPERATORS.has(node.operator) ) { checkUnsafeArithmetic(node.argument); } @@ -177,7 +192,7 @@ module.exports = { AssignmentExpression(node) { if ( disallowArithmeticOperators && - UNSAFE_ASSIGNMENT_OPERATORS.includes(node.operator) + UNSAFE_ASSIGNMENT_OPERATORS.has(node.operator) ) { checkUnsafeArithmetic(node.right); } diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index aed24a33ead..7e26357aa43 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -42,6 +42,8 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "var bar = {...foo?.bar};", "foo?.bar in {};", "[foo = obj?.bar] = [];", + "(foo?.bar, bar)();", + "(foo?.bar ? baz : qux)();", // logical operations "(obj?.foo ?? bar?.baz ?? qux)();", @@ -56,6 +58,14 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "obj?.foo / bar;", "obj?.foo % bar;", "obj?.foo ** bar;", + "+obj?.foo;", + "-obj?.foo;", + "bar += obj?.foo;", + "bar -= obj?.foo;", + "bar %= obj?.foo;", + "bar **= obj?.foo;", + "bar *= obj?.boo", + "bar /= obj?.boo", { code: "(obj?.foo || baz) + bar;", @@ -80,8 +90,8 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { invalid: [ ...[ "(obj?.foo)();", - "(obj?.foo ?? bar?.baz)();", - "(obj?.foo || bar?.baz)();", + "(obj.foo ?? bar?.baz)();", + "(obj.foo || bar?.baz)();", "(obj?.foo && bar)();", "(bar && obj?.foo)();", "(obj?.foo?.())();", @@ -119,7 +129,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "1 in foo?.bar;", // for...of - "for (foo of obj?.bar);" + "for (foo of obj?.bar);", + + // sequence expression + "(foo, obj?.foo)();", + "(foo, obj?.foo)[1];", + + // conditional expression + "(a ? obj?.foo : b)();", + "(a ? b : obj?.foo)();", + "(a ? obj?.foo : b)[1];", + "(a ? b : obj?.foo).bar;" ].map(code => ({ code, errors: [{ messageId: "unsafeOptionalChain", type: "ChainExpression" }] @@ -155,10 +175,30 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "(foo ? obj?.foo : obj?.bar).bar", + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 8 + }, + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 19 + } + ] + }, ...[ "obj?.foo + bar;", "(foo || obj?.foo) + bar;", "bar + (foo || obj?.foo);", + "(a ? obj?.foo : b) + bar", + "(a ? b : obj?.foo) + bar", + "(foo, bar, baz?.qux) + bar", "obj?.foo - bar;", "obj?.foo * bar;", "obj?.foo / bar;", @@ -166,10 +206,22 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "obj?.foo ** bar;", "+obj?.foo;", "-obj?.foo;", + "+(foo ?? obj?.foo);", + "+(foo || obj?.bar);", + "+(obj?.bar && foo);", + "+(foo ? obj?.foo : bar);", + "+(foo ? bar : obj?.foo);", "bar += obj?.foo;", "bar -= obj?.foo;", "bar %= obj?.foo;", - "bar **= obj?.foo;" + "bar **= obj?.foo;", + "bar *= obj?.boo", + "bar /= obj?.boo", + "bar += (foo ?? obj?.foo);", + "bar += (foo || obj?.foo);", + "bar += (foo && obj?.foo);", + "bar += (foo ? obj?.foo : bar);", + "bar += (foo ? bar : obj?.foo);" ].map(code => ({ code, options: [{ disallowArithmeticOperators: true }], From ab742d07dcb0a7d029574aa601046e40240d78e2 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Mon, 30 Nov 2020 19:41:27 +0900 Subject: [PATCH 12/20] fix title --- docs/rules/no-unsafe-optional-chaining.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 6e3aae09631..70f90039903 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -1,4 +1,4 @@ -# disallow optional chaining that possibly errors (no-unsafe-optional-chaining) +# disallow use of optional chaining in contexts where the `undefined` value is not allowed (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. For example: From 2e962a0615ce1752927d7655a7b8d10e2f96f1c8 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Mon, 30 Nov 2020 21:17:24 +0900 Subject: [PATCH 13/20] edit description --- docs/rules/no-unsafe-optional-chaining.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 70f90039903..33d7ef57751 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -12,7 +12,7 @@ bar instanceof obj?.foo; // TypeError const { bar } = obj?.foo; // TypeError ``` -Also, the parentheses around optional chaining can cause unexpected TypeError because it stop short-circuiting. For example: +Also, the parentheses around optional chaining in where the `undefined` not allowed can cause TypeError because it stops short-circuiting. For example: ```js var obj = undefined; From f8700d28b5774deb591517bc3977dbebaafacb21 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Tue, 1 Dec 2020 12:21:21 +0900 Subject: [PATCH 14/20] fix docs --- docs/rules/no-unsafe-optional-chaining.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 33d7ef57751..0b65fdc18f1 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -1,6 +1,6 @@ # disallow use of optional chaining in contexts where the `undefined` value is not allowed (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. For example: +The optional chaining (`?.`) expression can short-circuit with a return value of `undefined`. Therefore, treating an evaluated optional chaining expression as a function, object, number, etc., can cause TypeError or unexpected results. For example: ```js var obj = undefined; @@ -12,7 +12,7 @@ bar instanceof obj?.foo; // TypeError const { bar } = obj?.foo; // TypeError ``` -Also, the parentheses around optional chaining in where the `undefined` not allowed can cause TypeError because it stops short-circuiting. For example: +Also, parentheses limit the scope of short-circuiting in chains. For example: ```js var obj = undefined; @@ -22,7 +22,7 @@ var obj = undefined; ## Rule Details -This rule aims to detect some cases where the use of optional chaining doesn't prevent runtime errors. In particular, it flags optional chains in positions where short-circuiting to `undefined` causes throwing a TypeError afterward. +This rule aims to detect some cases where the use of optional chaining doesn't prevent runtime errors. In particular, it flags optional chaining expressions in positions where short-circuiting to `undefined` causes throwing a TypeError afterward. Examples of **incorrect** code for this rule: @@ -59,9 +59,9 @@ const { bar } = obj?.foo; with (obj?.foo); -class A extends obj?.foo {}; +class A extends obj?.foo {} -var a = class A extends obj?.foo {} +var a = class A extends obj?.foo {}; ``` Examples of **correct** code for this rule: @@ -70,10 +70,11 @@ Examples of **correct** code for this rule: /*eslint no-unsafe-optional-chaining: "error"*/ (obj?.foo)?.(); +obj?.foo(); (obj?.foo ?? bar)(); -obj?.foo?.bar; +obj?.foo.bar; (obj?.foo ?? bar)`template`; @@ -88,7 +89,7 @@ const { bar } = obj?.foo || baz; 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`: Disallow arithmetic operations on optional chaining expressions (Default `false`). If this is `true`, this rule warns arithmetic operations on optional chaining expressions, which possibly result in `NaN`. ### disallowArithmeticOperators From 022a5cd14ad01162182035cdb1cd07f86bef722e Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Tue, 1 Dec 2020 12:22:13 +0900 Subject: [PATCH 15/20] fix review - add test cases - fix docs --- docs/rules/no-unsafe-optional-chaining.md | 1 + lib/rules/no-unsafe-optional-chaining.js | 2 +- .../lib/rules/no-unsafe-optional-chaining.js | 61 ++++++++++++++++--- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 0b65fdc18f1..4ba906f927a 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -70,6 +70,7 @@ Examples of **correct** code for this rule: /*eslint no-unsafe-optional-chaining: "error"*/ (obj?.foo)?.(); + obj?.foo(); (obj?.foo ?? bar)(); diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index f92f612bc4e..b685678e703 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -120,7 +120,7 @@ module.exports = { } /** - * Checks unsafe arithmetic operaions on optional chaining + * Checks unsafe arithmetic operations on optional chaining * @param {ASTNode} node node to check * @returns {void} */ diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index 7e26357aa43..8271c28d99f 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -38,10 +38,20 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "new bar();", "obj?.foo?.()();", "const {foo} = obj?.baz || {};", + "const foo = obj?.bar", + "foo = obj?.bar", + "foo.bar = obj?.bar", "bar(...obj?.foo ?? []);", "var bar = {...foo?.bar};", "foo?.bar in {};", + "foo?.bar < foo?.baz;", + "foo?.bar <= foo?.baz;", + "foo?.bar > foo?.baz;", + "foo?.bar >= foo?.baz;", "[foo = obj?.bar] = [];", + "[foo.bar = obj?.bar] = [];", + "({foo = obj?.bar} = obj);", + "({foo: obj.bar = obj?.baz} = obj);", "(foo?.bar, bar)();", "(foo?.bar ? baz : qux)();", @@ -66,23 +76,53 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "bar **= obj?.foo;", "bar *= obj?.boo", "bar /= obj?.boo", - - { - code: "(obj?.foo || baz) + bar;", + ...[ + "obj?.foo | bar", + "obj?.foo & bar", + "obj?.foo >> obj?.bar;", + "obj?.foo << obj?.bar;", + "obj?.foo >>> obj?.bar;", + "(obj?.foo || baz) + bar;", + "(obj?.foo ?? baz) + bar;", + "(obj?.foo ?? baz) - bar;", + "(obj?.foo ?? baz) * bar;", + "(obj?.foo ?? baz) / bar;", + "(obj?.foo ?? baz) % bar;", + "(obj?.foo ?? baz) ** bar;", + "void obj?.foo;", + "typeof obj?.foo;", + "!obj?.foo", + "~obj?.foo", + "+(obj?.foo ?? bar)", + "-(obj?.foo ?? bar)", + "bar |= obj?.foo;", + "bar &= obj?.foo;", + "bar ^= obj?.foo;", + "bar <<= obj?.foo;", + "bar >>= obj?.foo;", + "bar >>>= obj?.foo;", + "bar ||= obj?.foo", + "bar &&= obj?.foo", + "bar += (obj?.foo ?? baz);", + "bar -= (obj?.foo ?? baz)", + "bar *= (obj?.foo ?? baz)", + "bar /= (obj?.foo ?? baz)", + "bar %= (obj?.foo ?? baz);", + "bar **= (obj?.foo ?? baz)" + ].map(code => ({ + code, options: [{ disallowArithmeticOperators: true }] - }, + })), { - code: "(obj?.foo ?? baz) + bar;", - options: [{ - disallowArithmeticOperators: true - }] + code: "obj?.foo - bar;", + options: [{}] }, { - code: "bar += obj?.foo ?? val", + code: "obj?.foo - bar;", options: [{ - disallowArithmeticOperators: true + disallowArithmeticOperators: false }] } ], @@ -110,6 +150,7 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { // destructuring "const {foo} = obj?.bar;", "const {foo} = obj?.bar();", + "const {foo: bar} = obj?.bar();", "const [foo] = obj?.bar;", "const [foo] = obj?.bar || obj?.foo;", "([foo] = obj?.bar);", From 68b3c7a3fda63576c1e180af79aa7cee8e4d3878 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Tue, 1 Dec 2020 20:03:50 +0900 Subject: [PATCH 16/20] add example --- docs/rules/no-unsafe-optional-chaining.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 4ba906f927a..081ba4ebba2 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -37,6 +37,8 @@ Examples of **incorrect** code for this rule: (obj?.foo).bar; +(foo?.()).bar; + (foo, obj?.bar).baz; (obj?.foo)`template`; @@ -77,6 +79,8 @@ obj?.foo(); obj?.foo.bar; +foo?.()?.bar; + (obj?.foo ?? bar)`template`; new (obj?.foo ?? bar)(); From 650d13a9566a0b84a43613cc2cea89afb6b20309 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 2 Dec 2020 21:40:42 +0900 Subject: [PATCH 17/20] add example --- docs/rules/no-unsafe-optional-chaining.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 081ba4ebba2..5e6eafe444b 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -17,7 +17,8 @@ Also, parentheses limit the scope of short-circuiting in chains. For example: ```js var obj = undefined; -(obj?.foo)(); // TypeError: obj?.foo is not a function +(obj?.foo)(); // TypeError +(obj?.foo).bar; // TypeError ``` ## Rule Details @@ -31,13 +32,19 @@ Examples of **incorrect** code for this rule: (obj?.foo)(); +(obj?.foo).bar; + +(foo?.()).bar; + +(foo?.()).bar(); + (obj?.foo ?? obj?.bar)(); -(foo ? obj?.foo : bar)(); +(foo || obj?.foo)(); -(obj?.foo).bar; +(obj?.foo && foo)(); -(foo?.()).bar; +(foo ? obj?.foo : bar)(); (foo, obj?.bar).baz; From 2f8d2f1deb689722d2b7a8e742726c4c667343a3 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 3 Dec 2020 23:28:57 +0900 Subject: [PATCH 18/20] handle await --- docs/rules/no-unsafe-optional-chaining.md | 30 +++++ lib/rules/no-unsafe-optional-chaining.js | 3 + .../lib/rules/no-unsafe-optional-chaining.js | 107 +++++++++++++++++- 3 files changed, 137 insertions(+), 3 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 5e6eafe444b..2bcd659554c 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -71,6 +71,12 @@ with (obj?.foo); class A extends obj?.foo {} var a = class A extends obj?.foo {}; + +async function foo () { + (await obj?.foo)(); + (await obj?.foo).bar; + const { bar } = await obj?.foo; +} ``` Examples of **correct** code for this rule: @@ -95,6 +101,11 @@ new (obj?.foo ?? bar)(); var baz = {...obj?.foo}; const { bar } = obj?.foo || baz; + +async function foo () { + const { bar } = await obj?.foo || baz; + await (obj?.foo)?.(); +} ``` ## Options @@ -132,4 +143,23 @@ baz /= obj?.foo; baz *= obj?.foo; baz %= obj?.foo; baz **= obj?.foo; + +async function foo () { + +await obj?.foo; + -await obj?.foo; + + await obj?.foo + bar; + await obj?.foo - bar; + await obj?.foo / bar; + await obj?.foo * bar; + await obj?.foo % bar; + await obj?.foo ** bar; + + baz += await obj?.foo; + baz -= await obj?.foo; + baz /= await obj?.foo; + baz *= await obj?.foo; + baz %= await obj?.foo; + baz **=await obj?.foo; +} ``` diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index b685678e703..2eafc1ad8f1 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -102,6 +102,9 @@ module.exports = { checkUndefinedShortCircuit(node.consequent, reportFunc); checkUndefinedShortCircuit(node.alternate, reportFunc); break; + case "AwaitExpression": + checkUndefinedShortCircuit(node.argument, reportFunc); + break; case "ChainExpression": reportFunc(node); break; diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index 8271c28d99f..81dbc1d968e 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -54,6 +54,17 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "({foo: obj.bar = obj?.baz} = obj);", "(foo?.bar, bar)();", "(foo?.bar ? baz : qux)();", + ` + async function func() { + await obj?.foo(); + await obj?.foo?.(); + await bar?.baz; + await (foo ?? obj?.foo.baz); + (await bar?.baz ?? bar).baz; + (await bar?.baz ?? await bar).baz; + await (foo?.bar ? baz : qux); + } + `, // logical operations "(obj?.foo ?? bar?.baz ?? qux)();", @@ -76,6 +87,20 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "bar **= obj?.foo;", "bar *= obj?.boo", "bar /= obj?.boo", + `async function func() { + await obj?.foo + await obj?.bar; + await obj?.foo - await obj?.bar; + await obj?.foo * await obj?.bar; + +await obj?.foo; + -await obj?.foo; + bar += await obj?.foo; + bar -= await obj?.foo; + bar %= await obj?.foo; + bar **= await obj?.foo; + bar *= await obj?.boo; + bar /= await obj?.boo; + } + `, ...[ "obj?.foo | bar", "obj?.foo & bar", @@ -108,7 +133,34 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "bar *= (obj?.foo ?? baz)", "bar /= (obj?.foo ?? baz)", "bar %= (obj?.foo ?? baz);", - "bar **= (obj?.foo ?? baz)" + "bar **= (obj?.foo ?? baz)", + + `async function foo() { + (await obj?.foo || baz) + bar; + (await obj?.foo ?? baz) + bar; + (await obj?.foo ?? baz) - bar; + (await obj?.foo ?? baz) * bar; + (await obj?.foo ?? baz) / bar; + (await obj?.foo ?? baz) % bar; + "(await obj?.foo ?? baz) ** bar;", + "void await obj?.foo;", + "typeof await obj?.foo;", + "!await obj?.foo", + "~await obj?.foo", + "+(await obj?.foo ?? bar)", + "-(await obj?.foo ?? bar)", + bar |= await obj?.foo; + bar &= await obj?.foo; + bar ^= await obj?.foo; + bar <<= await obj?.foo; + bar >>= await obj?.foo; + bar >>>= await obj?.foo + bar += ((await obj?.foo) ?? baz); + bar -= ((await obj?.foo) ?? baz); + bar /= ((await obj?.foo) ?? baz); + bar %= ((await obj?.foo) ?? baz); + bar **= ((await obj?.foo) ?? baz); + }` ].map(code => ({ code, options: [{ @@ -142,6 +194,19 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "new (obj?.foo?.())()", "new (obj?.foo?.() || obj?.bar)()", + `async function foo() { + (await obj?.foo)(); + }`, + `async function foo() { + (await obj.foo ?? bar?.baz)(); + }`, + `async function foo() { + (await obj?.foo || bar?.baz)(); + }`, + `async function foo() { + (await (bar && obj?.foo))(); + }`, + // spread "[...obj?.foo];", "bar(...obj?.foo);", @@ -158,29 +223,48 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "[{ foo } = obj?.bar] = [];", "({bar: [ foo ] = obj?.prop} = {});", "[[ foo ] = obj?.bar] = [];", + "async function foo() { const {foo} = await obj?.bar; }", + "async function foo() { const {foo} = await obj?.bar(); }", + "async function foo() { const [foo] = await obj?.bar || await obj?.foo; }", + "async function foo() { ([foo] = await obj?.bar); }", // class declaration "class A extends obj?.foo {}", + "async function foo() { class A extends (await obj?.foo) {}}", // class expression "var a = class A extends obj?.foo {}", + "async function foo() { var a = class A extends (await obj?.foo) {}}", // relational operations "foo instanceof obj?.prop", + "async function foo() { foo instanceof await obj?.prop }", "1 in foo?.bar;", + "async function foo() { 1 in await foo?.bar; }", // for...of "for (foo of obj?.bar);", + "async function foo() { for (foo of await obj?.bar);}", // sequence expression "(foo, obj?.foo)();", "(foo, obj?.foo)[1];", + "async function foo() { (await (foo, obj?.foo))(); }", + "async function foo() { ((foo, await obj?.foo))(); }", + "async function foo() { (foo, await obj?.foo)[1]; }", + "async function foo() { (await (foo, obj?.foo)) [1]; }", // conditional expression "(a ? obj?.foo : b)();", "(a ? b : obj?.foo)();", "(a ? obj?.foo : b)[1];", - "(a ? b : obj?.foo).bar;" + "(a ? b : obj?.foo).bar;", + "async function foo() { (await (a ? obj?.foo : b))(); }", + "async function foo() { (a ? await obj?.foo : b)(); }", + "async function foo() { (await (a ? b : obj?.foo))(); }", + "async function foo() { (await (a ? obj?.foo : b))[1]; }", + "async function foo() { (await (a ? b : obj?.foo)).bar; }", + "async function foo() { (a ? b : await obj?.foo).bar; }" ].map(code => ({ code, errors: [{ messageId: "unsafeOptionalChain", type: "ChainExpression" }] @@ -216,6 +300,20 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { } ] }, + { + code: "async function foo() { with ( await obj?.foo) {}; }", + parserOptions: { + sourceType: "script" + }, + errors: [ + { + messageId: "unsafeOptionalChain", + type: "ChainExpression", + line: 1, + column: 37 + } + ] + }, { code: "(foo ? obj?.foo : obj?.bar).bar", errors: [ @@ -262,7 +360,10 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { "bar += (foo || obj?.foo);", "bar += (foo && obj?.foo);", "bar += (foo ? obj?.foo : bar);", - "bar += (foo ? bar : obj?.foo);" + "bar += (foo ? bar : obj?.foo);", + "async function foo() { await obj?.foo + bar; }", + "async function foo() { (foo || await obj?.foo) + bar;}", + "async function foo() { bar + (foo || await obj?.foo); }" ].map(code => ({ code, options: [{ disallowArithmeticOperators: true }], From 85a5a655aa036354c3f6f9d76212e2000341dbea Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Fri, 4 Dec 2020 01:36:59 +0900 Subject: [PATCH 19/20] add test cases, fix docs --- docs/rules/no-unsafe-optional-chaining.md | 7 ++++--- tests/lib/rules/no-unsafe-optional-chaining.js | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 2bcd659554c..609a4026477 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -73,9 +73,9 @@ class A extends obj?.foo {} var a = class A extends obj?.foo {}; async function foo () { + const { bar } = await obj?.foo; (await obj?.foo)(); (await obj?.foo).bar; - const { bar } = await obj?.foo; } ``` @@ -104,7 +104,8 @@ const { bar } = obj?.foo || baz; async function foo () { const { bar } = await obj?.foo || baz; - await (obj?.foo)?.(); + (await obj?.foo)?.(); + (await obj?.foo)?.bar; } ``` @@ -160,6 +161,6 @@ async function foo () { baz /= await obj?.foo; baz *= await obj?.foo; baz %= await obj?.foo; - baz **=await obj?.foo; + baz **= await obj?.foo; } ``` diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index 81dbc1d968e..94d2cc3a658 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -58,6 +58,8 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { async function func() { await obj?.foo(); await obj?.foo?.(); + (await obj?.foo)?.(); + (await obj?.foo)?.bar; await bar?.baz; await (foo ?? obj?.foo.baz); (await bar?.baz ?? bar).baz; From 68a479d1f1e6ef60f709ac1229e9a7563ade4426 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Fri, 4 Dec 2020 10:08:35 +0900 Subject: [PATCH 20/20] fix test cases, docs --- docs/rules/no-unsafe-optional-chaining.md | 13 ------------- tests/lib/rules/no-unsafe-optional-chaining.js | 7 +++++-- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/docs/rules/no-unsafe-optional-chaining.md b/docs/rules/no-unsafe-optional-chaining.md index 609a4026477..48296aac2be 100644 --- a/docs/rules/no-unsafe-optional-chaining.md +++ b/docs/rules/no-unsafe-optional-chaining.md @@ -147,20 +147,7 @@ baz **= obj?.foo; async function foo () { +await obj?.foo; - -await obj?.foo; - await obj?.foo + bar; - await obj?.foo - bar; - await obj?.foo / bar; - await obj?.foo * bar; - await obj?.foo % bar; - await obj?.foo ** bar; - baz += await obj?.foo; - baz -= await obj?.foo; - baz /= await obj?.foo; - baz *= await obj?.foo; - baz %= await obj?.foo; - baz **= await obj?.foo; } ``` diff --git a/tests/lib/rules/no-unsafe-optional-chaining.js b/tests/lib/rules/no-unsafe-optional-chaining.js index 94d2cc3a658..38a4f246230 100644 --- a/tests/lib/rules/no-unsafe-optional-chaining.js +++ b/tests/lib/rules/no-unsafe-optional-chaining.js @@ -200,10 +200,13 @@ ruleTester.run("no-unsafe-optional-chaining", rule, { (await obj?.foo)(); }`, `async function foo() { - (await obj.foo ?? bar?.baz)(); + (await obj?.foo).bar; }`, `async function foo() { - (await obj?.foo || bar?.baz)(); + (bar?.baz ?? await obj?.foo)(); + }`, + `async function foo() { + (bar && await obj?.foo)(); }`, `async function foo() { (await (bar && obj?.foo))();