Repository: eslint/eslint (JavaScript)

Issue:
Do not fail on unrecognized logical operators
ESLint throws an error when trying to use the [nullish coalescing operator](https://github.com/tc39/proposal-nullish-coalescing) (`foo ?? bar`):

```
Error: unreachable
      at CodePathState.popChoiceContext (node_modules/eslint/lib/code-path-analysis/code-path-state.js:444:23)
      at processCodePathToExit (node_modules/eslint/lib/code-path-analysis/code-path-analyzer.js:434:19)
      at CodePathAnalyzer.leaveNode (node_modules/eslint/lib/code-path-analysis/code-path-analyzer.js:627:9)
      at Traverser.leave (node_modules/eslint/lib/linter.js:959:32)
      at Traverser.__execute (node_modules/estraverse/estraverse.js:397:31)
      at Traverser.traverse (node_modules/estraverse/estraverse.js:491:28)
      at Traverser.traverse (node_modules/eslint/lib/util/traverser.js:31:22)
      at Linter._verifyWithoutProcessors (node_modules/eslint/lib/linter.js:953:19)
      at preprocess.map.textBlock (node_modules/eslint/lib/linter.js:994:35)
      at Array.map (<anonymous>)
      at Linter.verify (node_modules/eslint/lib/linter.js:993:42)
      ...
```

That operator is part of a proposal in Stage 1, so according to the ESLint policy it doesn't have official support, but it shouldn't fail when it's present in the AST (from a different parser).

This problem happens for any operator other than `&&` and `||` in a logical expression, which are explicitly handled in the code.

This is just to track the discussion we're having in #10399. 

**Affected ESLint versions**: v4.19.1 (latest stable) and v5.0.0-rc.0 (latest).


Discussion:

Patch:
diff --git a/lib/code-path-analysis/code-path-analyzer.js b/lib/code-path-analysis/code-path-analyzer.js
index 1a4f7870baaa..80b73faa3cb1 100644
--- a/lib/code-path-analysis/code-path-analyzer.js
+++ b/lib/code-path-analysis/code-path-analyzer.js
@@ -30,6 +30,17 @@ function isCaseNode(node) {
     return Boolean(node.test);
 }
 
+/**
+ * Checks whether the given logical operator is taken into account for the code
+ * path analysis.
+ *
+ * @param {string} operator - The operator found in the LogicalExpression node
+ * @returns {boolean} `true` if the operator is "&&" or "||"
+ */
+function isHandledLogicalOperator(operator) {
+    return operator === "&&" || operator === "||";
+}
+
 /**
  * Checks whether or not a given logical expression node goes different path
  * between the `true` case and the `false` case.
@@ -230,7 +241,10 @@ function preprocess(analyzer, node) {
 
     switch (parent.type) {
         case "LogicalExpression":
-            if (parent.right === node) {
+            if (
+                parent.right === node &&
+                isHandledLogicalOperator(parent.operator)
+            ) {
                 state.makeLogicalRight();
             }
             break;
@@ -361,7 +375,12 @@ function processCodePathToEnter(analyzer, node) {
             break;
 
         case "LogicalExpression":
-            state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
+            if (isHandledLogicalOperator(node.operator)) {
+                state.pushChoiceContext(
+                    node.operator,
+                    isForkingByTrueOrFalse(node)
+                );
+            }
             break;
 
         case "ConditionalExpression":
@@ -430,10 +449,15 @@ function processCodePathToExit(analyzer, node) {
     switch (node.type) {
         case "IfStatement":
         case "ConditionalExpression":
-        case "LogicalExpression":
             state.popChoiceContext();
             break;
 
+        case "LogicalExpression":
+            if (isHandledLogicalOperator(node.operator)) {
+                state.popChoiceContext();
+            }
+            break;
+
         case "SwitchStatement":
             state.popSwitchContext();
             break;


Test patch:
diff --git a/tests/fixtures/parsers/unknown-operators/unknown-logical-operator.js b/tests/fixtures/parsers/unknown-operators/unknown-logical-operator.js
new file mode 100644
index 000000000000..eadc5979b760
--- /dev/null
+++ b/tests/fixtures/parsers/unknown-operators/unknown-logical-operator.js
@@ -0,0 +1,156 @@
+/**
+ * The output AST has been modified to represent an unknown operator
+ * (replaced ?? with %%)
+ *
+ * Parser: babel-eslint v8.2.3
+ * (with this fix for tokens: https://github.com/babel/babel-eslint/pull/632)
+ * Source code:
+ * null ?? 'foo'
+ */
+
+exports.parse = () => ({
+    type: "Program",
+    start: 0,
+    end: 13,
+    loc: {
+        start: {
+            line: 1,
+            column: 0
+        },
+        end: {
+            line: 1,
+            column: 13
+        }
+    },
+    range: [0, 13],
+    comments: [],
+    tokens: [
+        {
+            type: "Null",
+            value: "null",
+            start: 0,
+            end: 4,
+            loc: {
+                start: {
+                    line: 1,
+                    column: 0
+                },
+                end: {
+                    line: 1,
+                    column: 4
+                }
+            },
+            range: [0, 4]
+        },
+        {
+            type: "Punctuator",
+            value: "%%",
+            start: 5,
+            end: 7,
+            loc: {
+                start: {
+                    line: 1,
+                    column: 5
+                },
+                end: {
+                    line: 1,
+                    column: 7
+                }
+            },
+            range: [5, 7]
+        },
+        {
+            type: "String",
+            value: "'foo'",
+            start: 8,
+            end: 13,
+            loc: {
+                start: {
+                    line: 1,
+                    column: 8
+                },
+                end: {
+                    line: 1,
+                    column: 13
+                }
+            },
+            range: [8, 13]
+        }
+    ],
+    sourceType: "script",
+    body: [
+        {
+            type: "ExpressionStatement",
+            start: 0,
+            end: 13,
+            loc: {
+                start: {
+                    line: 1,
+                    column: 0
+                },
+                end: {
+                    line: 1,
+                    column: 13
+                }
+            },
+            range: [0, 13],
+            expression: {
+                type: "LogicalExpression",
+                start: 0,
+                end: 13,
+                loc: {
+                    start: {
+                        line: 1,
+                        column: 0
+                    },
+                    end: {
+                        line: 1,
+                        column: 13
+                    }
+                },
+                range: [0, 13],
+                left: {
+                    type: "Literal",
+                    start: 0,
+                    end: 4,
+                    loc: {
+                        start: {
+                            line: 1,
+                            column: 0
+                        },
+                        end: {
+                            line: 1,
+                            column: 4
+                        }
+                    },
+                    range: [0, 4],
+                    value: null,
+                    raw: "null",
+                    _babelType: "Literal"
+                },
+                operator: "%%",
+                right: {
+                    type: "Literal",
+                    start: 8,
+                    end: 13,
+                    loc: {
+                        start: {
+                            line: 1,
+                            column: 8
+                        },
+                        end: {
+                            line: 1,
+                            column: 13
+                        }
+                    },
+                    range: [8, 13],
+                    value: "foo",
+                    raw: "'foo'",
+                    _babelType: "Literal"
+                },
+                _babelType: "LogicalExpression"
+            },
+            _babelType: "ExpressionStatement"
+        }
+    ]
+});
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 15177c5cd961..251057021af4 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -4470,6 +4470,16 @@ describe("Linter", () => {
                 assert.strictEqual(messages[0].message, "Cannot find module 'esprima-xyz'");
             });
 
+            it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => {
+                const code = "null %% 'foo'";
+                const parser = path.join(parserFixtures, "unknown-operators", "unknown-logical-operator.js");
+
+                // This shouldn't throw
+                const messages = linter.verify(code, { parser }, filename, true);
+
+                assert.strictEqual(messages.length, 0);
+            });
+
             it("should strip leading line: prefix from parser error", () => {
                 const parser = path.join(parserFixtures, "line-error.js");
                 const messages = linter.verify(";", { parser }, "filename");