Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: optional chaining support (fixes #12642) #13416

Merged
merged 78 commits into from Jul 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
58c473e
update deps (to branch)
mysticatea Jun 12, 2020
4d5c41b
trivial fix for debug output
mysticatea Jun 12, 2020
a574e27
update code path analysis
mysticatea Jun 13, 2020
9125cf1
update accessor-pairs
mysticatea Jun 13, 2020
102c4db
update array-callback-return
mysticatea Jun 13, 2020
3de0a1c
add tests for camelcase
mysticatea Jun 13, 2020
f2dc7f2
add tests for computed-property-spacing
mysticatea Jun 13, 2020
33e4986
update dot-location
mysticatea Jun 13, 2020
f66ce6c
update dot-notation
mysticatea Jun 13, 2020
206d25f
update func-name-matching
mysticatea Jun 13, 2020
2824b22
update global-require
mysticatea Jun 13, 2020
9a14a09
update indent
mysticatea Jun 13, 2020
276526b
add tests for getter-return
mysticatea Jun 14, 2020
7574cfd
update new-cap
mysticatea Jun 14, 2020
ed71bd4
update newline-per-chained-call
mysticatea Jun 14, 2020
9fd0338
update no-alert
mysticatea Jun 14, 2020
45c5aea
update no-extend-native
mysticatea Jun 14, 2020
38592d2
update no-extra-bind
mysticatea Jun 14, 2020
a395b10
update no-extra-parens
mysticatea Jun 15, 2020
abcceae
update no-eval
mysticatea Jun 15, 2020
e550194
update no-implicit-coercion
mysticatea Jun 15, 2020
3a31699
update eslint-utils
mysticatea Jun 15, 2020
248faf1
update no-implied-eval
mysticatea Jun 16, 2020
b3dee67
update no-import-assign
mysticatea Jun 16, 2020
c63d3c1
update no-magic-numbers
mysticatea Jun 16, 2020
e13059f
update no-obj-calls
mysticatea Jun 16, 2020
5286f83
update no-prototype-builtins
mysticatea Jun 17, 2020
8a0097a
add tests for no-restricted-syntax
mysticatea Jun 17, 2020
d12b2be
update no-self-assign
mysticatea Jun 17, 2020
a31e04c
update no-setter-return
mysticatea Jun 17, 2020
6c6bd95
update no-unexpected-multiline
mysticatea Jun 17, 2020
6c00e33
update no-unused-expression
mysticatea Jun 17, 2020
f7fd797
update no-useless-call
mysticatea Jun 17, 2020
b5fa99a
update no-whitespace-before-property
mysticatea Jun 17, 2020
6468c0c
update operator-assignment
mysticatea Jun 18, 2020
5740538
update padding-line-between-statements
mysticatea Jun 18, 2020
d0fc1b1
update prefer-arrow-callback
mysticatea Jun 18, 2020
a443a1e
add tests for prefer-destructuring
mysticatea Jun 18, 2020
ca0b2a8
update prefer-exponentiation-operator
mysticatea Jun 18, 2020
d978d36
update prefer-numeric-literals
mysticatea Jun 18, 2020
5c7368c
update prefer-promise-reject-errors
mysticatea Jun 18, 2020
651bac3
update prefer-regex-literals
mysticatea Jun 18, 2020
a3cbe6e
update prefer-spread
mysticatea Jun 18, 2020
17ec3d0
update use-isnan
mysticatea Jun 18, 2020
89b65bf
update yoda
mysticatea Jun 18, 2020
233e737
update wrap-iife
mysticatea Jun 18, 2020
9a91ba1
remove __proto__
mysticatea Jun 21, 2020
f65df75
fix no-import-assign for delete op
mysticatea Jun 21, 2020
9939c4c
update eslint-visitor-keys
mysticatea Jun 21, 2020
776e96b
fix no-unexpected-multiline to just ignore optional chaining
mysticatea Jun 21, 2020
31412c2
update func-call-spacing
mysticatea Jun 21, 2020
80e5e92
update constructor-super
mysticatea Jun 21, 2020
bafe845
Merge branch 'master' into optional-chaining
mysticatea Jun 21, 2020
b91d17b
update dot-location for unstable sort
mysticatea Jun 21, 2020
d0c2f82
update no-extra-boolean-cast
mysticatea Jun 22, 2020
079a1b4
update func-call-spacing
mysticatea Jun 23, 2020
bf2da2b
update no-extra-parens for false positive on IIFE
mysticatea Jun 23, 2020
6bce1c2
update array-callback-return
mysticatea Jun 25, 2020
6088f8e
update no-invalid-this (astUtils.isDefaultThisBinding)
mysticatea Jun 25, 2020
c41ad78
update radix
mysticatea Jun 25, 2020
e5428c3
Merge remote-tracking branch 'origin/master' into optional-chaining
mysticatea Jun 25, 2020
8c7ea81
update a comment in no-implicit-coercion
mysticatea Jul 2, 2020
4f46b30
update comments in no-extra-bind
mysticatea Jul 2, 2020
d7e5e3c
remove unnecessary change from array-callback-return
mysticatea Jul 2, 2020
0a38bd8
update dot-notation for autofix about `let?.[`
mysticatea Jul 2, 2020
4514355
update new-cap
mysticatea Jul 2, 2020
996b51b
update wrap-iife
mysticatea Jul 2, 2020
9dc1b2f
update prefer-arrow-callback
mysticatea Jul 2, 2020
9091c34
change isSameReference to handle `a.b` and `a?.b` are same
mysticatea Jul 2, 2020
9a70361
Merge remote-tracking branch 'origin/master' into optional-chaining
mysticatea Jul 8, 2020
b3a0773
fix code path analysis for `node.arguments.length == 0` case
mysticatea Jul 8, 2020
1454dd8
update `astUtils.couldBeError`
mysticatea Jul 14, 2020
d0f2f3a
update `astUtils.isMethodWhichHasThisArg`
mysticatea Jul 14, 2020
06e3fe2
Merge remote-tracking branch 'origin/master' into optional-chaining
mysticatea Jul 14, 2020
e017249
improve coverage
mysticatea Jul 14, 2020
aab025a
fix isMethodWhichHasThisArg
mysticatea Jul 14, 2020
958a3ab
update no-self-assign
mysticatea Jul 16, 2020
a1e1dc3
Upgrade: espree@7.2.0
kaicataldo Jul 18, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
mysticatea marked this conversation as resolved.
Show resolved Hide resolved

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