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: support ?? operator, import.meta, and export * as ns #13196

Merged
merged 11 commits into from Jun 4, 2020
1 change: 1 addition & 0 deletions docs/rules/no-mixed-operators.md
Expand Up @@ -100,6 +100,7 @@ The following operators can be used in `groups` option:
* Bitwise Operators: `"&"`, `"|"`, `"^"`, `"~"`, `"<<"`, `">>"`, `">>>"`
* Comparison Operators: `"=="`, `"!="`, `"==="`, `"!=="`, `">"`, `">="`, `"<"`, `"<="`
* Logical Operators: `"&&"`, `"||"`
* Coalesce Operator: `"??"`
* Relational Operators: `"in"`, `"instanceof"`
* Ternary Operator: `?:`

Expand Down
4 changes: 2 additions & 2 deletions lib/linter/code-path-analysis/code-path-analyzer.js
Expand Up @@ -33,10 +33,10 @@ function isCaseNode(node) {
* 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 "||"
* @returns {boolean} `true` if the operator is "&&" or "||" or "??"
*/
function isHandledLogicalOperator(operator) {
return operator === "&&" || operator === "||";
return operator === "&&" || operator === "||" || operator === "??";
}

/**
Expand Down
46 changes: 34 additions & 12 deletions lib/linter/code-path-analysis/code-path-state.js
Expand Up @@ -201,6 +201,7 @@ function finalizeTestSegmentsOfFor(context, choiceContext, head) {
if (!choiceContext.processed) {
choiceContext.trueForkContext.add(head);
choiceContext.falseForkContext.add(head);
choiceContext.qqForkContext.add(head);
}

if (context.test !== true) {
Expand Down Expand Up @@ -351,6 +352,7 @@ class CodePathState {
isForkingAsResult,
trueForkContext: ForkContext.newEmpty(this.forkContext),
falseForkContext: ForkContext.newEmpty(this.forkContext),
qqForkContext: ForkContext.newEmpty(this.forkContext),
processed: false
};
}
Expand All @@ -370,6 +372,7 @@ class CodePathState {
switch (context.kind) {
case "&&":
case "||":
case "??":

/*
* If any result were not transferred from child contexts,
Expand All @@ -379,6 +382,7 @@ class CodePathState {
if (!context.processed) {
context.trueForkContext.add(headSegments);
context.falseForkContext.add(headSegments);
context.qqForkContext.add(headSegments);
}

/*
Expand All @@ -390,6 +394,7 @@ class CodePathState {

parentContext.trueForkContext.addAll(context.trueForkContext);
parentContext.falseForkContext.addAll(context.falseForkContext);
parentContext.qqForkContext.addAll(context.qqForkContext);
parentContext.processed = true;

return context;
Expand Down Expand Up @@ -456,13 +461,24 @@ class CodePathState {
* This got segments already from the child choice context.
* Creates the next path from own true/false fork context.
*/
const prevForkContext =
context.kind === "&&" ? context.trueForkContext
/* kind === "||" */ : context.falseForkContext;
let prevForkContext;

switch (context.kind) {
case "&&": // if true then go to the right-hand side.
prevForkContext = context.trueForkContext;
break;
case "||": // if false then go to the right-hand side.
prevForkContext = context.falseForkContext;
break;
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
prevForkContext = context.qqForkContext;
break;
default:
throw new Error("unreachable");
}

forkContext.replaceHead(prevForkContext.makeNext(0, -1));
prevForkContext.clear();

context.processed = false;
} else {

Expand All @@ -471,14 +487,19 @@ class CodePathState {
* So addresses the head segments.
* The head segments are the path of the left-hand operand.
*/
if (context.kind === "&&") {

// The path does short-circuit if false.
context.falseForkContext.add(forkContext.head);
} else {

// The path does short-circuit if true.
context.trueForkContext.add(forkContext.head);
switch (context.kind) {
case "&&": // the false path can short-circuit.
context.falseForkContext.add(forkContext.head);
break;
case "||": // the true path can short-circuit.
context.trueForkContext.add(forkContext.head);
break;
case "??": // both can short-circuit.
context.trueForkContext.add(forkContext.head);
context.falseForkContext.add(forkContext.head);
break;
default:
throw new Error("unreachable");
}

forkContext.replaceHead(forkContext.makeNext(-1, -1));
Expand All @@ -501,6 +522,7 @@ class CodePathState {
if (!context.processed) {
context.trueForkContext.add(forkContext.head);
context.falseForkContext.add(forkContext.head);
context.qqForkContext.add(forkContext.head);
}

context.processed = false;
Expand Down
6 changes: 6 additions & 0 deletions lib/rules/keyword-spacing.js
Expand Up @@ -442,6 +442,12 @@ module.exports = {
checkSpacingAround(sourceCode.getTokenAfter(firstToken));
}

if (node.type === "ExportAllDeclaration" && node.exported) {
const asToken = sourceCode.getTokenBefore(node.exported);

checkSpacingBefore(asToken, PREV_TOKEN_M);
}

if (node.source) {
const fromToken = sourceCode.getTokenBefore(node.source);

Expand Down
3 changes: 3 additions & 0 deletions lib/rules/no-extra-boolean-cast.js
Expand Up @@ -172,6 +172,9 @@ module.exports = {
case "UnaryExpression":
return precedence(node) < precedence(parent);
case "LogicalExpression":
if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
return true;
}
if (previousNode === parent.left) {
return precedence(node) < precedence(parent);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/no-extra-parens.js
Expand Up @@ -478,6 +478,7 @@ module.exports = {
if (!shouldSkipLeft && hasExcessParens(node.left)) {
if (
!(node.left.type === "UnaryExpression" && isExponentiation) &&
!astUtils.isMixedLogicalAndCoalesceExpressions(node.left, node) &&
(leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation)) ||
isParenthesisedTwice(node.left)
) {
Expand All @@ -487,6 +488,7 @@ module.exports = {

if (!shouldSkipRight && hasExcessParens(node.right)) {
if (
!astUtils.isMixedLogicalAndCoalesceExpressions(node.right, node) &&
(rightPrecedence > prec || (rightPrecedence === prec && isExponentiation)) ||
isParenthesisedTwice(node.right)
) {
Expand Down
5 changes: 3 additions & 2 deletions lib/rules/no-mixed-operators.js
Expand Up @@ -21,13 +21,15 @@ const COMPARISON_OPERATORS = ["==", "!=", "===", "!==", ">", ">=", "<", "<="];
const LOGICAL_OPERATORS = ["&&", "||"];
const RELATIONAL_OPERATORS = ["in", "instanceof"];
const TERNARY_OPERATOR = ["?:"];
const COALESCE_OPERATOR = ["??"];
const ALL_OPERATORS = [].concat(
ARITHMETIC_OPERATORS,
BITWISE_OPERATORS,
COMPARISON_OPERATORS,
LOGICAL_OPERATORS,
RELATIONAL_OPERATORS,
TERNARY_OPERATOR
TERNARY_OPERATOR,
COALESCE_OPERATOR
);
const DEFAULT_GROUPS = [
ARITHMETIC_OPERATORS,
Expand Down Expand Up @@ -236,7 +238,6 @@ module.exports = {
return {
BinaryExpression: check,
LogicalExpression: check

};
}
};
6 changes: 6 additions & 0 deletions lib/rules/no-restricted-exports.js
Expand Up @@ -61,6 +61,12 @@ module.exports = {
}

return {
ExportAllDeclaration(node) {
if (node.exported) {
checkExportedName(node.exported);
}
},

ExportNamedDeclaration(node) {
const declaration = node.declaration;

Expand Down
10 changes: 6 additions & 4 deletions lib/rules/no-unneeded-ternary.js
Expand Up @@ -147,10 +147,12 @@ module.exports = {
loc: node.consequent.loc.start,
messageId: "unnecessaryConditionalAssignment",
fix: fixer => {
const shouldParenthesizeAlternate = (
astUtils.getPrecedence(node.alternate) < OR_PRECEDENCE &&
!astUtils.isParenthesised(sourceCode, node.alternate)
);
const shouldParenthesizeAlternate =
(
astUtils.getPrecedence(node.alternate) < OR_PRECEDENCE ||
astUtils.isCoalesceExpression(node.alternate)
) &&
!astUtils.isParenthesised(sourceCode, node.alternate);
const alternateText = shouldParenthesizeAlternate
? `(${sourceCode.getText(node.alternate)})`
: astUtils.getParenthesisedText(sourceCode, node.alternate);
Expand Down
54 changes: 53 additions & 1 deletion lib/rules/utils/ast-utils.js
Expand Up @@ -416,6 +416,53 @@ function equalTokens(left, right, sourceCode) {
return true;
}

/**
* Check if the given node is a true logical expression or not.
*
* The three binary expressions logical-or (`||`), logical-and (`&&`), and
* coalesce (`??`) are known as `ShortCircuitExpression`.
* But ESTree represents those by `LogicalExpression` node.
*
* This function rejects coalesce expressions of `LogicalExpression` node.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is `&&` or `||`.
* @see https://tc39.es/ecma262/#prod-ShortCircuitExpression
*/
function isLogicalExpression(node) {
return (
node.type === "LogicalExpression" &&
(node.operator === "&&" || node.operator === "||")
);
}

/**
* Check if the given node is a nullish coalescing expression or not.
*
* The three binary expressions logical-or (`||`), logical-and (`&&`), and
* coalesce (`??`) are known as `ShortCircuitExpression`.
* But ESTree represents those by `LogicalExpression` node.
*
* This function finds only coalesce expressions of `LogicalExpression` node.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is `??`.
*/
function isCoalesceExpression(node) {
return node.type === "LogicalExpression" && node.operator === "??";
}

/**
* Check if given two nodes are the pair of a logical expression and a coalesce expression.
* @param {ASTNode} left A node to check.
* @param {ASTNode} right Another node to check.
* @returns {boolean} `true` if the two nodes are the pair of a logical expression and a coalesce expression.
*/
function isMixedLogicalAndCoalesceExpressions(left, right) {
return (
(isLogicalExpression(left) && isCoalesceExpression(right)) ||
(isCoalesceExpression(left) && isLogicalExpression(right))
);
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -779,6 +826,7 @@ module.exports = {
case "LogicalExpression":
switch (node.operator) {
case "||":
case "??":
return 4;
case "&&":
return 5;
Expand Down Expand Up @@ -1538,5 +1586,9 @@ module.exports = {
*/
hasOctalEscapeSequence(rawString) {
return OCTAL_ESCAPE_PATTERN.test(rawString);
}
},

isLogicalExpression,
isCoalesceExpression,
isMixedLogicalAndCoalesceExpressions
};
8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -52,10 +52,10 @@
"cross-spawn": "^7.0.2",
"debug": "^4.0.1",
"doctrine": "^3.0.0",
"eslint-scope": "^5.0.0",
"eslint-scope": "^5.1.0",
"eslint-utils": "^2.0.0",
"eslint-visitor-keys": "^1.1.0",
"espree": "^7.0.0",
"eslint-visitor-keys": "^1.2.0",
"espree": "^7.1.0",
"esquery": "^1.2.0",
"esutils": "^2.0.2",
"file-entry-cache": "^5.0.1",
Expand Down Expand Up @@ -86,7 +86,7 @@
"devDependencies": {
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
"acorn": "^7.1.1",
"acorn": "^7.2.0",
"babel-loader": "^8.0.5",
"chai": "^4.0.1",
"cheerio": "^0.22.0",
Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/code-path-analysis/logical--do-while-qq-1.js
@@ -0,0 +1,23 @@
/*expected
initial->s1_1->s1_2->s1_3->s1_2->s1_2;
s1_3->s1_4;
s1_2->s1_4->final;
*/
do {
foo();
} while (a ?? b);

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program\nDoWhileStatement"];
s1_2[label="BlockStatement\nExpressionStatement\nCallExpression\nIdentifier (foo)\nLogicalExpression\nIdentifier (a)\nIdentifier:exit (foo)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit\nIdentifier:exit (a)"];
s1_3[label="Identifier (b)\nIdentifier:exit (b)\nLogicalExpression:exit"];
s1_4[label="DoWhileStatement:exit\nProgram:exit"];
initial->s1_1->s1_2->s1_3->s1_2->s1_2;
s1_3->s1_4;
s1_2->s1_4->final;
}
*/
33 changes: 33 additions & 0 deletions tests/fixtures/code-path-analysis/logical--do-while-qq-2.js
@@ -0,0 +1,33 @@
/*expected
initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_2->s1_2;
s1_3->s1_2;
s1_4->s1_2;
s1_5->s1_6;
s1_2->s1_6;
s1_3->s1_6;
s1_4->s1_6->final;
*/
do {
foo();
} while (a ?? b ?? c ?? d);

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program\nDoWhileStatement"];
s1_2[label="BlockStatement\nExpressionStatement\nCallExpression\nIdentifier (foo)\nLogicalExpression\nLogicalExpression\nLogicalExpression\nIdentifier (a)\nIdentifier:exit (foo)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit\nIdentifier:exit (a)"];
s1_3[label="Identifier (b)\nIdentifier:exit (b)\nLogicalExpression:exit"];
s1_4[label="Identifier (c)\nIdentifier:exit (c)\nLogicalExpression:exit"];
s1_5[label="Identifier (d)\nIdentifier:exit (d)\nLogicalExpression:exit"];
s1_6[label="DoWhileStatement:exit\nProgram:exit"];
initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_2->s1_2;
s1_3->s1_2;
s1_4->s1_2;
s1_5->s1_6;
s1_2->s1_6;
s1_3->s1_6;
s1_4->s1_6->final;
}
*/