Skip to content

Commit d2f6801

Browse files
nzakasmdjermanovic
andauthored
fix: Ensure correct code path for && followed by ?? (#17618)
* fix: Ensure correct code path for && followed by ?? fixes #13614 * Remove .only * Update lib/linter/code-path-analysis/code-path-state.js Co-authored-by: Milos Djermanovic <[email protected]> * Update tests/lib/linter/code-path-analysis/code-path-analyzer.js Co-authored-by: Milos Djermanovic <[email protected]> * Update variable names in comments --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent 2665552 commit d2f6801

4 files changed

Lines changed: 130 additions & 59 deletions

File tree

lib/linter/code-path-analysis/code-path-state.js

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class ChainContext {
121121
* true go one way and if false go the other (tracked by `trueForkContext` and
122122
* `falseForkContext`). The `??` operator doesn't operate on true/false because
123123
* the left expression is evaluated to be nullish or not, so only if nullish do
124-
* we fork to the right expression (tracked by `qqForkcontext`).
124+
* we fork to the right expression (tracked by `nullishForkContext`).
125125
*/
126126
class ChoiceContext {
127127

@@ -170,14 +170,14 @@ class ChoiceContext {
170170
this.falseForkContext = ForkContext.newEmpty(forkContext);
171171

172172
/**
173-
* The fork context for the right side of the `??` path of the choice.
173+
* The fork context for when the choice result is `null` or `undefined`.
174174
* @type {ForkContext}
175175
*/
176-
this.qqForkContext = ForkContext.newEmpty(forkContext);
176+
this.nullishForkContext = ForkContext.newEmpty(forkContext);
177177

178178
/**
179179
* Indicates if any of `trueForkContext`, `falseForkContext`, or
180-
* `qqForkContext` have been updated with segments from a child context.
180+
* `nullishForkContext` have been updated with segments from a child context.
181181
* @type {boolean}
182182
*/
183183
this.processed = false;
@@ -849,7 +849,7 @@ function finalizeTestSegmentsOfFor(context, choiceContext, head) {
849849
if (!choiceContext.processed) {
850850
choiceContext.trueForkContext.add(head);
851851
choiceContext.falseForkContext.add(head);
852-
choiceContext.qqForkContext.add(head);
852+
choiceContext.nullishForkContext.add(head);
853853
}
854854

855855
/*
@@ -1095,31 +1095,31 @@ class CodePathState {
10951095

10961096
/**
10971097
* Pops the last choice context and finalizes it.
1098+
* This is called upon leaving a node that represents a choice.
10981099
* @throws {Error} (Unreachable.)
10991100
* @returns {ChoiceContext} The popped context.
11001101
*/
11011102
popChoiceContext() {
1102-
const context = this.choiceContext;
1103-
1104-
this.choiceContext = context.upper;
1105-
1103+
const poppedChoiceContext = this.choiceContext;
11061104
const forkContext = this.forkContext;
1107-
const headSegments = forkContext.head;
1105+
const head = forkContext.head;
11081106

1109-
switch (context.kind) {
1107+
this.choiceContext = poppedChoiceContext.upper;
1108+
1109+
switch (poppedChoiceContext.kind) {
11101110
case "&&":
11111111
case "||":
11121112
case "??":
11131113

11141114
/*
1115-
* The `headSegments` are the path of the right-hand operand.
1115+
* The `head` are the path of the right-hand operand.
11161116
* If we haven't previously added segments from child contexts,
11171117
* then we add these segments to all possible forks.
11181118
*/
1119-
if (!context.processed) {
1120-
context.trueForkContext.add(headSegments);
1121-
context.falseForkContext.add(headSegments);
1122-
context.qqForkContext.add(headSegments);
1119+
if (!poppedChoiceContext.processed) {
1120+
poppedChoiceContext.trueForkContext.add(head);
1121+
poppedChoiceContext.falseForkContext.add(head);
1122+
poppedChoiceContext.nullishForkContext.add(head);
11231123
}
11241124

11251125
/*
@@ -1128,38 +1128,38 @@ class CodePathState {
11281128
* then we take the segments for this context and move them up
11291129
* to the parent context.
11301130
*/
1131-
if (context.isForkingAsResult) {
1131+
if (poppedChoiceContext.isForkingAsResult) {
11321132
const parentContext = this.choiceContext;
11331133

1134-
parentContext.trueForkContext.addAll(context.trueForkContext);
1135-
parentContext.falseForkContext.addAll(context.falseForkContext);
1136-
parentContext.qqForkContext.addAll(context.qqForkContext);
1134+
parentContext.trueForkContext.addAll(poppedChoiceContext.trueForkContext);
1135+
parentContext.falseForkContext.addAll(poppedChoiceContext.falseForkContext);
1136+
parentContext.nullishForkContext.addAll(poppedChoiceContext.nullishForkContext);
11371137
parentContext.processed = true;
11381138

11391139
// Exit early so we don't collapse all paths into one.
1140-
return context;
1140+
return poppedChoiceContext;
11411141
}
11421142

11431143
break;
11441144

11451145
case "test":
1146-
if (!context.processed) {
1146+
if (!poppedChoiceContext.processed) {
11471147

11481148
/*
11491149
* The head segments are the path of the `if` block here.
11501150
* Updates the `true` path with the end of the `if` block.
11511151
*/
1152-
context.trueForkContext.clear();
1153-
context.trueForkContext.add(headSegments);
1152+
poppedChoiceContext.trueForkContext.clear();
1153+
poppedChoiceContext.trueForkContext.add(head);
11541154
} else {
11551155

11561156
/*
11571157
* The head segments are the path of the `else` block here.
11581158
* Updates the `false` path with the end of the `else`
11591159
* block.
11601160
*/
1161-
context.falseForkContext.clear();
1162-
context.falseForkContext.add(headSegments);
1161+
poppedChoiceContext.falseForkContext.clear();
1162+
poppedChoiceContext.falseForkContext.add(head);
11631163
}
11641164

11651165
break;
@@ -1170,7 +1170,7 @@ class CodePathState {
11701170
* Loops are addressed in `popLoopContext()` so just return
11711171
* the context without modification.
11721172
*/
1173-
return context;
1173+
return poppedChoiceContext;
11741174

11751175
/* c8 ignore next */
11761176
default:
@@ -1180,71 +1180,116 @@ class CodePathState {
11801180
/*
11811181
* Merge the true path with the false path to create a single path.
11821182
*/
1183-
const combinedForkContext = context.trueForkContext;
1183+
const combinedForkContext = poppedChoiceContext.trueForkContext;
11841184

1185-
combinedForkContext.addAll(context.falseForkContext);
1185+
combinedForkContext.addAll(poppedChoiceContext.falseForkContext);
11861186
forkContext.replaceHead(combinedForkContext.makeNext(0, -1));
11871187

1188-
return context;
1188+
return poppedChoiceContext;
11891189
}
11901190

11911191
/**
1192-
* Makes a code path segment of the right-hand operand of a logical
1192+
* Creates a code path segment to represent right-hand operand of a logical
11931193
* expression.
1194+
* This is called in the preprocessing phase when entering a node.
11941195
* @throws {Error} (Unreachable.)
11951196
* @returns {void}
11961197
*/
11971198
makeLogicalRight() {
1198-
const context = this.choiceContext;
1199+
const currentChoiceContext = this.choiceContext;
11991200
const forkContext = this.forkContext;
12001201

1201-
if (context.processed) {
1202+
if (currentChoiceContext.processed) {
12021203

12031204
/*
1204-
* This got segments already from the child choice context.
1205-
* Creates the next path from own true/false fork context.
1205+
* This context was already assigned segments from a child
1206+
* choice context. In this case, we are concerned only about
1207+
* the path that does not short-circuit and so ends up on the
1208+
* right-hand operand of the logical expression.
12061209
*/
12071210
let prevForkContext;
12081211

1209-
switch (context.kind) {
1212+
switch (currentChoiceContext.kind) {
12101213
case "&&": // if true then go to the right-hand side.
1211-
prevForkContext = context.trueForkContext;
1214+
prevForkContext = currentChoiceContext.trueForkContext;
12121215
break;
12131216
case "||": // if false then go to the right-hand side.
1214-
prevForkContext = context.falseForkContext;
1217+
prevForkContext = currentChoiceContext.falseForkContext;
12151218
break;
1216-
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
1217-
prevForkContext = context.qqForkContext;
1219+
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's nullishForkContext.
1220+
prevForkContext = currentChoiceContext.nullishForkContext;
12181221
break;
12191222
default:
12201223
throw new Error("unreachable");
12211224
}
12221225

1226+
/*
1227+
* Create the segment for the right-hand operand of the logical expression
1228+
* and adjust the fork context pointer to point there. The right-hand segment
1229+
* is added at the end of all segments in `prevForkContext`.
1230+
*/
12231231
forkContext.replaceHead(prevForkContext.makeNext(0, -1));
1232+
1233+
/*
1234+
* We no longer need this list of segments.
1235+
*
1236+
* Reset `processed` because we've removed the segments from the child
1237+
* choice context. This allows `popChoiceContext()` to continue adding
1238+
* segments later.
1239+
*/
12241240
prevForkContext.clear();
1225-
context.processed = false;
1241+
currentChoiceContext.processed = false;
1242+
12261243
} else {
12271244

12281245
/*
1229-
* This did not get segments from the child choice context.
1230-
* So addresses the head segments.
1231-
* The head segments are the path of the left-hand operand.
1246+
* This choice context was not assigned segments from a child
1247+
* choice context, which means that it's a terminal logical
1248+
* expression.
1249+
*
1250+
* `head` is the segments for the left-hand operand of the
1251+
* logical expression.
1252+
*
1253+
* Each of the fork contexts below are empty at this point. We choose
1254+
* the path(s) that will short-circuit and add the segment for the
1255+
* left-hand operand to it. Ultimately, this will be the only segment
1256+
* in that path due to the short-circuting, so we are just seeding
1257+
* these paths to start.
12321258
*/
1233-
switch (context.kind) {
1234-
case "&&": // the false path can short-circuit.
1235-
context.falseForkContext.add(forkContext.head);
1259+
switch (currentChoiceContext.kind) {
1260+
case "&&":
1261+
1262+
/*
1263+
* In most contexts, when a && expression evaluates to false,
1264+
* it short circuits, so we need to account for that by setting
1265+
* the `falseForkContext` to the left operand.
1266+
*
1267+
* When a && expression is the left-hand operand for a ??
1268+
* expression, such as `(a && b) ?? c`, a nullish value will
1269+
* also short-circuit in a different way than a false value,
1270+
* so we also set the `nullishForkContext` to the left operand.
1271+
* This path is only used with a ?? expression and is thrown
1272+
* away for any other type of logical expression, so it's safe
1273+
* to always add.
1274+
*/
1275+
currentChoiceContext.falseForkContext.add(forkContext.head);
1276+
currentChoiceContext.nullishForkContext.add(forkContext.head);
12361277
break;
12371278
case "||": // the true path can short-circuit.
1238-
context.trueForkContext.add(forkContext.head);
1279+
currentChoiceContext.trueForkContext.add(forkContext.head);
12391280
break;
12401281
case "??": // both can short-circuit.
1241-
context.trueForkContext.add(forkContext.head);
1242-
context.falseForkContext.add(forkContext.head);
1282+
currentChoiceContext.trueForkContext.add(forkContext.head);
1283+
currentChoiceContext.falseForkContext.add(forkContext.head);
12431284
break;
12441285
default:
12451286
throw new Error("unreachable");
12461287
}
12471288

1289+
/*
1290+
* Create the segment for the right-hand operand of the logical expression
1291+
* and adjust the fork context pointer to point there.
1292+
*/
12481293
forkContext.replaceHead(forkContext.makeNext(-1, -1));
12491294
}
12501295
}
@@ -1265,7 +1310,7 @@ class CodePathState {
12651310
if (!context.processed) {
12661311
context.trueForkContext.add(forkContext.head);
12671312
context.falseForkContext.add(forkContext.head);
1268-
context.qqForkContext.add(forkContext.head);
1313+
context.nullishForkContext.add(forkContext.head);
12691314
}
12701315

12711316
context.processed = false;

tests/fixtures/code-path-analysis/assignment--nested-and-3.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/*expected
22
initial->s1_1->s1_2->s1_3->s1_4;
3-
s1_1->s1_4;
4-
s1_2->s1_4->final;
3+
s1_1->s1_3;
4+
s1_2->s1_4;
5+
s1_1->s1_4->final;
56
*/
67
(a &&= b) ?? c;
78

@@ -15,7 +16,8 @@ digraph {
1516
s1_3[label="Identifier (c)"];
1617
s1_4[label="LogicalExpression:exit\nExpressionStatement:exit\nProgram:exit"];
1718
initial->s1_1->s1_2->s1_3->s1_4;
18-
s1_1->s1_4;
19-
s1_2->s1_4->final;
19+
s1_1->s1_3;
20+
s1_2->s1_4;
21+
s1_1->s1_4->final;
2022
}
2123
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*expected
2+
initial->s1_1->s1_2->s1_3->s1_4;
3+
s1_1->s1_3;
4+
s1_2->s1_4;
5+
s1_1->s1_4->final;
6+
*/
7+
(a && b) ?? c;
8+
9+
/*DOT
10+
digraph {
11+
node[shape=box,style="rounded,filled",fillcolor=white];
12+
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
13+
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
14+
s1_1[label="Program:enter\nExpressionStatement:enter\nLogicalExpression:enter\nLogicalExpression:enter\nIdentifier (a)"];
15+
s1_2[label="Identifier (b)\nLogicalExpression:exit"];
16+
s1_3[label="Identifier (c)"];
17+
s1_4[label="LogicalExpression:exit\nExpressionStatement:exit\nProgram:exit"];
18+
initial->s1_1->s1_2->s1_3->s1_4;
19+
s1_1->s1_3;
20+
s1_2->s1_4;
21+
s1_1->s1_4->final;
22+
}*/

tests/fixtures/code-path-analysis/logical--if-mix-and-qq-1.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/*expected
22
initial->s1_1->s1_2->s1_3->s1_4->s1_6;
3-
s1_1->s1_5->s1_6;
3+
s1_1->s1_3;
44
s1_2->s1_4;
5-
s1_3->s1_5;
5+
s1_3->s1_5->s1_6;
6+
s1_1->s1_5;
67
s1_2->s1_5;
78
s1_6->final;
89
*/
@@ -24,9 +25,10 @@ digraph {
2425
s1_6[label="IfStatement:exit\nProgram:exit"];
2526
s1_5[label="BlockStatement\nExpressionStatement\nCallExpression\nIdentifier (bar)\nIdentifier:exit (bar)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"];
2627
initial->s1_1->s1_2->s1_3->s1_4->s1_6;
27-
s1_1->s1_5->s1_6;
28+
s1_1->s1_3;
2829
s1_2->s1_4;
29-
s1_3->s1_5;
30+
s1_3->s1_5->s1_6;
31+
s1_1->s1_5;
3032
s1_2->s1_5;
3133
s1_6->final;
3234
}

0 commit comments

Comments
 (0)