Skip to content

Commit

Permalink
SONARJAVA-4119 Handle new switch patterns in CFG
Browse files Browse the repository at this point in the history
  • Loading branch information
Wohops committed Jan 17, 2022
1 parent cf7b742 commit 0f8ee52
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.foo;

/**
* Requires Java 17 (JEP-406) preview feature
*/
public class SwitchWithPatterns {

public static void main(String[] args) {
System.out.println(String.format("%d,%d,%d",
switch_array_null_pattern(null),
switch_array_null_pattern(new A()),
switch_array_null_pattern(new A[10])));

System.out.println(String.format("%d,%d,%d",
switch_array_default_null_pattern(null),
switch_array_default_null_pattern(new A()),
switch_array_default_null_pattern(new A[10])));
}

static Object foo(Object o) {
return switch (o) {
case default -> o;
};
}

static int switch_array_null_pattern(Object o) {
return switch (o) {
case Object[] arr -> arr.length;
default -> -1;
case null -> 42;
};
}

static int switch_array_default_null_pattern(Object o) {
return switch (o) {
case Object[] arr -> arr.length;
case default, null -> 42;
};
}

static String switch_sealed_class_minimum(Shape shape) {
return switch (shape) {
case Triangle t -> "triangle";
case Rectangle r -> "rectangle";
};
}

static String switch_sealed_class_null_default_sub_classes(Shape shape) {
return switch (shape) {
case null -> "null case";
case Triangle t -> String.format("triangle (%d,%d,%d)", t.a(), t.b(), t.c());
case Rectangle r && r.volume() > 42 -> String.format("big rectangle of volume %d!", r.volume());
case Square s -> "Square!";
case Rectangle r -> String.format("Rectangle (%d,%d)", r.base, r.height);
default -> "default case";
};
}

public sealed interface Shape permits Rectangle,Triangle {
default int volume() { return 0; }
}

public static non-sealed class Rectangle implements Shape {
private int base, height;
Rectangle(int base, int height) { this.base = base; this.height = height; }
}

public static final class Square extends Rectangle {
Square(int side) { super(side, side); }
}

public static record Triangle(int a, int b, int c) implements Shape {}
}
26 changes: 25 additions & 1 deletion java-frontend/src/main/java/org/sonar/java/cfg/CFG.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.ForEachStatement;
import org.sonar.plugins.java.api.tree.ForStatementTree;
import org.sonar.plugins.java.api.tree.GuardedPatternTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.IfStatementTree;
import org.sonar.plugins.java.api.tree.InstanceOfTree;
Expand All @@ -67,6 +68,7 @@
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.NewArrayTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.NullPatternTree;
import org.sonar.plugins.java.api.tree.ParenthesizedTree;
import org.sonar.plugins.java.api.tree.PatternInstanceOfTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
Expand All @@ -79,6 +81,7 @@
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TryStatementTree;
import org.sonar.plugins.java.api.tree.TypeCastTree;
import org.sonar.plugins.java.api.tree.TypePatternTree;
import org.sonar.plugins.java.api.tree.UnaryExpressionTree;
import org.sonar.plugins.java.api.tree.VariableTree;
import org.sonar.plugins.java.api.tree.WhileStatementTree;
Expand Down Expand Up @@ -610,6 +613,25 @@ private void build(Tree tree) {
case NULL_LITERAL:
currentBlock.elements.add(tree);
break;
case NULL_PATTERN:
currentBlock.elements.add(((NullPatternTree) tree).nullLiteral());
currentBlock.elements.add(tree);
break;
case TYPE_PATTERN:
buildVariable(((TypePatternTree) tree).patternVariable());
currentBlock.elements.add(tree);
break;
case GUARDED_PATTERN:
GuardedPatternTree guardedPatternTree = (GuardedPatternTree) tree;
// reverted order
build(guardedPatternTree.expression());
build(guardedPatternTree.pattern());
currentBlock.elements.add(tree);
break;
case DEFAULT_PATTERN:
// do nothing - handled when building the switch
currentBlock.elements.add(tree);
break;
default:
throw new UnsupportedOperationException(tree.kind().name() + " " + ((JavaTree) tree).getLine());
}
Expand Down Expand Up @@ -815,7 +837,9 @@ private static boolean switchWithoutFallThrough(SwitchTree switchTree) {
}

private static boolean containsDefaultCase(List<CaseLabelTree> labels) {
return labels.stream().anyMatch(caseLabel -> "default".equals(caseLabel.caseOrDefaultKeyword().text()));
return labels.stream().anyMatch(caseLabel -> "default".equals(caseLabel.caseOrDefaultKeyword().text())
// JDK 17 preview feature
|| caseLabel.expressions().stream().anyMatch(expr -> expr.is(Tree.Kind.DEFAULT_PATTERN)));
}

private void buildBreakStatement(BreakStatementTree tree) {
Expand Down
53 changes: 53 additions & 0 deletions java-frontend/src/test/java/org/sonar/java/cfg/CFGTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
import static org.sonar.plugins.java.api.tree.Tree.Kind.VARIABLE;
import static org.sonar.plugins.java.api.tree.Tree.Kind.WHILE_STATEMENT;
import static org.sonar.plugins.java.api.tree.Tree.Kind.YIELD_STATEMENT;
import static org.sonar.plugins.java.api.tree.Tree.Kind.DEFAULT_PATTERN;
import static org.sonar.plugins.java.api.tree.Tree.Kind.TYPE_PATTERN;
import static org.sonar.plugins.java.api.tree.Tree.Kind.NULL_PATTERN;
import static org.sonar.plugins.java.api.tree.Tree.Kind.GUARDED_PATTERN;
import static org.sonar.plugins.java.api.tree.Tree.Kind.GREATER_THAN;

class CFGTest {

Expand Down Expand Up @@ -362,8 +367,13 @@ public ElementChecker(final Tree.Kind kind) {
case ARRAY_ACCESS_EXPRESSION:
case LOGICAL_COMPLEMENT:
case MULTIPLY_ASSIGNMENT:
case UNARY_MINUS:
case PLUS:
case CASE_GROUP:
case NULL_PATTERN:
case TYPE_PATTERN:
case GUARDED_PATTERN:
case DEFAULT_PATTERN:
break;
default:
throw new IllegalArgumentException("Unsupported element kind: " + kind);
Expand Down Expand Up @@ -959,6 +969,49 @@ void switch_expression_with_fallthrough() {
cfgChecker.check(cfg);
}

// FIXME add tests for jdk 17
@Test
void switch_with_pattern() {
final CFG cfg = buildCFG("static int switch_array_default_null_pattern(Object o) {\n"
+ " return switch (o) {\n"
// array type pattern
+ " case Object[] arr -> arr.length;\n"
// guarded pattern
+ " case Rectangle r && r.volume() > 42 -> -1;\n"
// default and null pattern
+ " case default, null -> 42;\n"
+ " };\n"
+ " }");
final CFGChecker cfgChecker = checker(
block(
element(Tree.Kind.CASE_GROUP),
element(IDENTIFIER, "arr"),
element(MEMBER_SELECT)).hasCaseGroup().terminator(Tree.Kind.YIELD_STATEMENT).successors(1),
block(
element(Tree.Kind.CASE_GROUP),
element(INT_LITERAL, 1),
element(Tree.Kind.UNARY_MINUS)).hasCaseGroup().terminator(Tree.Kind.YIELD_STATEMENT).successors(1),
block(
element(Tree.Kind.CASE_GROUP),
element(INT_LITERAL, 42)).hasCaseGroup().terminator(Tree.Kind.YIELD_STATEMENT).successors(1),
block(
element(IDENTIFIER, "o"),
element(TYPE_PATTERN),
element(VARIABLE, "arr"),
element(GUARDED_PATTERN),
element(TYPE_PATTERN),
element(VARIABLE, "r"),
element(IDENTIFIER, "r"),
element(METHOD_INVOCATION),
element(INT_LITERAL, 42),
element(GREATER_THAN),
element(DEFAULT_PATTERN),
element(NULL_PATTERN),
element(NULL_LITERAL)).terminator(SWITCH_EXPRESSION).successors(3, 4, 5),
terminator(RETURN_STATEMENT).successors(0));
cfgChecker.check(cfg);
}

@Test
void return_statement() {
final CFG cfg = buildCFG("void fun(Object foo) { if(foo == null) return; }");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@ private void handleSwitch(CFG.Block programPosition, List<CaseGroupTree> caseGro

Map<CaseGroupTree, List<ProgramState.SymbolicValueSymbol>> caseValues = new HashMap<>();
for (CaseGroupTree caseGroup : ListUtils.reverse(caseGroups)) {
// FIXME this logic do not work with patterns from JDK17, as we need to consume the pattern adequately.
// The NULL_PATTERN can lead to NPE in the block, as it is equivalent to if (o == null) { ... }
// also, there is a good chance that the stack would be corrupted when patterns are used.
int numberOfCaseValues = caseGroup.labels()
.stream()
.map(CaseLabelTree::expressions)
Expand Down Expand Up @@ -734,6 +737,21 @@ private void visit(Tree tree, @Nullable Tree terminator) {
case SWITCH_EXPRESSION:
programState = programState.stackValue(constraintManager.createSymbolicValue(tree));
break;
case DEFAULT_PATTERN:
programState = programState.stackValue(constraintManager.createSymbolicValue(tree));
break;
case TYPE_PATTERN:
programState = programState.stackValue(constraintManager.createSymbolicValue(tree));
break;
case NULL_PATTERN:
// FIXME we could add a null constraint to the argument of the switch, since it should be
// considered being null in this branch
programState = programState.stackValue(constraintManager.createSymbolicValue(tree));
break;
case GUARDED_PATTERN:
// push a random SV, just to be consumed
programState = programState.stackValue(constraintManager.createSymbolicValue(tree));
break;
case ASSERT_STATEMENT:
executeAssertStatement(tree);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,20 @@ void switchExpression() {
.verifyIssues();
}

/**
* Checking that Java 17 patterns do not fail SE engine
* TODO once feature is final: make sure learned constraints propagate in branches
*/
@Test
void switchWithPatterns() {
SECheckVerifier.newVerifier()
.onFile(TestUtils.nonCompilingTestSourcesPath("symbolicexecution/engine/SwitchWithPatterns.java"))
.withChecks(seChecks())
.withClassPath(SETestUtils.CLASS_PATH)
.withJavaVersion(17)
.verifyNoIssues();
}

@Test
void different_exceptions_lead_to_different_program_states_with_catch_exception_block() {
Set<Type> encounteredExceptions = new HashSet<>();
Expand Down

0 comments on commit 0f8ee52

Please sign in to comment.