Skip to content

Commit

Permalink
JS-230 Java ESTree AST should correctly build array expressions and p…
Browse files Browse the repository at this point in the history
…atterns with empty elements (#4753)
  • Loading branch information
quentin-jaquier-sonarsource authored Jul 15, 2024
1 parent 7499f54 commit 8480767
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 17 deletions.
18 changes: 12 additions & 6 deletions packages/jsts/src/parsers/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,9 @@ export function visitNode(node: estree.BaseNodeWithoutComments | undefined | nul
}

function visitArrayPattern(node: estree.ArrayPattern) {
// If the elements ever contain null together with other nodes, protobuf will fail to serialize the array.
// It is unclear from the estree documentation if this is possible, but as it is from a type perspective, we prefer to be safe.
// When an entry is empty, it is represented as null in the array.
return {
elements: node.elements.filter(e => e !== null).map(visitNode),
elements: node.elements.map(visitArrayElement),
};
}

Expand Down Expand Up @@ -524,10 +523,17 @@ export function visitNode(node: estree.BaseNodeWithoutComments | undefined | nul
}

function visitArrayExpression(node: estree.ArrayExpression) {
// If the elements ever contain null together with other nodes, protobuf will fail to serialize the array.
// It is unclear from the estree documentation if this is possible, but as it is from a type perspective, we prefer to be safe.
// When an entry is empty, it is represented as null in the array.
return {
elements: node.elements.filter(e => e !== null).map(visitNode),
elements: node.elements.map(visitArrayElement),
};
}

function visitArrayElement(
element: estree.Pattern | estree.Expression | estree.SpreadElement | null,
) {
return {
element: visitNode(element),
};
}

Expand Down
7 changes: 5 additions & 2 deletions packages/jsts/src/parsers/estree.proto
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ message RestElement {
Node argument = 1;
}
message ArrayPattern {
repeated Node elements = 1;
repeated ArrayElement elements = 1;
}
message ObjectPattern {
repeated Node properties = 1;
Expand Down Expand Up @@ -332,7 +332,10 @@ message ArrowFunctionExpression {
optional bool async = 5;
}
message ArrayExpression {
repeated Node elements = 1;
repeated ArrayElement elements = 1;
}
message ArrayElement {
optional Node element = 1;
}
message ClassDeclaration {
optional Node id = 1;
Expand Down
2 changes: 2 additions & 0 deletions packages/jsts/tests/parsers/fixtures/ast/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ const [a1, b1] = [1, 2];
const [a2, ...b2] = [1, 2, 3];
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
const arrWithEmptyElements = [, , 5];
const [, pattern, withEmpty, , elements] = [1, 2, 3, 4, 5, 6]

let a3;
a3 = 1, 2, 3;
Expand Down
Binary file removed serialized.proto
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ public sealed interface DirectiveOrModuleDeclarationOrStatement extends Node {}
public sealed interface ExpressionOrPattern extends Node {}
public sealed interface IdentifierOrLiteral extends Node {}

public record ArrayExpression(Location loc, List<ExpressionOrSpreadElement> elements) implements Expression {}
public record ArrayPattern(Location loc, List<Pattern> elements) implements Pattern {}
public record ArrayExpression(Location loc, List<Optional<ExpressionOrSpreadElement>> elements) implements Expression {}
public record ArrayPattern(Location loc, List<Optional<Pattern>> elements) implements Pattern {}
public record ArrowFunctionExpression(Location loc, boolean expression, BlockStatementOrExpression body, List<Pattern> params, boolean generator, boolean async) implements Expression {}
public record AssignmentExpression(Location loc, AssignmentOperator operator, MemberExpressionOrPattern left, Expression right) implements Expression {}
public record AssignmentPattern(Location loc, Pattern left, Expression right) implements Pattern {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.List;
import java.util.Optional;
import org.sonar.plugins.javascript.api.estree.ESTree;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayElement;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayExpression;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayPattern;
import org.sonar.plugins.javascript.bridge.protobuf.ArrowFunctionExpression;
Expand Down Expand Up @@ -311,7 +312,11 @@ private static ESTree.RestElement fromRestElementType(Node node) {
private static ESTree.ArrayPattern fromArrayPatternType(Node node) {
ArrayPattern arrayPattern = node.getArrayPattern();
return new ESTree.ArrayPattern(fromLocation(node.getLoc()),
from(arrayPattern.getElementsList(), ESTree.Pattern.class));
arrayPattern.getElementsList().stream().map(ESTreeFactory::fromArrayPatternElement).toList());
}

private static Optional<ESTree.Pattern> fromArrayPatternElement(ArrayElement element) {
return element.hasElement() ? Optional.of(from(element.getElement(), ESTree.Pattern.class)) : Optional.empty();
}

private static ESTree.ObjectPattern fromObjectPatternType(Node node) {
Expand Down Expand Up @@ -467,7 +472,11 @@ private static ESTree.ArrowFunctionExpression fromArrowFunctionExpressionType(No
private static ESTree.ArrayExpression fromArrayExpressionType(Node node) {
ArrayExpression arrayExpression = node.getArrayExpression();
return new ESTree.ArrayExpression(fromLocation(node.getLoc()),
from(arrayExpression.getElementsList(), ESTree.ExpressionOrSpreadElement.class));
arrayExpression.getElementsList().stream().map(ESTreeFactory::fromArrayExpressionElement).toList());
}

private static Optional<ESTree.ExpressionOrSpreadElement> fromArrayExpressionElement(ArrayElement element) {
return element.hasElement() ? Optional.of(from(element.getElement(), ESTree.ExpressionOrSpreadElement.class)) : Optional.empty();
}

private static ESTree.ClassDeclaration fromClassDeclarationType(Node node) {
Expand Down
7 changes: 5 additions & 2 deletions sonar-plugin/bridge/src/main/protobuf/estree.proto
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ message RestElement {
Node argument = 1;
}
message ArrayPattern {
repeated Node elements = 1;
repeated ArrayElement elements = 1;
}
message ObjectPattern {
repeated Node properties = 1;
Expand Down Expand Up @@ -332,7 +332,10 @@ message ArrowFunctionExpression {
optional bool async = 5;
}
message ArrayExpression {
repeated Node elements = 1;
repeated ArrayElement elements = 1;
}
message ArrayElement {
optional Node element = 1;
}
message ClassDeclaration {
optional Node id = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.sonar.plugins.javascript.api.estree.ESTree;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayElement;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayExpression;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayPattern;
import org.sonar.plugins.javascript.bridge.protobuf.AssignmentExpression;
import org.sonar.plugins.javascript.bridge.protobuf.AssignmentPattern;
import org.sonar.plugins.javascript.bridge.protobuf.BinaryExpression;
Expand Down Expand Up @@ -71,7 +75,7 @@ void should_create_nodes_from_serialized_data() throws IOException {
ESTree.Node root = ESTreeFactory.from(node, ESTree.Node.class);
assertThat(root).isInstanceOf(ESTree.Program.class);
ESTree.Program program = (ESTree.Program) root;
assertThat(program.body()).hasSize(52);
assertThat(program.body()).hasSize(55);
// Assert a few nodes.
assertThat(program.body().get(0)).isInstanceOfSatisfying(ESTree.VariableDeclaration.class, variableDeclaration -> {
assertThat(variableDeclaration.declarations()).hasSize(1);
Expand All @@ -90,6 +94,18 @@ void should_create_nodes_from_serialized_data() throws IOException {
assertThat(ifStatement.consequent()).isInstanceOf(ESTree.BlockStatement.class);
assertThat(ifStatement.alternate()).isEmpty();
});

// Source code: [, , 5]
assertThat(program.body().get(42)).isInstanceOfSatisfying(ESTree.VariableDeclaration.class, declaration -> {
Optional<ESTree.Expression> initializer = declaration.declarations().get(0).init();
assertThat(initializer).isPresent();
assertThat(initializer.get()).isInstanceOfSatisfying(ESTree.ArrayExpression.class, arrayExpression -> {
assertThat(arrayExpression.elements()).hasSize(3);
assertThat(arrayExpression.elements().get(0)).isEmpty();
assertThat(arrayExpression.elements().get(1)).isEmpty();
assertThat(arrayExpression.elements().get(2)).isNotEmpty();
});
});
}

@Test
Expand Down Expand Up @@ -641,6 +657,46 @@ void directive_can_be_in_block_statement() {
});
}

@Test
void array_expression_can_contain_empty_elements() {
Node protobufNode = Node.newBuilder()
.setType(NodeType.ArrayExpressionType)
.setArrayExpression(ArrayExpression.newBuilder()
.addElements(ArrayElement.newBuilder().build())
.addElements(ArrayElement.newBuilder().setElement(Node.newBuilder().setType(NodeType.ThisExpressionType).build()).build())
.addElements(ArrayElement.newBuilder().build())
.build())
.build();

ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
assertThat(estree).isInstanceOfSatisfying(ESTree.ArrayExpression.class, array -> {
assertThat(array.elements()).hasSize(3);
assertThat(array.elements().get(0)).isEmpty();
assertThat(array.elements().get(1).get()).isInstanceOf(ESTree.ThisExpression.class);
assertThat(array.elements().get(2)).isEmpty();
});
}

@Test
void array_pattern_can_contain_empty_elements() {
Node protobufNode = Node.newBuilder()
.setType(NodeType.ArrayPatternType)
.setArrayPattern(ArrayPattern.newBuilder()
.addElements(ArrayElement.newBuilder().build())
.addElements(ArrayElement.newBuilder().setElement(Node.newBuilder().setType(NodeType.IdentifierType).build()).build())
.addElements(ArrayElement.newBuilder().build())
.build())
.build();

ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
assertThat(estree).isInstanceOfSatisfying(ESTree.ArrayPattern.class, array -> {
assertThat(array.elements()).hasSize(3);
assertThat(array.elements().get(0)).isEmpty();
assertThat(array.elements().get(1).get()).isInstanceOf(ESTree.Identifier.class);
assertThat(array.elements().get(2)).isEmpty();
});
}

@Test
void throw_exception_from_unrecognized_type() {
Node protobufNode = Node.newBuilder()
Expand Down
Binary file modified sonar-plugin/bridge/src/test/resources/files/serialized.proto
Binary file not shown.
7 changes: 5 additions & 2 deletions tools/estree/output/estree.proto
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ message RestElement {
Node argument = 1;
}
message ArrayPattern {
repeated Node elements = 1;
repeated ArrayElement elements = 1;
}
message ObjectPattern {
repeated Node properties = 1;
Expand Down Expand Up @@ -332,7 +332,10 @@ message ArrowFunctionExpression {
optional bool async = 5;
}
message ArrayExpression {
repeated Node elements = 1;
repeated ArrayElement elements = 1;
}
message ArrayElement {
optional Node element = 1;
}
message ClassDeclaration {
optional Node id = 1;
Expand Down

0 comments on commit 8480767

Please sign in to comment.