Skip to content

Commit 6668a31

Browse files
brad4dcopybara-github
authored andcommitted
Improve NodeUtil support for optional chains
* Rename some methods to clarify the difference between a chain segment and the entire chain. * Add a method to detect the end of an entire optional chain. PiperOrigin-RevId: 321811850
1 parent 051f246 commit 6668a31

File tree

4 files changed

+81
-41
lines changed

4 files changed

+81
-41
lines changed

src/com/google/javascript/jscomp/ExpressionDecomposer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ private static Node findNonconditionalParent(Node subExpression, Node expression
337337
// result is currently `x?.y.z(subExpression)`, but we want it to be the full sub-chain
338338
// containing subExpression
339339
// `x?.y.z(subExpression).p.q`
340-
result = NodeUtil.getEndOfOptChain(result);
340+
result = NodeUtil.getEndOfOptChainSegment(result);
341341
}
342342

343343
return result;
@@ -456,7 +456,7 @@ private void extractOptionalChain(Node optChainNode, Node injectionPoint) {
456456
checkState(NodeUtil.isOptChainNode(optChainNode), optChainNode);
457457

458458
// find the start of the chain & convert it to non-optional
459-
final Node optChainStart = NodeUtil.getStartOfOptChain(optChainNode);
459+
final Node optChainStart = NodeUtil.getStartOfOptChainSegment(optChainNode);
460460
optionalToNonOptionalChain(optChainStart);
461461

462462
// Identify or create the statement that will need to go into the if-statement body

src/com/google/javascript/jscomp/NodeUtil.java

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,49 +1950,79 @@ public static boolean isOptChainNode(Node n) {
19501950
* @param n A node in an optional chain
19511951
* @return The start of the optional chain that `n` is part of.
19521952
*/
1953-
static Node getStartOfOptChain(Node n) {
1953+
static Node getStartOfOptChainSegment(Node n) {
19541954
checkState(NodeUtil.isOptChainNode(n), n);
19551955
if (n.isOptionalChainStart()) {
19561956
return n;
19571957
}
1958-
return getStartOfOptChain(n.getFirstChild());
1958+
return getStartOfOptChainSegment(n.getFirstChild());
19591959
}
19601960

19611961
/**
1962-
* Find the end of the optional chain.
1962+
* Find the end of an optional chain segment.
1963+
*
1964+
* <p>Each `?.` ends one segment and starts another.
19631965
*
19641966
* <p>Examples
19651967
*
19661968
* <pre>
1967-
* a?.b.c.d // end is b.c
1968-
* a?.b.c?.d // given a?.b end is b.c
1969-
* (a?.b.c).d // given a?.b end is b.c
1969+
* a?.b.c.d // end is the node with children `a?.b.c` and `d`
1970+
* a?.b.c?.d // given a?.b end is the node with children `a?.b` and `c`
1971+
* a?.b.c?.d // given a?.b.c end is the node with children `a?.b.c` and `d`
1972+
* (a?.b.c).d // given a?.b end is the node with children `a?.b` and `c`
19701973
* </pre>
19711974
*
19721975
* @param n A node in an optional chain
19731976
* @return The end of the optional chain that `n` is part of.
19741977
*/
1975-
static Node getEndOfOptChain(Node n) {
1978+
static Node getEndOfOptChainSegment(Node n) {
19761979
checkState(NodeUtil.isOptChainNode(n), n);
1977-
if (isEndOfOptChain(n)) {
1980+
if (isEndOfOptChainSegment(n)) {
19781981
return n;
19791982
} else {
1980-
return getEndOfOptChain(n.getParent());
1983+
return getEndOfOptChainSegment(n.getParent());
19811984
}
19821985
}
19831986

1984-
static boolean isEndOfOptChain(Node n) {
1987+
/**
1988+
* Is this node the final node of a full optional chain?
1989+
*
1990+
* <p>e.g. for `a?.b.c?.d` this method returns true only for the Node with children `a?.b.c` and
1991+
* `d`. That node is the end of the whole chain, and also represents the whole chain in the AST.
1992+
*/
1993+
static boolean isEndOfFullOptChain(Node n) {
19851994
if (NodeUtil.isOptChainNode(n)) {
19861995
Node parent = n.getParent();
1987-
return parent == null
1988-
|| !NodeUtil.isOptChainNode(parent)
1989-
|| parent.isOptionalChainStart()
1990-
// if an optional chain node `n` is also a call's arg or an index into a GETELEM, it will
1991-
// be the end of its chain
1992-
// e.g. In `a?.(x?.y.z)`or `a?.[x?.y.z]`, the `x?.y.z` is the end node
1993-
|| !n.isFirstChildOf(parent);
1996+
// the chain continues if this is the first child of another optional chain node
1997+
return !(NodeUtil.isOptChainNode(parent) && n.isFirstChildOf(parent));
19941998
} else {
1999+
// not even an optional chain node
2000+
return false;
2001+
}
2002+
}
2003+
2004+
/**
2005+
* Is this node the end of an optional chain segment?
2006+
*
2007+
* <p>Each `?.` begins a new segment and ends the previous one, if any. The end of the whole chain
2008+
* is also the end of its final segment.
2009+
*/
2010+
static boolean isEndOfOptChainSegment(Node n) {
2011+
if (!NodeUtil.isOptChainNode(n)) {
19952012
return false;
2013+
} else {
2014+
Node parent = n.getParent();
2015+
// Check for null so this method will work for a disconnected Node.
2016+
if (parent != null && n.isFirstChildOf(parent) && NodeUtil.isOptChainNode(parent)) {
2017+
// The parent is a continuation of this optional chain.
2018+
// If it starts a new segment, then this node is the end of a segment
2019+
return parent.isOptionalChainStart();
2020+
} else {
2021+
// The parent doesn't continue this node's optional chain, though it might be part of a
2022+
// different one.
2023+
// e.g. in `a?.(x?.y.z)` the parent of `x?.y.z` is part of a different optional chain.
2024+
return true;
2025+
}
19962026
}
19972027
}
19982028

src/com/google/javascript/jscomp/TypeInference.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,9 +2336,9 @@ private static class OptChainInfo {
23362336
private FlowScope traverseOptChain(Node n, FlowScope scope) {
23372337
checkArgument(NodeUtil.isOptChainNode(n));
23382338

2339-
if (NodeUtil.isEndOfOptChain(n)) {
2339+
if (NodeUtil.isEndOfOptChainSegment(n)) {
23402340
// Create new optional chain tracking object and push it onto the stack.
2341-
final Node startOfChain = NodeUtil.getStartOfOptChain(n);
2341+
final Node startOfChain = NodeUtil.getStartOfOptChainSegment(n);
23422342
OptChainInfo optChainInfo = new OptChainInfo(n, startOfChain);
23432343
optChainArrayDeque.addFirst(optChainInfo);
23442344
}
@@ -2370,10 +2370,10 @@ private FlowScope traverseOptChain(Node n, FlowScope scope) {
23702370
n.setJSType(unknownType);
23712371
}
23722372

2373-
if (NodeUtil.isEndOfOptChain(n)) {
2373+
if (NodeUtil.isEndOfOptChainSegment(n)) {
23742374
// Use the startNode's type to selectively join the executed scope with the unexecuted scope,
23752375
// and update the type assigned to `n` in `setXAfterChildrenTraversed()`
2376-
final Node startOfChain = NodeUtil.getStartOfOptChain(n);
2376+
final Node startOfChain = NodeUtil.getStartOfOptChainSegment(n);
23772377

23782378
// Pop the stack to obtain the current chain.
23792379
OptChainInfo currentChain = optChainArrayDeque.removeFirst();

test/com/google/javascript/jscomp/NodeUtilTest.java

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3729,7 +3729,7 @@ public void isStartOfChain() {
37293729
// `expr?.prop`
37303730
Node optChainGet = IR.startOptChainGetprop(IR.name("expr"), IR.string("prop"));
37313731

3732-
assertThat(NodeUtil.getStartOfOptChain(optChainGet)).isEqualTo(optChainGet);
3732+
assertThat(NodeUtil.getStartOfOptChainSegment(optChainGet)).isEqualTo(optChainGet);
37333733
}
37343734

37353735
@Test
@@ -3738,7 +3738,7 @@ public void shortChain() {
37383738
Node innerGetProp = IR.startOptChainGetprop(IR.name("expr"), IR.string("pro1"));
37393739
Node outterGetProp = IR.continueOptChainGetprop(innerGetProp, IR.string("prop2"));
37403740

3741-
assertThat(NodeUtil.getStartOfOptChain(outterGetProp)).isEqualTo(innerGetProp);
3741+
assertThat(NodeUtil.getStartOfOptChainSegment(outterGetProp)).isEqualTo(innerGetProp);
37423742
}
37433743

37443744
@Test
@@ -3750,8 +3750,8 @@ public void mixedChain() {
37503750
Node optCall = IR.continueOptChainCall(optGetProp);
37513751
Node optGetElem = IR.continueOptChainGetelem(optCall, IR.name("prop3"));
37523752

3753-
assertThat(NodeUtil.getStartOfOptChain(optGetElem)).isEqualTo(optGetProp);
3754-
assertThat(NodeUtil.getStartOfOptChain(optCall)).isEqualTo(optGetProp);
3753+
assertThat(NodeUtil.getStartOfOptChainSegment(optGetElem)).isEqualTo(optGetProp);
3754+
assertThat(NodeUtil.getStartOfOptChainSegment(optCall)).isEqualTo(optGetProp);
37553755
}
37563756
}
37573757

@@ -3763,7 +3763,7 @@ public void isEndOfChain() {
37633763
// `expr?.prop`
37643764
Node optChainGet = IR.startOptChainGetprop(IR.name("expr"), IR.string("prop"));
37653765

3766-
assertThat(NodeUtil.getEndOfOptChain(optChainGet)).isEqualTo(optChainGet);
3766+
assertThat(NodeUtil.getEndOfOptChainSegment(optChainGet)).isEqualTo(optChainGet);
37673767
}
37683768

37693769
@Test
@@ -3775,27 +3775,32 @@ public void isEndOfChain_call_innerChain() {
37753775
Node innerOptChain = optChainCall.getLastChild(); // `x?.y`
37763776
assertThat(innerOptChain.isOptChainGetProp()).isTrue();
37773777
assertThat(innerOptChain.isOptionalChainStart()).isTrue();
3778-
assertThat(NodeUtil.isEndOfOptChain(innerOptChain)).isTrue();
3778+
assertThat(NodeUtil.isEndOfOptChainSegment(innerOptChain)).isTrue();
3779+
assertThat(NodeUtil.isEndOfFullOptChain(innerOptChain)).isTrue();
37793780
}
37803781

37813782
@Test
37823783
public void isEndOfChain_getProp_innerChain() {
37833784
Node optChainGetProp = parseExpr("a?.b.x?.y");
37843785
assertThat(optChainGetProp.isOptChainGetProp()).isTrue();
37853786
assertThat(optChainGetProp.isOptionalChainStart()).isTrue();
3786-
assertThat(NodeUtil.isEndOfOptChain(optChainGetProp)).isTrue();
3787+
assertThat(NodeUtil.isEndOfOptChainSegment(optChainGetProp)).isTrue();
3788+
assertThat(NodeUtil.isEndOfFullOptChain(optChainGetProp)).isTrue();
37873789

3788-
// Check that `a?.b.x` is not the start of optChain, but it ends the chain `a?.b`.
3790+
// Check that `a?.b.x` is not the start of the optional chain, but it ends the segment that
3791+
// starts with `a?.b`.
37893792
Node innerOptChain = optChainGetProp.getFirstChild(); // `a?.b.x`
37903793
assertThat(innerOptChain.isOptChainGetProp()).isTrue();
37913794
assertThat(innerOptChain.isOptionalChainStart()).isFalse();
3792-
assertThat(NodeUtil.isEndOfOptChain(innerOptChain)).isTrue();
3795+
assertThat(NodeUtil.isEndOfOptChainSegment(innerOptChain)).isTrue();
3796+
assertThat(NodeUtil.isEndOfFullOptChain(innerOptChain)).isFalse();
37933797

37943798
// Check that `a?.b` is the start of optChain but not the end
37953799
Node innerMostOptChain = innerOptChain.getFirstChild(); // `a?.b`
37963800
assertThat(innerMostOptChain.isOptChainGetProp()).isTrue();
37973801
assertThat(innerMostOptChain.isOptionalChainStart()).isTrue();
3798-
assertThat(NodeUtil.isEndOfOptChain(innerMostOptChain)).isFalse();
3802+
assertThat(NodeUtil.isEndOfOptChainSegment(innerMostOptChain)).isFalse();
3803+
assertThat(NodeUtil.isEndOfFullOptChain(innerMostOptChain)).isFalse();
37993804
}
38003805

38013806
@Test
@@ -3808,7 +3813,8 @@ public void isEndOfChain_call_innerChain2() {
38083813
Node innerOptChain = optChainCall.getLastChild();
38093814
assertThat(innerOptChain.isOptChainGetProp()).isTrue();
38103815
assertThat(innerOptChain.isOptionalChainStart()).isTrue();
3811-
assertThat(NodeUtil.isEndOfOptChain(innerOptChain)).isTrue();
3816+
assertThat(NodeUtil.isEndOfOptChainSegment(innerOptChain)).isTrue();
3817+
assertThat(NodeUtil.isEndOfFullOptChain(innerOptChain)).isTrue();
38123818
}
38133819

38143820
@Test
@@ -3821,13 +3827,15 @@ public void isEndOfChain_callAndGetProp_innerChain() {
38213827
Node optChainCall = optChainGetProp.getFirstChild();
38223828
assertThat(optChainCall.isOptChainCall()).isTrue();
38233829
assertThat(optChainCall.isOptionalChainStart()).isFalse();
3824-
assertThat(NodeUtil.isEndOfOptChain(optChainCall)).isFalse();
3830+
assertThat(NodeUtil.isEndOfOptChainSegment(optChainCall)).isFalse();
3831+
assertThat(NodeUtil.isEndOfFullOptChain(optChainCall)).isFalse();
38253832

38263833
// Check that `x?.y` is the start and end of the chain
38273834
Node innerOptChain = optChainCall.getLastChild();
38283835
assertThat(innerOptChain.isOptChainGetProp()).isTrue();
38293836
assertThat(innerOptChain.isOptionalChainStart()).isTrue();
3830-
assertThat(NodeUtil.isEndOfOptChain(innerOptChain)).isTrue();
3837+
assertThat(NodeUtil.isEndOfOptChainSegment(innerOptChain)).isTrue();
3838+
assertThat(NodeUtil.isEndOfFullOptChain(innerOptChain)).isTrue();
38313839
}
38323840

38333841
@Test
@@ -3840,13 +3848,15 @@ public void isEndOfChain_callAndGetProp_innerChain_multipleInnerArgs() {
38403848
Node optChainCall = optChainGetProp.getFirstChild();
38413849
assertThat(optChainCall.isOptChainCall()).isTrue();
38423850
assertThat(optChainCall.isOptionalChainStart()).isFalse();
3843-
assertThat(NodeUtil.isEndOfOptChain(optChainCall)).isFalse();
3851+
assertThat(NodeUtil.isEndOfOptChainSegment(optChainCall)).isFalse();
3852+
assertThat(NodeUtil.isEndOfFullOptChain(optChainCall)).isFalse();
38443853

38453854
// Check that `x?.y` is the start and end of its chain
38463855
Node innerOptChain = optChainCall.getSecondChild();
38473856
assertThat(innerOptChain.isOptChainGetProp()).isTrue();
38483857
assertThat(innerOptChain.isOptionalChainStart()).isTrue();
3849-
assertThat(NodeUtil.isEndOfOptChain(innerOptChain)).isTrue();
3858+
assertThat(NodeUtil.isEndOfOptChainSegment(innerOptChain)).isTrue();
3859+
assertThat(NodeUtil.isEndOfFullOptChain(innerOptChain)).isTrue();
38503860
}
38513861

38523862
@Test
@@ -3855,7 +3865,7 @@ public void shortChain() {
38553865
Node innerGetProp = IR.startOptChainGetprop(IR.name("expr"), IR.string("pro1"));
38563866
Node outerGetProp = IR.continueOptChainGetprop(innerGetProp, IR.string("prop2"));
38573867

3858-
assertThat(NodeUtil.getEndOfOptChain(innerGetProp)).isEqualTo(outerGetProp);
3868+
assertThat(NodeUtil.getEndOfOptChainSegment(innerGetProp)).isEqualTo(outerGetProp);
38593869
}
38603870

38613871
@Test
@@ -3867,7 +3877,7 @@ public void twoChains() {
38673877
Node optCall = IR.continueOptChainCall(optGetProp);
38683878
IR.startOptChainGetelem(optCall, IR.name("prop3"));
38693879

3870-
assertThat(NodeUtil.getEndOfOptChain(startOptGetProp)).isEqualTo(optCall);
3880+
assertThat(NodeUtil.getEndOfOptChainSegment(startOptGetProp)).isEqualTo(optCall);
38713881
}
38723882

38733883
@Test
@@ -3877,7 +3887,7 @@ public void breakingOutOfOptChain() {
38773887
Node optGetProp = IR.continueOptChainGetprop(startOptGetProp, IR.string("prop2"));
38783888
IR.getprop(optGetProp, IR.string("prop3"));
38793889

3880-
assertThat(NodeUtil.getEndOfOptChain(startOptGetProp)).isEqualTo(optGetProp);
3890+
assertThat(NodeUtil.getEndOfOptChainSegment(startOptGetProp)).isEqualTo(optGetProp);
38813891
}
38823892
}
38833893

0 commit comments

Comments
 (0)