Skip to content

Commit

Permalink
Update: support ?? operator, import.meta, and export * as ns (#13196
Browse files Browse the repository at this point in the history
)

* upgrade deps

* update code path analysis for nullish coalescing

* update rules for `export * as ns from "source"`

* add a few tests for import.meta

* update rules for nullish coalescing

* update deps

* add tests to no-restricted-imports

* add tests to camelcase

* add tests to id-blacklist

* add tests for id-match
  • Loading branch information
mysticatea committed Jun 4, 2020
1 parent d5fce9f commit dd949ae
Show file tree
Hide file tree
Showing 39 changed files with 834 additions and 31 deletions.
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;
}
*/

0 comments on commit dd949ae

Please sign in to comment.