Skip to content

Commit

Permalink
Update: optional chaining support (fixes #12642) (#13416)
Browse files Browse the repository at this point in the history
* update deps (to branch)

* trivial fix for debug output

* update code path analysis

* update accessor-pairs

* update array-callback-return

* add tests for camelcase

* add tests for computed-property-spacing

* update dot-location

* update dot-notation

* update func-name-matching

* update global-require

* update indent

* add tests for getter-return

* update new-cap

* update newline-per-chained-call

* update no-alert

* update no-extend-native

* update no-extra-bind

* update no-extra-parens

* update no-eval

* update no-implicit-coercion

* update eslint-utils

* update no-implied-eval

* update no-import-assign

* update no-magic-numbers

* update no-obj-calls

* update no-prototype-builtins

* add tests for no-restricted-syntax

* update no-self-assign

* update no-setter-return

* update no-unexpected-multiline

* update no-unused-expression

* update no-useless-call

* update no-whitespace-before-property

* update operator-assignment

* update padding-line-between-statements

* update prefer-arrow-callback

* add tests for prefer-destructuring

* update prefer-exponentiation-operator

* update prefer-numeric-literals

* update prefer-promise-reject-errors

* update prefer-regex-literals

* update prefer-spread

* update use-isnan

* update yoda

* update wrap-iife

* remove __proto__

* fix no-import-assign for delete op

* update eslint-visitor-keys

* fix no-unexpected-multiline to just ignore optional chaining

* update func-call-spacing

* update constructor-super

* update dot-location for unstable sort

* update no-extra-boolean-cast

* update func-call-spacing

* update no-extra-parens for false positive on IIFE

* update array-callback-return

* update no-invalid-this (astUtils.isDefaultThisBinding)

* update radix

* update a comment in no-implicit-coercion

* update comments in no-extra-bind

* remove unnecessary change from array-callback-return

* update dot-notation for autofix about `let?.[`

* update new-cap

* update wrap-iife

* update prefer-arrow-callback

* change isSameReference to handle `a.b` and `a?.b` are same

* fix code path analysis for `node.arguments.length == 0` case

* update `astUtils.couldBeError`

* update `astUtils.isMethodWhichHasThisArg`

* improve coverage

* fix isMethodWhichHasThisArg

* update no-self-assign

* Upgrade: espree@7.2.0

Co-authored-by: Kai Cataldo <kai@kaicataldo.com>
  • Loading branch information
mysticatea and kaicataldo committed Jul 18, 2020
1 parent 540b1af commit 6ea3178
Show file tree
Hide file tree
Showing 106 changed files with 4,293 additions and 769 deletions.
38 changes: 38 additions & 0 deletions lib/linter/code-path-analysis/code-path-analyzer.js
Expand Up @@ -244,6 +244,19 @@ function preprocess(analyzer, node) {
const parent = node.parent;

switch (parent.type) {

// The `arguments.length == 0` case is in `postprocess` function.
case "CallExpression":
if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) {
state.makeOptionalRight();
}
break;
case "MemberExpression":
if (parent.optional === true && parent.property === node) {
state.makeOptionalRight();
}
break;

case "LogicalExpression":
if (
parent.right === node &&
Expand Down Expand Up @@ -377,6 +390,20 @@ function processCodePathToEnter(analyzer, node) {
analyzer.emitter.emit("onCodePathStart", codePath, node);
break;

case "ChainExpression":
state.pushChainContext();
break;
case "CallExpression":
if (node.optional === true) {
state.makeOptionalNode();
}
break;
case "MemberExpression":
if (node.optional === true) {
state.makeOptionalNode();
}
break;

case "LogicalExpression":
if (isHandledLogicalOperator(node.operator)) {
state.pushChoiceContext(
Expand Down Expand Up @@ -449,6 +476,10 @@ function processCodePathToExit(analyzer, node) {
let dontForward = false;

switch (node.type) {
case "ChainExpression":
state.popChainContext();
break;

case "IfStatement":
case "ConditionalExpression":
state.popChoiceContext();
Expand Down Expand Up @@ -583,6 +614,13 @@ function postprocess(analyzer, node) {
break;
}

// The `arguments.length >= 1` case is in `preprocess` function.
case "CallExpression":
if (node.optional === true && node.arguments.length === 0) {
CodePath.getState(analyzer.codePath).makeOptionalRight();
}
break;

default:
break;
}
Expand Down
1 change: 0 additions & 1 deletion lib/linter/code-path-analysis/code-path-segment.js
Expand Up @@ -92,7 +92,6 @@ class CodePathSegment {
/* istanbul ignore if */
if (debug.enabled) {
this.internal.nodes = [];
this.internal.exitNodes = [];
}
}

Expand Down
59 changes: 59 additions & 0 deletions lib/linter/code-path-analysis/code-path-state.js
Expand Up @@ -234,6 +234,7 @@ class CodePathState {
this.tryContext = null;
this.loopContext = null;
this.breakContext = null;
this.chainContext = null;

this.currentSegments = [];
this.initialSegment = this.forkContext.head[0];
Expand Down Expand Up @@ -555,6 +556,64 @@ class CodePathState {
);
}

//--------------------------------------------------------------------------
// ChainExpression
//--------------------------------------------------------------------------

/**
* Push a new `ChainExpression` context to the stack.
* This method is called on entering to each `ChainExpression` node.
* This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
* @returns {void}
*/
pushChainContext() {
this.chainContext = {
upper: this.chainContext,
countChoiceContexts: 0
};
}

/**
* Pop a `ChainExpression` context from the stack.
* This method is called on exiting from each `ChainExpression` node.
* This merges all forks of the last optional chaining.
* @returns {void}
*/
popChainContext() {
const context = this.chainContext;

this.chainContext = context.upper;

// pop all choice contexts of this.
for (let i = context.countChoiceContexts; i > 0; --i) {
this.popChoiceContext();
}
}

/**
* Create a choice context for optional access.
* This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
* This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
* @returns {void}
*/
makeOptionalNode() {
if (this.chainContext) {
this.chainContext.countChoiceContexts += 1;
this.pushChoiceContext("??", false);
}
}

/**
* Create a fork.
* This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
* @returns {void}
*/
makeOptionalRight() {
if (this.chainContext) {
this.makeLogicalRight();
}
}

//--------------------------------------------------------------------------
// SwitchStatement
//--------------------------------------------------------------------------
Expand Down
45 changes: 26 additions & 19 deletions lib/linter/code-path-analysis/debug-helpers.js
Expand Up @@ -25,6 +25,22 @@ function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc
return segment.id + (segment.reachable ? "" : "!");
}

/**
* Get string for the given node and operation.
* @param {ASTNode} node The node to convert.
* @param {"enter" | "exit" | undefined} label The operation label.
* @returns {string} The string representation.
*/
function nodeToString(node, label) {
const suffix = label ? `:${label}` : "";

switch (node.type) {
case "Identifier": return `${node.type}${suffix} (${node.name})`;
case "Literal": return `${node.type}${suffix} (${node.value})`;
default: return `${node.type}${suffix}`;
}
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -56,9 +72,15 @@ module.exports = {
const segInternal = state.currentSegments[i].internal;

if (leaving) {
segInternal.exitNodes.push(node);
const last = segInternal.nodes.length - 1;

if (last >= 0 && segInternal.nodes[last] === nodeToString(node, "enter")) {
segInternal.nodes[last] = nodeToString(node, void 0);
} else {
segInternal.nodes.push(nodeToString(node, "exit"));
}
} else {
segInternal.nodes.push(node);
segInternal.nodes.push(nodeToString(node, "enter"));
}
}

Expand Down Expand Up @@ -104,23 +126,8 @@ module.exports = {
text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<<unreachable>>\\n";
}

if (segment.internal.nodes.length > 0 || segment.internal.exitNodes.length > 0) {
text += [].concat(
segment.internal.nodes.map(node => {
switch (node.type) {
case "Identifier": return `${node.type} (${node.name})`;
case "Literal": return `${node.type} (${node.value})`;
default: return node.type;
}
}),
segment.internal.exitNodes.map(node => {
switch (node.type) {
case "Identifier": return `${node.type}:exit (${node.name})`;
case "Literal": return `${node.type}:exit (${node.value})`;
default: return `${node.type}:exit`;
}
})
).join("\\n");
if (segment.internal.nodes.length > 0) {
text += segment.internal.nodes.join("\\n");
} else {
text += "????";
}
Expand Down
15 changes: 1 addition & 14 deletions lib/rules/accessor-pairs.js
Expand Up @@ -86,16 +86,6 @@ function isAccessorKind(node) {
return node.kind === "get" || node.kind === "set";
}

/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node A node to check.
* @param {string} name An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}

/**
* Checks whether or not a given node is an argument of a specified method call.
* @param {ASTNode} node A node to check.
Expand All @@ -109,10 +99,7 @@ function isArgumentOfMethodCall(node, index, object, property) {

return (
parent.type === "CallExpression" &&
parent.callee.type === "MemberExpression" &&
parent.callee.computed === false &&
isIdentifier(parent.callee.object, object) &&
isIdentifier(parent.callee.property, property) &&
astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
parent.arguments[index] === node
);
}
Expand Down
12 changes: 5 additions & 7 deletions lib/rules/array-callback-return.js
Expand Up @@ -28,17 +28,14 @@ function isReachable(segment) {
}

/**
* Checks a given node is a MemberExpression node which has the specified name's
* Checks a given node is a member access which has the specified name's
* property.
* @param {ASTNode} node A node to check.
* @returns {boolean} `true` if the node is a MemberExpression node which has
* the specified name's property
* @returns {boolean} `true` if the node is a member access which has
* the specified name's property. The node may be a `(Chain|Member)Expression` node.
*/
function isTargetMethod(node) {
return (
node.type === "MemberExpression" &&
TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "")
);
return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
}

/**
Expand Down Expand Up @@ -76,6 +73,7 @@ function getArrayMethodName(node) {
*/
case "LogicalExpression":
case "ConditionalExpression":
case "ChainExpression":
currentNode = parent;
break;

Expand Down
13 changes: 1 addition & 12 deletions lib/rules/consistent-return.js
Expand Up @@ -9,23 +9,12 @@
//------------------------------------------------------------------------------

const lodash = require("lodash");

const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node A node to check.
* @param {string} name An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}

/**
* Checks whether or not a given code path segment is unreachable.
* @param {CodePathSegment} segment A CodePathSegment to check.
Expand Down Expand Up @@ -165,7 +154,7 @@ module.exports = {
let hasReturnValue = Boolean(argument);

if (treatUndefinedAsUnspecified && hasReturnValue) {
hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void";
hasReturnValue = !astUtils.isSpecificId(argument, "undefined") && argument.operator !== "void";
}

if (!funcInfo.hasReturn) {
Expand Down
1 change: 1 addition & 0 deletions lib/rules/constructor-super.js
Expand Up @@ -50,6 +50,7 @@ function isPossibleConstructor(node) {
case "MemberExpression":
case "CallExpression":
case "NewExpression":
case "ChainExpression":
case "YieldExpression":
case "TaggedTemplateExpression":
case "MetaProperty":
Expand Down
34 changes: 20 additions & 14 deletions lib/rules/dot-location.js
Expand Up @@ -52,31 +52,37 @@ module.exports = {
*/
function checkDotLocation(node) {
const property = node.property;
const dot = sourceCode.getTokenBefore(property);

// `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node.
const tokenBeforeDot = sourceCode.getTokenBefore(dot);

const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]);
const textAfterDot = sourceCode.getText().slice(dot.range[1], property.range[0]);
const dotToken = sourceCode.getTokenBefore(property);

if (onObject) {
if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) {
const neededTextAfterToken = astUtils.isDecimalIntegerNumericToken(tokenBeforeDot) ? " " : "";

// `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node.
const tokenBeforeDot = sourceCode.getTokenBefore(dotToken);

if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dotToken)) {
context.report({
node,
loc: dot.loc,
loc: dotToken.loc,
messageId: "expectedDotAfterObject",
fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`)
*fix(fixer) {
if (dotToken.value.startsWith(".") && astUtils.isDecimalIntegerNumericToken(tokenBeforeDot)) {
yield fixer.insertTextAfter(tokenBeforeDot, ` ${dotToken.value}`);
} else {
yield fixer.insertTextAfter(tokenBeforeDot, dotToken.value);
}
yield fixer.remove(dotToken);
}
});
}
} else if (!astUtils.isTokenOnSameLine(dot, property)) {
} else if (!astUtils.isTokenOnSameLine(dotToken, property)) {
context.report({
node,
loc: dot.loc,
loc: dotToken.loc,
messageId: "expectedDotBeforeProperty",
fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${textBeforeDot}${textAfterDot}.`)
*fix(fixer) {
yield fixer.remove(dotToken);
yield fixer.insertTextBefore(property, dotToken.value);
}
});
}
}
Expand Down

0 comments on commit 6ea3178

Please sign in to comment.