From 644e00b4891fb29b17cc65d4a51e1b0992f901a1 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Sun, 5 Nov 2023 19:53:09 +0100 Subject: [PATCH 01/29] OC-126: a list of things we need to test --- .../clover/JavaSyntax14CompilationTest.groovy | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy new file mode 100644 index 00000000..d79d16e7 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -0,0 +1,103 @@ +package com.atlassian.clover + +import com.atlassian.clover.util.JavaEnvUtils +import org.junit.Before +import org.junit.Test + +import static org.junit.Assume.assumeTrue + +/** + * The purpose of this test is to + * a) make sure the code compiles under a JDK14 + * b) make sure that when that code is instrumented, it still compiles + * c) test new language features introduced + */ +class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { + + protected File srcDir + + @Before + void setUp() throws Exception { + setUpProject() + srcDir = new File(mTestcasesSrcDir, "javasyntax14") + resetAntOutput() + } + + @Test + void testCaseWithColonCanUseBothBreakAndYield() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testSwitchWithCaseAndDefaultWithLambdas() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testSwitchWithCaseExpressionAndBlockReturningYield() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testSwitchWithCaseWithMultipleValues() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testCaseExpressionCanBeVoid() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testCaseExpressionCanReturnValue() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testSwitchIsAnExpression() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + // assignment to a variable + // argument of a method call + // part of an expression + } + + @Test + void testSwitchIsAStatement() { + // a standalone statement in method, instance initializer block, static block + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } + + @Test + void testSwitchCaseWithColonsMixedWithExpressions() { + // a standalone statement in method, instance initializer block, static block + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + } +} From f73046e3aa6cbb48d299c1c9eeb063cc5cfda6d5 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Sun, 5 Nov 2023 22:37:58 +0100 Subject: [PATCH 02/29] OC-126: more test samples --- .../clover/JavaSyntax14CompilationTest.groovy | 19 ++++-- .../Java14CaseAndDefaultWithLambdas.java | 48 +++++++++++++++ .../Java14CaseColonBreakYield.java | 60 +++++++++++++++++++ .../Java14CaseMixedYieldAndExpression.java | 16 +++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index d79d16e7..db43e3f5 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -24,19 +24,28 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void testCaseWithColonCanUseBothBreakAndYield() { + void caseWithColonCanUseBothBreakAndYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14CaseColonBreakYield.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + // using break in switch statements + + // using yield in switch expressions + } @Test - void testSwitchWithCaseAndDefaultWithLambdas() { + void switchExpressionWithCaseAndDefaultCanUseLambdas() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14CaseAndDefaultWithLambdas.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + // using lambdas in switch statements + // assert case + // assert default } @Test @@ -97,7 +106,7 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { // a standalone statement in method, instance initializer block, static block assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14CaseMixedYieldAndExpression.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) } } diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java b/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java new file mode 100644 index 00000000..4aabc2e5 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java @@ -0,0 +1,48 @@ +public class Java14CaseAndDefaultWithLambdas { + enum Colors { + R, G, B; + } + + enum EvenOrOdd { + EVEN, ODD, UNKNOWN; + } + + static void switchExpressionWithCasesOnly(Colors c) { + String name = switch (c) { + case R -> "red"; + case G -> "green"; + case B -> "blue"; + }; + } + + static void switchExpressionWithCasesReturningVoidOnly(Colors c) { + switch (c) { + case R -> System.out.println("red"); + case G -> System.out.println("green"); + case B -> System.out.println("blue"); + } + } + + static void switchExpressionWithCaseAndDefault(int i) { + EvenOrOdd evenOrOdd = switch (i) { + case 0 -> EvenOrOdd.EVEN; + case 1 -> EvenOrOdd.ODD; + default -> EvenOrOdd.UNKNOWN; + }; + } + + static void switchExpressionWithCaseAndDefaultReturningVoid(int i) { + switch (i) { + case 0 -> System.out.println(EvenOrOdd.EVEN); + case 1 -> System.out.println(EvenOrOdd.ODD); + default -> System.out.println(EvenOrOdd.UNKNOWN); + } + } + + public static void main(String[] args) { + switchExpressionWithCasesOnly(Colors.G); + switchExpressionWithCasesReturningVoidOnly(Colors.B); + switchExpressionWithCaseAndDefault(2); + switchExpressionWithCaseAndDefaultReturningVoid(0); + } +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java b/clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java new file mode 100644 index 00000000..a6d1a8fc --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java @@ -0,0 +1,60 @@ +public class Java14CaseColonBreakYield { + + static void yieldInSwitchExpression() { + // NOT ALLOWED - yield outside of switch expression + //int i = 0; + //switch (i) { + // case 0: + // yield 0; + // default: + // yield 1; + //}; + + // ALLOWED + int j = 0, k; + k = switch (j) { + case 0: + yield 10; + default: + yield 11; + }; + + // ALLOWED + foo(switch (k) { + case 10: + yield 20; + case 11: + yield 21; + default: + yield 22; + }); + } + + static void breakInSwitchStatement() { + // NOT ALLOWED - break out of switch expression is not allowed + //int i = 0, j; + //j = switch (i) { + // case 0: + // break; + // default: + // break; + //}; + + // ALLOWED + int k = 30; + switch (k) { + case 30: + k++; + break; + default: + break; + } + } + + static void foo(int i) { } + + public static void main(String[] args) { + yieldInSwitchExpression(); + breakInSwitchStatement(); + } +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java b/clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java new file mode 100644 index 00000000..1471c338 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java @@ -0,0 +1,16 @@ +public class Java14CaseMixedYieldAndExpression { + + static void mixedYieldAndExpressions(int i) { + // NOT ALLOWED - different case kinds used in the switch + //int j = switch (i) { + // case 0: yield 0; + // case 1 -> 10; + // default: + // yield -1; + //}; + } + + public static void main(String[] args) { + mixedYieldAndExpressions(0); + } +} From 25a6c4a30e1153286c887d4d6c1e9c64478985e9 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 6 Dec 2023 17:01:32 +0100 Subject: [PATCH 03/29] OC-126: more test cases for switches --- .../Java14CaseAndDefaultWithLambdas.java | 28 +++++++++++++++++++ .../Java17SwitchExpressionsWithPatterns.java | 22 +++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 clover-core/src/test/resources/javasyntax17/Java17SwitchExpressionsWithPatterns.java diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java b/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java index 4aabc2e5..62fdd241 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java +++ b/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java @@ -39,10 +39,38 @@ static void switchExpressionWithCaseAndDefaultReturningVoid(int i) { } } + static void switchExpressionWithExpressionsBlocksAndThrows(int i) { + // a code after -> can be one of: expression, block, throw + int result = switch (i) { + case -1 -> throw new IllegalArgumentException("negative"); + case 0 -> 0; + default -> { + int j = i * 10; + yield j; + } + }; + } + + static void switchWithTrowsOnly(int i) { + // note: it can't return any value, not possible to assign switch to a variable + switch (i) { + case -1 -> throw new IllegalArgumentException("negative"); + case 0 -> throw new IllegalArgumentException("zero"); + default -> throw new IllegalArgumentException("anything"); + } + } + public static void main(String[] args) { switchExpressionWithCasesOnly(Colors.G); switchExpressionWithCasesReturningVoidOnly(Colors.B); switchExpressionWithCaseAndDefault(2); switchExpressionWithCaseAndDefaultReturningVoid(0); + switchExpressionWithExpressionsBlocksAndThrows(2); + try { + switchWithTrowsOnly(2); + } catch (IllegalArgumentException ex) { + System.out.println(ex.getMessage()); + } + } } diff --git a/clover-core/src/test/resources/javasyntax17/Java17SwitchExpressionsWithPatterns.java b/clover-core/src/test/resources/javasyntax17/Java17SwitchExpressionsWithPatterns.java new file mode 100644 index 00000000..69102b04 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax17/Java17SwitchExpressionsWithPatterns.java @@ -0,0 +1,22 @@ +public class Java17SwitchExpressionsWithPatterns { + enum EvenOrOdd { + EVEN, ODD, UNKNOWN; + } + + static void switchExpressionWithNullAndDefault(Integer i) { + // in a traditional switch it's possible to define case ...:, followed by default: with no break + // to allow the same behaviour in expressions, the default keyword can be used also inside case pattern + EvenOrOdd evenOrOdd = switch(i) { + case 1 -> EvenOrOdd.ODD; + case 2 -> EvenOrOdd.EVEN; + // note: pattern matching is available in Java 17-preview + case null, default -> EvenOrOdd.UNKNOWN; + }; + } + + public static void main(String[] args) { + switchExpressionWithNullAndDefault(1); + switchExpressionWithNullAndDefault(2); + switchExpressionWithNullAndDefault(null); + } +} From d85e805a28790c95ae2c727ce84fde63854b6d96 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 6 Dec 2023 17:11:34 +0100 Subject: [PATCH 04/29] OC-126: more test cases for text blocks --- .../clover/JavaSyntax15CompilationTest.groovy | 18 ++++++++++++++++++ .../javasyntax15/Java15TextBlockInvalid.java | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 clover-core/src/test/resources/javasyntax15/Java15TextBlockInvalid.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy index bacaddd1..d428a2bb 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy @@ -4,6 +4,10 @@ import com.atlassian.clover.util.JavaEnvUtils import org.junit.Before import org.junit.Test +import static org.hamcrest.MatcherAssert.assertThat +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.instanceOf +import static org.junit.Assert.fail import static org.junit.Assume.assumeTrue /** @@ -30,4 +34,18 @@ class JavaSyntax15CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_15) assertFileMatches(fileName, R_INC + "System.out.println", false) } + + @Test + void testTextBlockInvalid() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_15)) + + final String fileName = "Java15TextBlockInvalid.java" + try { + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_15) + fail("Expected instrumentation to fail") + } catch (Exception ex) { + assertThat(ex, instanceOf(AssertionError.class)) + assertThat(ex.message, containsString("instrumentation problem processing")) + } + } } diff --git a/clover-core/src/test/resources/javasyntax15/Java15TextBlockInvalid.java b/clover-core/src/test/resources/javasyntax15/Java15TextBlockInvalid.java new file mode 100644 index 00000000..b7d5b91f --- /dev/null +++ b/clover-core/src/test/resources/javasyntax15/Java15TextBlockInvalid.java @@ -0,0 +1,18 @@ +public class Java15TextBlockInvalid { + public void invalidTextBlocks() { + // no line terminator + String a = """"""; + + // no line terminator + String b = """ """; + + // no closing delimiter (text block continues to EOF) + //String c = """ + // "; + + // unescaped backslash (see below for escape processing) + String d = """ + abc \ def + """; + } +} \ No newline at end of file From 61c666226cea7352150bd954a93f076827b82b6a Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Tue, 12 Dec 2023 15:53:50 +0100 Subject: [PATCH 05/29] OC-126: correct Java15TextBlockInvalid test --- .../clover/JavaSyntax15CompilationTest.groovy | 15 ++++----------- .../clover/JavaSyntaxCompilationTestBase.groovy | 8 ++++++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy index d428a2bb..86c7acfe 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax15CompilationTest.groovy @@ -5,9 +5,7 @@ import org.junit.Before import org.junit.Test import static org.hamcrest.MatcherAssert.assertThat -import static org.hamcrest.Matchers.containsString -import static org.hamcrest.Matchers.instanceOf -import static org.junit.Assert.fail +import static org.hamcrest.Matchers.equalTo import static org.junit.Assume.assumeTrue /** @@ -39,13 +37,8 @@ class JavaSyntax15CompilationTest extends JavaSyntaxCompilationTestBase { void testTextBlockInvalid() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_15)) - final String fileName = "Java15TextBlockInvalid.java" - try { - instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_15) - fail("Expected instrumentation to fail") - } catch (Exception ex) { - assertThat(ex, instanceOf(AssertionError.class)) - assertThat(ex.message, containsString("instrumentation problem processing")) - } + File srcFile = new File(srcDir, "Java15TextBlockInvalid.java") + int retCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_15) + assertThat(retCode, equalTo(1)) } } diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy index 022b2a0c..a4540cb2 100755 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy @@ -174,6 +174,11 @@ abstract class JavaSyntaxCompilationTestBase { * @param srcVersion */ protected void instrumentSourceFile(final File file, final String srcVersion, final String[] extraArgs) { + final int result = instrumentSourceFileNoAssert(file, srcVersion, extraArgs) + assertEquals("instrumentation problem processing \"$file.absolutePath\":".toString(), 0, result) + } + + protected int instrumentSourceFileNoAssert(final File file, final String srcVersion, final String[] extraArgs) { final String[] args = [ "--source", srcVersion, "--verbose", @@ -184,8 +189,7 @@ abstract class JavaSyntaxCompilationTestBase { file.getAbsolutePath() ] - final int result = CloverInstr.mainImpl((String[]) ArrayUtils.addAll(args, extraArgs)) - assertEquals("instrumentation problem processing \"$file.absolutePath\":".toString(), 0, result) + return CloverInstr.mainImpl((String[]) ArrayUtils.addAll(args, extraArgs)) } /** From 0c298e23784e3ef01a08a3471fb45f04a49ab40a Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 27 Dec 2023 17:37:16 +0100 Subject: [PATCH 06/29] OC-126: helper methods in JavaSyntaxCompilationTestBase, test cases for switch expressions and switch statements when yield and break are allowed or not --- .../clover/JavaSyntax14CompilationTest.groovy | 48 +++++++++++++-- .../JavaSyntaxCompilationTestBase.groovy | 8 ++- .../Java14CaseColonBreakYield.java | 60 ------------------- .../Java14SwitchExpressionWithCaseColon.java | 26 ++++++++ ...14SwitchExpressionWithCaseColonFailed.java | 16 +++++ .../Java14SwitchStatementWithCaseColon.java | 14 +++++ ...a14SwitchStatementWithCaseColonFailed.java | 16 +++++ 7 files changed, 122 insertions(+), 66 deletions(-) delete mode 100644 clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColon.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColonFailed.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index db43e3f5..2d524853 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -4,6 +4,7 @@ import com.atlassian.clover.util.JavaEnvUtils import org.junit.Before import org.junit.Test +import static org.junit.Assert.assertEquals import static org.junit.Assume.assumeTrue /** @@ -24,16 +25,55 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void caseWithColonCanUseBothBreakAndYield() { + void switchExpressionWithCaseWithColonCanUseYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14CaseColonBreakYield.java" + final String fileName = "Java14SwitchExpressionWithCaseColon.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - // using break in switch statements + // using yield in switch expressions is allowed also for "case X:" form + assertFileMatches(fileName, "case 0:.*" + R_INC + "yield 10;") + assertFileMatches(fileName, "default:.*" + R_INC + "yield 11;") + assertFileMatches(fileName, "case 10:.*" + R_INC + "yield 20;") + assertFileMatches(fileName, "case 11:.*" + R_INC + "yield 21;") + assertFileMatches(fileName, "default:.*" + R_INC + "yield 22;") + } + + @Test + void switchExpressionWithCaseWithColonCannotUseBreak() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14SwitchExpressionWithCaseColonFailed.java" + final File srcFile = new File(srcDir, fileName) + int returnCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_14, [] as String[]) + + // using break in switch expressions is NOT allowed for "case X:" form + // a reason is that a switch expression must return a value and break returns no value + assertEquals(1, returnCode) + } + + @Test + void switchStatementWithCaseWithColonCanUseBreak() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14SwitchStatementWithCaseColon.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + // this is a regression test, using break in switch statements must be allowed + assertFileMatches(fileName, "case 30:.*" + R_INC + "k\\+\\+;" + R_INC + "break;") + assertFileMatches(fileName, "default:.*" + R_INC + "break;") + } + + @Test + void switchStatementWithCaseWithColonCannotUseYield() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - // using yield in switch expressions + final String fileName = "Java14SwitchStatementWithCaseColonFailed.java" + final File srcFile = new File(srcDir, fileName) + int returnCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_14, [] as String[]) + // it's not allowed to use yield in switch statements (a value returned cannot be ignored) + assertEquals(1, returnCode) } @Test diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy index a4540cb2..8d5abf5a 100755 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy @@ -61,6 +61,10 @@ abstract class JavaSyntaxCompilationTestBase { protected final String R_LAMBDA_INC_LEFT = "__CLR[a-zA-Z0-9_]+\\.lambdaInc\\([0-9]+," protected final String R_LAMBDA_INC_RIGHT = ",[0-9]+\\)" + /** Regular expression for case expression returning value */ + protected final String R_CASE_EXPRESSION_LEFT = "{" + R_INC + "yield " + protected final String R_CASE_EXPRESSION_LEFT_VOID = "{" + R_INC + protected final String R_CASE_EXPRESSION_RIGHT = "}" protected File mTestcasesSrcDir private File mOutputDir @@ -367,7 +371,7 @@ abstract class JavaSyntaxCompilationTestBase { * @param regExp regular expression to be searched inside a file * @param negate negate assertion - if set to true then assert that file does NOT contain regexp */ - protected void assertFileMatches(String instrumentedFileName, String regExp, boolean negate) { + protected void assertFileMatches(String instrumentedFileName, String regExp, boolean negate = false) { final File instrumentedFile = new File(mGenSrcDir, instrumentedFileName) AssertionUtils.assertFileMatches(regExp, instrumentedFile, negate) } @@ -378,7 +382,7 @@ abstract class JavaSyntaxCompilationTestBase { * @param subString substring to be searched inside a file * @param negate negate assertion - if set to true then assert that file does NOT contain regexp */ - protected void assertFileContains(String instrumentedFileName, String subString, boolean negate) { + protected void assertFileContains(String instrumentedFileName, String subString, boolean negate = false) { final File instrumentedFile = new File(mGenSrcDir, instrumentedFileName) AssertionUtils.assertFileContains(subString, instrumentedFile, negate) } diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java b/clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java deleted file mode 100644 index a6d1a8fc..00000000 --- a/clover-core/src/test/resources/javasyntax14/Java14CaseColonBreakYield.java +++ /dev/null @@ -1,60 +0,0 @@ -public class Java14CaseColonBreakYield { - - static void yieldInSwitchExpression() { - // NOT ALLOWED - yield outside of switch expression - //int i = 0; - //switch (i) { - // case 0: - // yield 0; - // default: - // yield 1; - //}; - - // ALLOWED - int j = 0, k; - k = switch (j) { - case 0: - yield 10; - default: - yield 11; - }; - - // ALLOWED - foo(switch (k) { - case 10: - yield 20; - case 11: - yield 21; - default: - yield 22; - }); - } - - static void breakInSwitchStatement() { - // NOT ALLOWED - break out of switch expression is not allowed - //int i = 0, j; - //j = switch (i) { - // case 0: - // break; - // default: - // break; - //}; - - // ALLOWED - int k = 30; - switch (k) { - case 30: - k++; - break; - default: - break; - } - } - - static void foo(int i) { } - - public static void main(String[] args) { - yieldInSwitchExpression(); - breakInSwitchStatement(); - } -} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColon.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColon.java new file mode 100644 index 00000000..f7f09ce6 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColon.java @@ -0,0 +1,26 @@ +public class Java14SwitchExpressionWithCaseColon { + + static void yieldInSwitchExpression() { + // ALLOWED + int j = 0, k; + k = switch (j) { + case 0: + yield 10; + default: + yield 11; + }; + + // ALLOWED + foo(switch (k) { + case 10: + yield 20; + case 11: + yield 21; + default: + yield 22; + }); + } + + static void foo(int i) { } + +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java new file mode 100644 index 00000000..6000cde4 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java @@ -0,0 +1,16 @@ +/** + * This test file fails to instrument and fails to compile. + */ +public class Java14SwitchExpressionWithCaseColonFailed { + + static void breakInSwitchExpression() { + // NOT ALLOWED - break out of switch expression is not allowed + int i = 0, j; + j = switch (i) { + case 0: + break; + default: + break; + }; + } +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java new file mode 100644 index 00000000..4be90c1b --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java @@ -0,0 +1,14 @@ +public class Java14SwitchStatementWithCaseColon { + + static void breakInSwitchStatement() { + // ALLOWED + int k = 30; + switch (k) { + case 30: + k++; + break; + default: + break; + } + } +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColonFailed.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColonFailed.java new file mode 100644 index 00000000..2d40ce86 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColonFailed.java @@ -0,0 +1,16 @@ +/** + * This test file fails to instrument and fails to compile. + */ +public class Java14SwitchStatementWithCaseColonFailed { + + static void yieldInSwitchStatementIsNotAllowed() { + // NOT ALLOWED - yield outside of switch expression + int i = 0; + switch (i) { + case 0: + yield 0; + default: + yield 1; + }; + } +} From 3dcfe7e435efd1bed5b31ad0bf101670242668fd Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 27 Dec 2023 18:00:52 +0100 Subject: [PATCH 07/29] OC-126: test cases for switch expressions when lambda expressions can return values, be void, or throw exceptions --- .../clover/JavaSyntax14CompilationTest.groovy | 53 +++++++++++++++++-- ...hExpressionCaseAndDefaultWithLambdas.java} | 28 ---------- ...tchExpressionCaseAndDefaultWithThrows.java | 28 ++++++++++ 3 files changed, 76 insertions(+), 33 deletions(-) rename clover-core/src/test/resources/javasyntax14/{Java14CaseAndDefaultWithLambdas.java => Java14SwitchExpressionCaseAndDefaultWithLambdas.java} (58%) create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithThrows.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index 2d524853..d7542501 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -77,17 +77,60 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void switchExpressionWithCaseAndDefaultCanUseLambdas() { + void switchExpressionWithCaseAndDefaultCanUseLambdasReturningValues() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14CaseAndDefaultWithLambdas.java" + final String fileName = "Java14SwitchExpressionCaseAndDefaultWithLambdas.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - // using lambdas in switch statements - // assert case - // assert default + // switch expression with value-returning lambdas + assertFileMatches(fileName, "case R -> " + R_CASE_EXPRESSION_LEFT + "\"red\";" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case G -> " + R_CASE_EXPRESSION_LEFT + "\"green\";" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case B -> " + R_CASE_EXPRESSION_LEFT + "\"blue\";" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case 0 -> " + R_CASE_EXPRESSION_LEFT + "\"EvenOrOdd\\.EVEN\";" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case 1 -> " + R_CASE_EXPRESSION_LEFT + "\"EvenOrOdd\\.ODD\";" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT + "\"EvenOrOdd\\.UNKNOWN\";" + R_CASE_EXPRESSION_RIGHT) } + @Test + void switchExpressionWithCaseAndDefaultCanUseLambdasReturningVoid() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14SwitchExpressionCaseAndDefaultWithLambdas.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + // switch expression with void lambdas + assertFileMatches(fileName, "case R -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(\"red\"\\);" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case G -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(\"green\"\\);" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case B -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(\"blue\"\\);" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case 0 -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(EvenOrOdd\\.EVEN\\);" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case 1 -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(EvenOrOdd\\.ODD\\);" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(EvenOrOdd\\.UNKNOWN\\);" + R_CASE_EXPRESSION_RIGHT) + } + + @Test + void switchExpressionWithCaseAndDefaultCanThrowExceptions() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14SwitchExpressionCaseAndDefaultWithThrows.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + assertFileMatches(fileName, "case -1 -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"negative\");" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT + "0;" + R_CASE_EXPRESSION_RIGHT) + + assertFileMatches(fileName, "case -1 -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"negative\");" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "case 0 -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"zero\");" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"anything\");" + R_CASE_EXPRESSION_RIGHT) + } + + + + + + + + + @Test void testSwitchWithCaseExpressionAndBlockReturningYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java similarity index 58% rename from clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java rename to clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java index 62fdd241..4aabc2e5 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14CaseAndDefaultWithLambdas.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java @@ -39,38 +39,10 @@ static void switchExpressionWithCaseAndDefaultReturningVoid(int i) { } } - static void switchExpressionWithExpressionsBlocksAndThrows(int i) { - // a code after -> can be one of: expression, block, throw - int result = switch (i) { - case -1 -> throw new IllegalArgumentException("negative"); - case 0 -> 0; - default -> { - int j = i * 10; - yield j; - } - }; - } - - static void switchWithTrowsOnly(int i) { - // note: it can't return any value, not possible to assign switch to a variable - switch (i) { - case -1 -> throw new IllegalArgumentException("negative"); - case 0 -> throw new IllegalArgumentException("zero"); - default -> throw new IllegalArgumentException("anything"); - } - } - public static void main(String[] args) { switchExpressionWithCasesOnly(Colors.G); switchExpressionWithCasesReturningVoidOnly(Colors.B); switchExpressionWithCaseAndDefault(2); switchExpressionWithCaseAndDefaultReturningVoid(0); - switchExpressionWithExpressionsBlocksAndThrows(2); - try { - switchWithTrowsOnly(2); - } catch (IllegalArgumentException ex) { - System.out.println(ex.getMessage()); - } - } } diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithThrows.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithThrows.java new file mode 100644 index 00000000..44880849 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithThrows.java @@ -0,0 +1,28 @@ +public class Java14SwitchExpressionCaseAndDefaultWithThrows { + + static void switchExpressionWithExpressionsBlocksAndThrows(int i) { + // some branches of a switch expression can throw exceptions + int result = switch (i) { + case -1 -> throw new IllegalArgumentException("negative"); + default -> 0; + }; + } + + static void switchWithTrowsOnly(int i) { + // note: it can't return any value, so it's not possible to assign switch to a variable + switch (i) { + case -1 -> throw new IllegalArgumentException("negative"); + case 0 -> throw new IllegalArgumentException("zero"); + default -> throw new IllegalArgumentException("anything"); + } + } + + public static void main(String[] args) { + try { + switchExpressionWithExpressionsBlocksAndThrows(2); + switchWithTrowsOnly(2); + } catch (IllegalArgumentException ex) { + System.out.println(ex.getMessage()); + } + } +} From 8702d61f5612b25bb98b9d1736228d338f617f2e Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 27 Dec 2023 20:52:53 +0100 Subject: [PATCH 08/29] OC-126: quote code matched in regular expressions --- .../clover/JavaSyntax14CompilationTest.groovy | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index d7542501..8f4822a9 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -4,6 +4,7 @@ import com.atlassian.clover.util.JavaEnvUtils import org.junit.Before import org.junit.Test +import static java.util.regex.Pattern.quote import static org.junit.Assert.assertEquals import static org.junit.Assume.assumeTrue @@ -32,11 +33,11 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // using yield in switch expressions is allowed also for "case X:" form - assertFileMatches(fileName, "case 0:.*" + R_INC + "yield 10;") - assertFileMatches(fileName, "default:.*" + R_INC + "yield 11;") - assertFileMatches(fileName, "case 10:.*" + R_INC + "yield 20;") - assertFileMatches(fileName, "case 11:.*" + R_INC + "yield 21;") - assertFileMatches(fileName, "default:.*" + R_INC + "yield 22;") + assertFileMatches(fileName, quote("case 0:") + "\\w*" + R_INC + quote("yield 10;")) + assertFileMatches(fileName, quote("default:") + "\\w*" + R_INC + quote("yield 11;")) + assertFileMatches(fileName, quote("case 10:") + "\\w*" + R_INC + quote("yield 20;")) + assertFileMatches(fileName, quote("case 11:") + "\\w*" + R_INC + quote("yield 21;")) + assertFileMatches(fileName, quote("default:") + "\\w*" + R_INC + quote("yield 22;")) } @Test @@ -60,8 +61,8 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // this is a regression test, using break in switch statements must be allowed - assertFileMatches(fileName, "case 30:.*" + R_INC + "k\\+\\+;" + R_INC + "break;") - assertFileMatches(fileName, "default:.*" + R_INC + "break;") + assertFileMatches(fileName, quote("case 30:") + "\\w*" + R_INC + quote("k++;") + R_INC + quote("break;")) + assertFileMatches(fileName, quote("default:") + "\\w*" + R_INC + quote("break;")) } @Test @@ -84,12 +85,12 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // switch expression with value-returning lambdas - assertFileMatches(fileName, "case R -> " + R_CASE_EXPRESSION_LEFT + "\"red\";" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case G -> " + R_CASE_EXPRESSION_LEFT + "\"green\";" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case B -> " + R_CASE_EXPRESSION_LEFT + "\"blue\";" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case 0 -> " + R_CASE_EXPRESSION_LEFT + "\"EvenOrOdd\\.EVEN\";" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case 1 -> " + R_CASE_EXPRESSION_LEFT + "\"EvenOrOdd\\.ODD\";" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT + "\"EvenOrOdd\\.UNKNOWN\";" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT + quote("\"red\";") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT + quote("\"green\";") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT + quote("\"blue\";") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.EVEN;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.ODD;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.UNKNOWN;") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -100,12 +101,18 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // switch expression with void lambdas - assertFileMatches(fileName, "case R -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(\"red\"\\);" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case G -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(\"green\"\\);" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case B -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(\"blue\"\\);" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case 0 -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(EvenOrOdd\\.EVEN\\);" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case 1 -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(EvenOrOdd\\.ODD\\);" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT_VOID + "System\\.out\\.println\\(EvenOrOdd\\.UNKNOWN\\);" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT_VOID + + quote("System.out.println(\"red\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT_VOID + + quote("System.out.println(\"green\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT_VOID + + quote("System.out.println(\"blue\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT_VOID + + quote("System.out.println(EvenOrOdd.EVEN);") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT_VOID + + quote("System.out.println(EvenOrOdd.ODD);") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT_VOID + + quote("System.out.println(EvenOrOdd.UNKNOWN);") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -115,22 +122,19 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { final String fileName = "Java14SwitchExpressionCaseAndDefaultWithThrows.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - assertFileMatches(fileName, "case -1 -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"negative\");" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT + "0;" + R_CASE_EXPRESSION_RIGHT) - - assertFileMatches(fileName, "case -1 -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"negative\");" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "case 0 -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"zero\");" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, "default -> " + R_CASE_EXPRESSION_LEFT + "throw new IllegalArgumentException(\"anything\");" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + + quote("0;") + R_CASE_EXPRESSION_RIGHT) + + assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"zero\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"anything\");") + R_CASE_EXPRESSION_RIGHT) } - - - - - - - - @Test void testSwitchWithCaseExpressionAndBlockReturningYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) From 7994476ea4cb6c42b4562620df0a6f73f631e5b3 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 27 Dec 2023 20:56:43 +0100 Subject: [PATCH 09/29] OC-126: test cases for switch expressions with blocks returning value, returning void or throwing exceptions --- .../clover/JavaSyntax14CompilationTest.groovy | 26 +++++++--- ...tchExpressionCaseAndDefaultWithBlocks.java | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index 8f4822a9..d24d8be8 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -136,31 +136,43 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void testSwitchWithCaseExpressionAndBlockReturningYield() { + void switchExpressionWithBlockWithYieldOrReturn() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14SwitchExpressionCaseAndDefaultWithBlocks.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + assertFileMatches(fileName, R_INC + quote("int color = switch (i)")) + assertFileMatches(fileName, quote("case 0 -> { ") + R_INC + quote("yield 0x00; }")) + assertFileMatches(fileName, quote("case 1 -> { ") + R_INC + quote("yield 0x10; }")) + assertFileMatches(fileName, quote("default -> { ") + R_INC + quote("return null; }")) } @Test - void testSwitchWithCaseWithMultipleValues() { + void switchExpressionWithBlockReturningVoid() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14SwitchExpressionCaseAndDefaultWithBlocks.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + assertFileMatches(fileName, quote("case 0 -> { ") + R_INC + quote("System.out.println(\"0x00\"); }")) + assertFileMatches(fileName, quote("case 1 -> { ") + R_INC + quote("System.out.println(\"0x10\"); }")) + assertFileMatches(fileName, quote("default -> { ") + R_INC + quote("System.out.println(\"0xFF\"); }")) } @Test - void testCaseExpressionCanBeVoid() { + void switchExpressionWithBlockThrowingException() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14SwitchExpressionCaseAndDefaultWithBlocks.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + assertFileMatches(fileName, R_INC + quote("throw new IllegalArgumentException(\"negative\");")) + assertFileMatches(fileName, R_INC + quote("throw new IllegalArgumentException(\"positive\");")) } @Test - void testCaseExpressionCanReturnValue() { + void testSwitchWithCaseWithMultipleValues() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) final String fileName = "Java14.java" diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java new file mode 100644 index 00000000..7e8c6af4 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java @@ -0,0 +1,48 @@ +public class Java14SwitchExpressionCaseAndDefaultWithBlocks { + + static Integer switchExpressionWithBlocksReturningValues(int i) { + int color = switch (i) { + case 0 -> { yield 0x00; } + case 1 -> { yield 0x10; } + default -> { return null; } + }; + return color; + } + + static void switchExpressionWithBlocksReturningVoid(int i) { + // all void, can't assign any value + switch (i) { + case 0 -> { System.out.println("0x00"); } + case 1 -> { System.out.println("0x10"); } + default -> { System.out.println("0xFF"); } + } + } + + static void switchExpressionWithBlocksThrowingExceptions(int i) { + // case expression blocks can throw exceptions + int onlyZero = switch (i) { + case -1 -> { + throw new IllegalArgumentException("negative"); + } + case 0 -> 0; + default -> { + throw new IllegalArgumentException("positive"); + }; + }; + } + + public static void main(String[] args) { + try { + switchExpressionWithBlocksReturningValues(0); + switchExpressionWithBlocksReturningValues(1); + switchExpressionWithBlocksReturningValues(2); + switchExpressionWithBlocksReturningVoid(0); + switchExpressionWithBlocksReturningVoid(1); + switchExpressionWithBlocksReturningVoid(2); + switchExpressionWithBlocksThrowingExceptions(0); + switchExpressionWithBlocksThrowingExceptions(1); + } catch (IllegalArgumentException ex) { + System.out.println(ex.getMessage()); + } + } +} From 570a5618c34b6da61778d35e72d9d835b62ad3d6 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 27 Dec 2023 21:13:48 +0100 Subject: [PATCH 10/29] OC-126: test cases for switch expressions with multiple values in a case matcher, including special cases like 'case null, default'; also a test verifying that mixing different forms of case in a single statement is not allowed --- .../clover/JavaSyntax14CompilationTest.groovy | 20 +++++++++---- .../Java14CaseMixedYieldAndExpression.java | 16 ----------- ...ava14SwitchExpressionMixedCasesFailed.java | 15 ++++++++++ ...a14SwitchExpressionWithMultiValueCase.java | 28 +++++++++++++++++++ 4 files changed, 58 insertions(+), 21 deletions(-) delete mode 100644 clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionMixedCasesFailed.java create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index d24d8be8..a762a2ec 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -172,11 +172,18 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void testSwitchWithCaseWithMultipleValues() { + void switchExpressionWithCaseWithMultipleValues() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14SwitchExpressionWithMultiValueCase.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + assertFileMatches(fileName, quote("case 0, 1, 2*3 -> ") + R_CASE_EXPRESSION_LEFT + quote("10;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("11;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0, 1, 2 -> ") + R_CASE_EXPRESSION_LEFT + quote("20;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case null -> ") + R_CASE_EXPRESSION_LEFT + quote("21;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0, 1, 2 -> ") + R_CASE_EXPRESSION_LEFT + quote("30;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case null, default -> ") + R_CASE_EXPRESSION_LEFT + quote("31;") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -202,10 +209,13 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { @Test void testSwitchCaseWithColonsMixedWithExpressions() { - // a standalone statement in method, instance initializer block, static block assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14CaseMixedYieldAndExpression.java" - instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + final String fileName = "Java14SwitchExpressionMixedCasesFailed.java" + final File srcFile = new File(srcDir, fileName) + int returnCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_14, [] as String[]) + + // it's not allowed to mix "case X:" with "case X ->" in the same switch statement + assertEquals(1, returnCode) } } diff --git a/clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java b/clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java deleted file mode 100644 index 1471c338..00000000 --- a/clover-core/src/test/resources/javasyntax14/Java14CaseMixedYieldAndExpression.java +++ /dev/null @@ -1,16 +0,0 @@ -public class Java14CaseMixedYieldAndExpression { - - static void mixedYieldAndExpressions(int i) { - // NOT ALLOWED - different case kinds used in the switch - //int j = switch (i) { - // case 0: yield 0; - // case 1 -> 10; - // default: - // yield -1; - //}; - } - - public static void main(String[] args) { - mixedYieldAndExpressions(0); - } -} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionMixedCasesFailed.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionMixedCasesFailed.java new file mode 100644 index 00000000..eb0c8224 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionMixedCasesFailed.java @@ -0,0 +1,15 @@ +/** + * This test file fails to instrument and fails to compile. + */ +public class Java14SwitchExpressionMixedCasesFailed { + + static void mixedCaseWithColonWithCaseWithLambda(int i) { + // NOT ALLOWED - different case kinds used in the same switch + int j = switch (i) { + case 0: yield 0; + case 1 -> 10; + default: + yield -1; + }; + } +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java new file mode 100644 index 00000000..42a8df22 --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java @@ -0,0 +1,28 @@ +public class Java14SwitchExpressionWithMultivalueCase { + + static void switchExpressionWithMultipleCaseValues(int i) { + int k; + k = switch (i) { + case 0, 1, 2*3 -> 10; + default -> 11; + }; + } + + static void switchExpressionWithNullAsCase(int i) { + Integer j = Integer.valueOf(i); + int k = switch (j) { + case 0, 1, 2 -> 20; + // the "null" keyword must be also allowed as case value + case null -> 21; + }; + } + + static void switchExpressionWithNullAndDefaultAsCase(int i) { + Integer j = Integer.valueOf(i); + int k = switch (j) { + case 0, 1, 2 -> 30; + // a special case when "default" is written as "case default:" instead of "default:" + case null, default -> 31; + }; + } +} From 04663ff990eb7c0569e495de05aedf39264765fd Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Tue, 2 Jan 2024 16:26:07 +0100 Subject: [PATCH 11/29] OC-126: test cases for switch expressions used in different contexts, corrected compilation errors --- .../clover/JavaSyntax14CompilationTest.groovy | 39 ++++++++++++--- ...tchExpressionCaseAndDefaultWithBlocks.java | 4 +- ...chExpressionCaseAndDefaultWithLambdas.java | 2 +- ...va14SwitchExpressionInVariousContexts.java | 49 +++++++++++++++++++ ...a14SwitchExpressionWithMultiValueCase.java | 10 ++-- 5 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index a762a2ec..ea520090 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -136,7 +136,7 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void switchExpressionWithBlockWithYieldOrReturn() { + void switchExpressionWithBlockWithYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) final String fileName = "Java14SwitchExpressionCaseAndDefaultWithBlocks.java" @@ -145,7 +145,7 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { assertFileMatches(fileName, R_INC + quote("int color = switch (i)")) assertFileMatches(fileName, quote("case 0 -> { ") + R_INC + quote("yield 0x00; }")) assertFileMatches(fileName, quote("case 1 -> { ") + R_INC + quote("yield 0x10; }")) - assertFileMatches(fileName, quote("default -> { ") + R_INC + quote("return null; }")) + assertFileMatches(fileName, quote("default -> { ") + R_INC + quote("yield 0x20; }")) } @Test @@ -187,24 +187,49 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void testSwitchIsAnExpression() { + void switchIsAnExpressionInDifferentContexts() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14SwitchExpressionInVariousContexts.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // assignment to a variable + assertFileMatches(fileName, R_INC + quote("int k = switch (j) {")) + // argument of a method call + assertFileMatches(fileName, R_INC + quote("foo(switch (k) {")) // no R_INC before switch + assertFileMatches(fileName, quote("case 10 -> ") + R_CASE_EXPRESSION_LEFT + "100;" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + "200;" + R_CASE_EXPRESSION_RIGHT) + // part of an expression + assertFileMatches(fileName, quote("") + R_CASE_EXPRESSION_LEFT + + quote("if (switch (j) {") + ".*" + + quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("30;") + R_CASE_EXPRESSION_RIGHT + + ".*" + + quote("} % 10 == 0)") + + quote(")&&") + R_IGET_TRUE // part of the branch coverage expression + ) } @Test - void testSwitchIsAStatement() { - // a standalone statement in method, instance initializer block, static block + void switchIsAStatementInDifferentContexts() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14.java" + final String fileName = "Java14SwitchExpressionInVariousContexts.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + + // a standalone statement in method + assertFileMatches(fileName, quote("") + R_CASE_EXPRESSION_LEFT + + quote("") + R_CASE_EXPRESSION_RIGHT) + + // instance initializer block + assertFileMatches(fileName, quote("{") + "\\b+" + + R_INC + quote("switch (kkk) {") + R_CASE_EXPRESSION_LEFT + + quote("") + R_CASE_EXPRESSION_RIGHT) + + // static block + assertFileMatches(fileName, quote("") + R_CASE_EXPRESSION_LEFT + + quote("") + R_CASE_EXPRESSION_RIGHT) } @Test diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java index 7e8c6af4..dcac83f5 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithBlocks.java @@ -4,7 +4,7 @@ static Integer switchExpressionWithBlocksReturningValues(int i) { int color = switch (i) { case 0 -> { yield 0x00; } case 1 -> { yield 0x10; } - default -> { return null; } + default -> { yield 0x20; } }; return color; } @@ -27,7 +27,7 @@ static void switchExpressionWithBlocksThrowingExceptions(int i) { case 0 -> 0; default -> { throw new IllegalArgumentException("positive"); - }; + } }; } diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java index 4aabc2e5..725cfac9 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionCaseAndDefaultWithLambdas.java @@ -1,4 +1,4 @@ -public class Java14CaseAndDefaultWithLambdas { +public class Java14SwitchExpressionCaseAndDefaultWithLambdas { enum Colors { R, G, B; } diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java new file mode 100644 index 00000000..1c0af28a --- /dev/null +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java @@ -0,0 +1,49 @@ +public class Java14SwitchExpressionInVariousContexts { + + static void switchExpressionInAssignment(int j) { + int k = switch (j) { + case 0 -> 10; + default -> 11; + }; + } + + static void switchExpressionInExpression(int j) { + int k = 8 + (switch (j) { + case 0 -> 20; + default -> 21; + } * 24) / 2; + + if (switch (j) { + case 0 -> 30; + default -> 31; + } % 10 == 0) { + int l = 32; + } + } + + static void switchExpressionAsMethodArgument(int k) { + foo(switch (k) { + case 10 -> 100; + default -> 200; + }); + } + + static void switchExpressionAsMethodStatement(int kk) { + switch (kk) { + case 77 -> System.out.println("77"); + default -> System.out.println("not 77"); + } + } + + // switch expression in an initializer block + int kkk = 88; + { + switch (kkk) { + case 88 -> System.out.println("88"); + default -> System.out.println("not 88"); + } + } + + static void foo(int i) { } + +} diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java index 42a8df22..6f8cb1de 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java @@ -1,4 +1,4 @@ -public class Java14SwitchExpressionWithMultivalueCase { +public class Java14SwitchExpressionWithMultiValueCase { static void switchExpressionWithMultipleCaseValues(int i) { int k; @@ -13,7 +13,9 @@ static void switchExpressionWithNullAsCase(int i) { int k = switch (j) { case 0, 1, 2 -> 20; // the "null" keyword must be also allowed as case value - case null -> 21; + // note: pattern matching can be used since Java 17 preview + // TODO: enable this test for Java 21 + // case null -> 21; }; } @@ -22,7 +24,9 @@ static void switchExpressionWithNullAndDefaultAsCase(int i) { int k = switch (j) { case 0, 1, 2 -> 30; // a special case when "default" is written as "case default:" instead of "default:" - case null, default -> 31; + // note: pattern matching can be used since Java 17 preview + // TODO: enable this test for Java 21 + // case null, default -> 31; }; } } From a1a4d34c22f09b03f0c7a995a220d0e2a3f96290 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 3 Jan 2024 18:15:42 +0100 Subject: [PATCH 12/29] OC-126: rename existing rules related with switch statements, add documentation --- .../com/atlassian/clover/instr/java/java.g | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index e0222943..031be1ea 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -2449,7 +2449,7 @@ statement [CloverToken owningLabel] returns [CloverToken last] } LPAREN! expression RPAREN! LCURLY! ( - tmpCmp = casesGroup[flag] + tmpCmp = colonCasesGroup[flag] { complexity += tmpCmp; } @@ -2507,7 +2507,10 @@ statement [CloverToken owningLabel] returns [CloverToken last] } ; -casesGroup[FlagDeclEmitter flag] returns [int complexity] +/** + * A group of one or more "case x:" labels, followed by a list of statements. + */ +colonCasesGroup[FlagDeclEmitter flag] returns [int complexity] { int tmp = 0; complexity = 0; @@ -2521,15 +2524,18 @@ casesGroup[FlagDeclEmitter flag] returns [int complexity] options { warnWhenFollowAmbig = false; }: - tmp = aCase[flag] + tmp = colonCase[flag] { complexity += tmp; } )+ - caseSList + caseStatementsList ; -aCase[FlagDeclEmitter flag] returns [int complexity] +/** + * A single "case x:" or "default:" label. + */ +colonCase[FlagDeclEmitter flag] returns [int complexity] { Token pos = null; complexity = 0; @@ -2559,7 +2565,10 @@ aCase[FlagDeclEmitter flag] returns [int complexity] } ; -caseSList +/** + * A list of statements inside a single "case" or "default" block. + */ +caseStatementsList { CloverToken tmp; } From 44b8fcdff602be9fc6a34b707507a366ca2f8649 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 3 Jan 2024 21:27:07 +0100 Subject: [PATCH 13/29] OC-126: a draft of the switch expression grammar --- .../com/atlassian/clover/instr/java/java.g | 95 ++++++++++++++++++- ...va14SwitchExpressionInVariousContexts.java | 7 ++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 031be1ea..45e9cd59 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -377,14 +377,26 @@ tokens { * before: [() -> 1 + 2] * after : [() -> 1 + 2;}] */ - private void instrExitLambdaExprToBlockExpression(LambdaExprToBlockStartEntryEmitter entryEmitter, CloverToken tok) { + private void instrExitLambdaExprToBlockExpression(LambdaExprToBlockStartEntryEmitter entryEmitter, CloverToken tok) { if (cfg.getInstrumentLambda() == LambdaInstrumentation.ALL || cfg.getInstrumentLambda() == LambdaInstrumentation.ALL_BUT_REFERENCE || cfg.getInstrumentLambda() == LambdaInstrumentation.EXPRESSION) { tok.addPostEmitter( new LambdaExprToBlockExitEmitter(entryEmitter, tok.getLine(), tok.getColumn()+tok.getText().length())); } - } + } + + private void instrEnterCaseExpression(CloverToken insertionPoint, ContextSet context) { + // TODO + // insertionPoint.addPreEmitter( + // new CaseExpressionEntryEmitter(insertionPoint.getLine(), insertionPoint.getColumn()+tok.getText().length())); + } + + private void instrExitCaseExpression(CloverToken insertionPoint, ContextSet context) { + // TODO + // insertionPoint.addPostEmitter( + // new CaseExpressionExitEmitter(insertionPoint.getLine(), insertionPoint.getColumn()+tok.getText().length())); + } private CloverToken maybeAddFlushInstr(CloverToken last) { last.addPostEmitter(new DirectedFlushEmitter()); @@ -2284,6 +2296,7 @@ statement [CloverToken owningLabel] returns [CloverToken last] // An expression statement. This could be a method call, // assignment statement, or any other expression evaluated for // side-effects. + ( expression SEMI ) => expression se2:SEMI! { flushAfter = ct(se2); @@ -2436,7 +2449,8 @@ statement [CloverToken owningLabel] returns [CloverToken last] RETURN (expression)? SEMI! | - // switch/case statement + // switch/case statement XXX + ( SWITCH LPAREN expression RPAREN LCURLY) => sw:SWITCH { tmp = ct(sw); @@ -3188,6 +3202,7 @@ postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] primaryExpressionPart { String type = null; + int complexity; } : IDENT @@ -3219,6 +3234,80 @@ primaryExpressionPart | // hack: "non-sealed" in expression means "non - sealed", allow this to parse NON_SEALED + | + ( SWITCH LPAREN expression RPAREN LCURLY ) => + complexity = switchExpression + ; + +/** + * A switch expression containing one or more "case ->" or "default ->" conditions. + */ +switchExpression returns [int complexity] +{ + int caseComplexity = 0; + ContextSet saveContext = getCurrentContext(); + CloverToken tmp = null; + complexity = 0; +} + : + sw:SWITCH + { + tmp = ct(sw); + enterContext(ContextStore.CONTEXT_SWITCH); + saveContext = getCurrentContext(); + } + LPAREN! expression RPAREN! LCURLY! + ( + caseComplexity = lambdaCase[saveContext] + { + complexity += caseComplexity; + } + )+ + { + exitContext(); + } + rc:RCURLY! + ; + +/** + * A single "case x ->" or "default ->" label, followed by an expression or a block statement. + */ +lambdaCase[ContextSet context] returns [int complexity] +{ + CloverToken endTok = null; + Token pos = null; + complexity = 1; +} + : + ( + si1:CASE + { + constExpr = true; + } + expression + { + constExpr = false; + pos = si1; + } + | + si2:DEFAULT + { + pos = si2; + } + ) + t:LAMBDA! + ( + { + instrEnterCaseExpression(lt(1), context); + } + expression + { + instrExitCaseExpression(lt(0), context); + } + | + // no need for special instrumentation, we will instrument it like a simple { } block, inside + endTok=compoundStatement + ) ; /** diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java index 1c0af28a..2403ef2e 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java @@ -44,6 +44,13 @@ static void switchExpressionAsMethodStatement(int kk) { } } + static void switchExpressionInThrowWithThrow(int j) { + throw switch (j) { + case 0 -> new RuntimeException("zero"); + case 1 -> new IllegalArgumentException("one"); + default -> throw new IllegalArgumentException("unsupported"); + }; + } static void foo(int i) { } } From c02ef052742185f4769ff0717467ff84a5a5d059 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Mon, 8 Jan 2024 23:46:46 +0100 Subject: [PATCH 14/29] OC-126: java.g - handle 'yield x' statement; correct lambdaCase grammar, first fix was to not treat entire case as a lambda function, but as a simple value, followed by ->, followed by an expression; a second fix was to match a semicolon after an expression; JavaSyntax14CompilationTest correct regexps in switchStatementWithCaseWithColonCanUseBreak (there are extra if-boolean inserted, regex does not match multi-line); ignore switchStatementWithCaseWithColonCannotUseYield as grammar is not so strict --- .../java/com/atlassian/clover/instr/java/java.g | 15 ++++++++++++++- .../clover/JavaSyntax14CompilationTest.groovy | 6 ++++-- .../Java14SwitchStatementWithCaseColon.java | 7 ++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 45e9cd59..0b200c6a 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -2273,6 +2273,12 @@ statement [CloverToken owningLabel] returns [CloverToken last] flushAfter = ct(se1); } + | + // note: yield can appear inside traditional case blocks and also in compound statement blocks inside + // lambda case; that's why rule is placed here; a drawback is that instrumenter accepts yield in any place + ( { isNextKeyword("yield") }? IDENT expression SEMI ) => + { isNextKeyword("yield") }? IDENT expression SEMI! + | // NOTE: we check for records before normal statement as "record" can be recognized as IDENT leading to syntax error // record definition @@ -3284,7 +3290,7 @@ lambdaCase[ContextSet context] returns [int complexity] { constExpr = true; } - expression + patternMatch { constExpr = false; pos = si1; @@ -3304,12 +3310,19 @@ lambdaCase[ContextSet context] returns [int complexity] { instrExitCaseExpression(lt(0), context); } + SEMI! | // no need for special instrumentation, we will instrument it like a simple { } block, inside endTok=compoundStatement ) ; +patternMatch + : + // just constants, string literals, true/false, null etc + primaryExpressionPart + ; + /** * A supplementary part for the primaryExpressionPart, which allows us to use array indexes, dot-qualified names, * this/class/super calls etc. Shall be used in conjunction with the primaryExpressionPart. diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index ea520090..61f04231 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -2,6 +2,7 @@ package com.atlassian.clover import com.atlassian.clover.util.JavaEnvUtils import org.junit.Before +import org.junit.Ignore import org.junit.Test import static java.util.regex.Pattern.quote @@ -61,10 +62,11 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // this is a regression test, using break in switch statements must be allowed - assertFileMatches(fileName, quote("case 30:") + "\\w*" + R_INC + quote("k++;") + R_INC + quote("break;")) - assertFileMatches(fileName, quote("default:") + "\\w*" + R_INC + quote("break;")) + assertFileMatches(fileName, R_INC + quote("k++;") + R_INC + quote("break;")) + assertFileMatches(fileName, quote("default:") + ".*" + R_INC + quote("break;")) } + @Ignore("Current grammar rule is too loose, and allows use of 'yield' in place where any statement is allowed") @Test void switchStatementWithCaseWithColonCannotUseYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java index 4be90c1b..d51d58e1 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchStatementWithCaseColon.java @@ -4,11 +4,8 @@ static void breakInSwitchStatement() { // ALLOWED int k = 30; switch (k) { - case 30: - k++; - break; - default: - break; + case 30:k++;break; + default:break; } } } From 6f48dbf464a0d11512b30b5970b74daebfe56815 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Tue, 16 Jan 2024 19:51:32 +0100 Subject: [PATCH 15/29] OC-126: refactor grammar related with two forms of switch blocks; both the colon-based and the lambda-based switch can be used inside expressions; both the colon-based and the lambda-based can be a standalone statement (in the latter case the lambda-based needs to call void lambdas); using semantic predicates for disambiguation of both formats; returning ContextSetAndComplexity from both types of switches --- .../instr/java/ContextSetAndComplexity.java | 11 ++ .../com/atlassian/clover/instr/java/java.g | 141 ++++++++++++------ .../JavaSyntaxCompilationTestBase.groovy | 6 +- ...va14SwitchExpressionInVariousContexts.java | 19 ++- ...14SwitchExpressionWithCaseColonFailed.java | 1 + 5 files changed, 124 insertions(+), 54 deletions(-) create mode 100644 clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java new file mode 100644 index 00000000..575f07d2 --- /dev/null +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java @@ -0,0 +1,11 @@ +package com.atlassian.clover.instr.java; + +import com.atlassian.clover.context.ContextSet; + +/** + * A helper class used in java.g + */ +class ContextSetAndComplexity { + ContextSet context; + int complexity; +} diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 0b200c6a..97031a6b 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -2228,6 +2228,7 @@ statement [CloverToken owningLabel] returns [CloverToken last] Parameter parameter = null; String classname = null; ContextSet saveContext = getCurrentContext(); + ContextSetAndComplexity contextAndComplexity = null; } : { @@ -2455,30 +2456,21 @@ statement [CloverToken owningLabel] returns [CloverToken last] RETURN (expression)? SEMI! | - // switch/case statement XXX - ( SWITCH LPAREN expression RPAREN LCURLY) => - sw:SWITCH + // a classic switch/case with colons + ( SWITCH LPAREN expression RPAREN LCURLY (CASE expression | DEFAULT) COLON) => + contextAndComplexity = colonSwitchExpression[owningLabel, false] { - tmp = ct(sw); - if (labelled) { - tmp = owningLabel; - } - flag = declareFlagBefore(tmp); - enterContext(ContextStore.CONTEXT_SWITCH); - saveContext = getCurrentContext(); + saveContext = contextAndComplexity.context; + complexity += contextAndComplexity.complexity; } - LPAREN! expression RPAREN! LCURLY! - ( - tmpCmp = colonCasesGroup[flag] - { - complexity += tmpCmp; - } - )* + | + // a new switch/case with lambdas + ( SWITCH LPAREN expression RPAREN LCURLY (CASE patternMatch | DEFAULT) LAMBDA) => + contextAndComplexity = lambdaSwitchExpression[owningLabel] { - exitContext(); + saveContext = contextAndComplexity.context; + complexity += contextAndComplexity.complexity; } - rc:RCURLY! - | // exception try-catch block (tryCatchBlock[labelled]) => @@ -2580,7 +2572,11 @@ colonCase[FlagDeclEmitter flag] returns [int complexity] ) t:COLON! { - instrInlineAfter(ct(t), ct(pos), ct(t), flag); + if (flag != null) { + instrInlineAfter(ct(t), ct(pos), ct(t), flag); + } else { + instrInlineAfter(ct(t), ct(pos), ct(t)); + } fileInfo.setSuppressFallthroughWarnings(true); } ; @@ -3205,32 +3201,41 @@ postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] // the basic element of an expression -primaryExpressionPart +primaryExpressionPart returns [ContextSetAndComplexity ret] { String type = null; - int complexity; + ret = null; } : IDENT { - pushIdentifierToHeadStack(LT(0).getText()); + pushIdentifierToHeadStack(LT(0).getText()); } - | constant - | TRUE - | FALSE - | THIS - { - pushIdentifierToHeadStack(LT(0).getText()); - } - | NULL - | newExpression - | LPAREN! assignmentExpression RPAREN! - | SUPER - { - pushIdentifierToHeadStack(LT(0).getText()); - } + | + constant + | + TRUE + | + FALSE + | + THIS + { + pushIdentifierToHeadStack(LT(0).getText()); + } + | + NULL + | + newExpression + | + LPAREN! assignmentExpression RPAREN! + | + SUPER + { + pushIdentifierToHeadStack(LT(0).getText()); + } + | // look for int.class, int[].class, and int[]::new - | type=builtInType + type=builtInType ( LBRACK RBRACK! )* ( DOT CLASS @@ -3241,32 +3246,76 @@ primaryExpressionPart // hack: "non-sealed" in expression means "non - sealed", allow this to parse NON_SEALED | - ( SWITCH LPAREN expression RPAREN LCURLY ) => - complexity = switchExpression + // a new lambda switch can be a part of an expression + ( SWITCH LPAREN expression RPAREN LCURLY (CASE patternMatch | DEFAULT) LAMBDA) => + ret = lambdaSwitchExpression[null] + | + // even the old one colon switch has been retrofitted + ( SWITCH LPAREN expression RPAREN LCURLY (CASE expression | DEFAULT) COLON) => + ret = colonSwitchExpression[null, true] ; /** - * A switch expression containing one or more "case ->" or "default ->" conditions. + * A switch statement or expression containing one or more "case :" or "default :" conditions. + * @param owningLabel a label before switch or null if not present + * @param isInsideExpression true if the switch is part of an expression, false if is a standalone statement */ -switchExpression returns [int complexity] +colonSwitchExpression [CloverToken owningLabel, boolean isInsideExpression] returns [ContextSetAndComplexity ret] +{ + CloverToken tmp = null; + boolean labelled = (owningLabel != null); + FlagDeclEmitter flag = null; + ret = new ContextSetAndComplexity(); + int casesGroupComplexity; +} + : + sw:SWITCH + { + tmp = ct(sw); + if (labelled) { + tmp = owningLabel; + } + if (!isInsideExpression) { + flag = declareFlagBefore(tmp); + } + enterContext(ContextStore.CONTEXT_SWITCH); + ret.context = getCurrentContext(); + } + LPAREN! expression RPAREN! LCURLY! + ( + casesGroupComplexity = colonCasesGroup[flag] + { + ret.complexity += casesGroupComplexity; + } + )* + { + exitContext(); + } + rc:RCURLY! + ; + +/** + * A switch statement or expression containing one or more "case ->" or "default ->" conditions. + */ +lambdaSwitchExpression [CloverToken owningLabel] returns [ContextSetAndComplexity ret] { int caseComplexity = 0; ContextSet saveContext = getCurrentContext(); CloverToken tmp = null; - complexity = 0; + ret = new ContextSetAndComplexity(); } : sw:SWITCH { tmp = ct(sw); enterContext(ContextStore.CONTEXT_SWITCH); - saveContext = getCurrentContext(); + ret.context = getCurrentContext(); } LPAREN! expression RPAREN! LCURLY! ( caseComplexity = lambdaCase[saveContext] { - complexity += caseComplexity; + ret.complexity += caseComplexity; } )+ { diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy index 8d5abf5a..8a1703bd 100755 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy @@ -62,9 +62,9 @@ abstract class JavaSyntaxCompilationTestBase { protected final String R_LAMBDA_INC_RIGHT = ",[0-9]+\\)" /** Regular expression for case expression returning value */ - protected final String R_CASE_EXPRESSION_LEFT = "{" + R_INC + "yield " - protected final String R_CASE_EXPRESSION_LEFT_VOID = "{" + R_INC - protected final String R_CASE_EXPRESSION_RIGHT = "}" + protected final String R_CASE_EXPRESSION_LEFT = "\\{" + R_INC + "yield " + protected final String R_CASE_EXPRESSION_LEFT_VOID = "\\{" + R_INC + protected final String R_CASE_EXPRESSION_RIGHT = "\\}" protected File mTestcasesSrcDir private File mOutputDir diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java index 2403ef2e..d58dbd8a 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java @@ -1,13 +1,21 @@ public class Java14SwitchExpressionInVariousContexts { - static void switchExpressionInAssignment(int j) { + static void colonSwitchExpressionInAssignment(int j) { + int k = switch (j) { + case 0: yield 10; + case 1: yield 11; + default: yield 12; + }; + } + + static void lambdaSwitchExpressionInAssignment(int j) { int k = switch (j) { case 0 -> 10; default -> 11; }; } - static void switchExpressionInExpression(int j) { + static void lambdaSwitchExpressionInExpression(int j) { int k = 8 + (switch (j) { case 0 -> 20; default -> 21; @@ -21,21 +29,21 @@ static void switchExpressionInExpression(int j) { } } - static void switchExpressionAsMethodArgument(int k) { + static void lambdaSwitchExpressionAsMethodArgument(int k) { foo(switch (k) { case 10 -> 100; default -> 200; }); } - static void switchExpressionAsMethodStatement(int kk) { + static void lambdsSwitchExpressionAsMethodStatement(int kk) { switch (kk) { case 77 -> System.out.println("77"); default -> System.out.println("not 77"); } } - // switch expression in an initializer block + // lambda switch expression in an initializer block int kkk = 88; { switch (kkk) { @@ -51,6 +59,7 @@ static void switchExpressionInThrowWithThrow(int j) { default -> throw new IllegalArgumentException("unsupported"); }; } + static void foo(int i) { } } diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java index 6000cde4..20c3e694 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithCaseColonFailed.java @@ -5,6 +5,7 @@ public class Java14SwitchExpressionWithCaseColonFailed { static void breakInSwitchExpression() { // NOT ALLOWED - break out of switch expression is not allowed + // + switch expression does not have any result expressions int i = 0, j; j = switch (i) { case 0: From c0c0fa6dc030d322049c6133ae9952ab1e541d46 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 17 Jan 2024 23:47:53 +0100 Subject: [PATCH 16/29] OC-126: new LanguageFeature.SWITCH_EXPRESSIONS added for Java 14+; the RecorderInstrEmitter generates two helper caseInc() methods --- .../cfg/instr/java/LanguageFeature.java | 4 +- .../clover/cfg/instr/java/SourceLevel.java | 9 ++-- .../instr/java/RecorderInstrEmitter.java | 45 +++++++++++++++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/LanguageFeature.java b/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/LanguageFeature.java index eeb318b1..d1b4fb93 100644 --- a/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/LanguageFeature.java +++ b/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/LanguageFeature.java @@ -8,5 +8,7 @@ public enum LanguageFeature { /** Multi-line text blocks in """...""" */ TEXT_BLOCKS, /** Record classes and compact canonical constructors */ - RECORDS + RECORDS, + /** Switch treated as expressions */ + SWITCH_EXPRESSIONS } diff --git a/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/SourceLevel.java b/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/SourceLevel.java index 69f0c101..f58fe05e 100644 --- a/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/SourceLevel.java +++ b/clover-core/src/main/java/com/atlassian/clover/cfg/instr/java/SourceLevel.java @@ -10,6 +10,7 @@ import static com.atlassian.clover.cfg.instr.java.LanguageFeature.LAMBDA; import static com.atlassian.clover.cfg.instr.java.LanguageFeature.MODULES; import static com.atlassian.clover.cfg.instr.java.LanguageFeature.RECORDS; +import static com.atlassian.clover.cfg.instr.java.LanguageFeature.SWITCH_EXPRESSIONS; import static com.atlassian.clover.cfg.instr.java.LanguageFeature.TEXT_BLOCKS; import static org.openclover.util.Sets.newHashSet; @@ -24,10 +25,10 @@ public enum SourceLevel { JAVA_11("11", newHashSet("1.11", "11"), newHashSet(LAMBDA, MODULES)), JAVA_12("12", newHashSet("12"), newHashSet(LAMBDA, MODULES)), JAVA_13("13", newHashSet("13"), newHashSet(LAMBDA, MODULES)), - JAVA_14("14", newHashSet("14"), newHashSet(LAMBDA, MODULES)), - JAVA_15("15", newHashSet("15"), newHashSet(LAMBDA, MODULES, TEXT_BLOCKS)), - JAVA_16("16", newHashSet("16"), newHashSet(LAMBDA, MODULES, TEXT_BLOCKS, RECORDS)), - JAVA_17("17", newHashSet("17"), newHashSet(LAMBDA, MODULES, TEXT_BLOCKS, RECORDS)); + JAVA_14("14", newHashSet("14"), newHashSet(LAMBDA, MODULES, SWITCH_EXPRESSIONS)), + JAVA_15("15", newHashSet("15"), newHashSet(LAMBDA, MODULES, SWITCH_EXPRESSIONS, TEXT_BLOCKS)), + JAVA_16("16", newHashSet("16"), newHashSet(LAMBDA, MODULES, SWITCH_EXPRESSIONS, TEXT_BLOCKS, RECORDS)), + JAVA_17("17", newHashSet("17"), newHashSet(LAMBDA, MODULES, SWITCH_EXPRESSIONS, TEXT_BLOCKS, RECORDS)); private static final Set unsupportedSourceLevels = newHashSet("1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "5", "1.6", "6"); diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/RecorderInstrEmitter.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/RecorderInstrEmitter.java index 6249bd39..b53cecda 100755 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/RecorderInstrEmitter.java +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/RecorderInstrEmitter.java @@ -24,11 +24,10 @@ public class RecorderInstrEmitter extends Emitter { static final String LAMBDA_INC_METHOD = "lambdaInc"; + static final String CASE_INC_METHOD = "caseInc"; private static final String INCOMPATIBLE_MSG = - "[CLOVER] WARNING: The Clover version used in instrumentation does " + - "not match the runtime version. You need to run instrumented classes against the same version of Clover " + - "that you instrumented with."; + "[CLOVER] WARNING: The Clover version used in instrumentation shall match the runtime version."; private static final String DEFAULT_CLASSNOTFOUND_MSG = "[CLOVER] FATAL ERROR: Clover could not be initialised. Are you " + "sure you have Clover in the runtime classpath?"; @@ -78,6 +77,7 @@ private boolean isJUnit5ParameterizedTest() { private boolean shouldEmitWarningMethod; private List profiles; private boolean areLambdasSupported; + private boolean areSwitchExpressionsSupported; public RecorderInstrEmitter(boolean isEnum) { super(); @@ -97,6 +97,7 @@ public void init(InstrumentationState state) { registryVersion = state.getSession().getVersion(); javaLangPrefix = state.getCfg().getJavaLangPrefix(); areLambdasSupported = state.getCfg().getSourceLevel().supportsFeature(LanguageFeature.LAMBDA); + areSwitchExpressionsSupported = state.getCfg().getSourceLevel().supportsFeature(LanguageFeature.SWITCH_EXPRESSIONS); testClass = state.isDetectTests(); isSpockTestClass = state.isSpockTestClass(); isParameterizedJUnitTestClass = state.isParameterizedJUnitTestClass(); @@ -131,6 +132,12 @@ public String getInstr() { instrString += generateLambdaIncMethod(recorderSuffix); } + // add caseInc() wrappers for switch case written as expressions + if (areSwitchExpressionsSupported) { + instrString += generateCaseIncValueMethod(recorderSuffix); + instrString += generateCaseIncVoidMethod(recorderSuffix); + } + // static initialization block instrString += "static{"; @@ -300,6 +307,38 @@ private String generateLambdaIncMethod(final String recorderSuffix) { return str.toString(); } + /** + * Generates caseInc helper method to wrap case expressions returning values. + *
+     *     public static  T caseInc(int i, java.util.function.Supplier s) {
+     *         inc(i);
+     *         return s.get();
+     *     }
+     * 
+ * @param recorderSuffix name of the coverage recorder field + * @return String code of the method + */ + private String generateCaseIncValueMethod(String recorderSuffix) { + return "public static T caseInc(int i,java.util.function.Supplier s){" + + recorderSuffix + ".inc(i);return s.get();}"; + } + + /** + * Generates caseInc helper method to wrap case expressions returning values. + *
+     *     public static void caseInc(int i, Runnable r) {
+     *         inc(i);
+     *         r.run();
+     *     }
+     * 
+ * @param recorderSuffix name of the coverage recorder field + * @return String code of the method + */ + private String generateCaseIncVoidMethod(String recorderSuffix) { + return "public static void caseInc(int i,Runnable r){" + + recorderSuffix + ".inc(i);r.run();}"; + } + /** * Return a string containing declaration of a field with a static array containing * list of profiles. Example: From e91b382cef55d66f4b644696d45f77ade4cb1de5 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 17 Jan 2024 23:50:34 +0100 Subject: [PATCH 17/29] OC-126: created code entry/exit emitters for case expressions; value-returning and void expressions are wrapped inside helper caseInc(); throw expressions are converted to blocks with R.inc() before throw --- .../java/CaseExpressionEntryEmitter.java | 90 +++++++++++++++++++ .../instr/java/CaseExpressionExitEmitter.java | 33 +++++++ .../java/CaseThrowExpressionEntryEmitter.java | 74 +++++++++++++++ .../java/CaseThrowExpressionExitEmitter.java | 36 ++++++++ 4 files changed, 233 insertions(+) create mode 100644 clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionEntryEmitter.java create mode 100644 clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionExitEmitter.java create mode 100644 clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionEntryEmitter.java create mode 100644 clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionExitEmitter.java diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionEntryEmitter.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionEntryEmitter.java new file mode 100644 index 00000000..473a39eb --- /dev/null +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionEntryEmitter.java @@ -0,0 +1,90 @@ +package com.atlassian.clover.instr.java; + +import com.atlassian.clover.context.ContextSet; +import com.atlassian.clover.context.NamedContext; +import com.atlassian.clover.registry.FixedSourceRegion; +import com.atlassian.clover.registry.entities.FullStatementInfo; +import com.atlassian.clover.spi.lang.LanguageConstruct; + +/** + * A code emitter for lambda case expressions. Rewrites an expression like: + *
+ *    case 0 -> "abc";
+ *              ^    ^
+ * 
+ * into: + *
+ *    case 0 -> caseInc(123, () -> "abc";
+ *              ~~~~~~~~~~~~~~~~~~ ^    ^
+ * 
+ *

+ * {@link CaseExpressionExitEmitter} + */ +public class CaseExpressionEntryEmitter extends Emitter { + + /** + * A line number where expression ends (position of a semicolon) + */ + private final int endLine; + + /** + * A column number where expression ends (position of a semicolon) + */ + private final int endCol; + + /** + * A complexity of the entire expression. + * It can be non-0 if a ternary expression or another switch expression is present in it. + */ + private final int complexity; + + /** + * A statement registered for this case expression. + */ + FullStatementInfo stmtInfo; + + public CaseExpressionEntryEmitter(ContextSet context, int line, int column, int endLine, int endCol, int complexity) { + super(context, line, column); + this.endLine = endLine; + this.endCol = endCol; + this.complexity = complexity; + } + + @Override + protected void init(InstrumentationState state) { + if (state.isInstrEnabled()) { + state.setDirty(); + + // record the statement + stmtInfo = state.getSession().addStatement( + getElementContext(), + new FixedSourceRegion(getLine(), getColumn(), endLine, endCol), + complexity, + LanguageConstruct.Builtin.STATEMENT); + + boolean classInstrStrategy = state.getCfg().isClassInstrStrategy(); + if (classInstrStrategy) { + // emit text like [__CLRxxxxxxxx.caseInc(123,()->] + final String recorderBase = state.getRecorderPrefix().substring(0, state.getRecorderPrefix().lastIndexOf('.')); + final StringBuilder instr = new StringBuilder(); + instr.append(recorderBase); + instr.append("."); + instr.append(RecorderInstrEmitter.CASE_INC_METHOD); + instr.append("("); + instr.append(stmtInfo.getDataIndex()); + // add a comma and lambda call, because we'll have original case expression wrapped in a lambda as a second argument + instr.append(",()->"); + setInstr(instr.toString()); + } + } + } + + @Override + public void addContext(NamedContext ctx) { + super.addContext(ctx); + if (stmtInfo != null) { + stmtInfo.addContext(ctx); + } + } + +} diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionExitEmitter.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionExitEmitter.java new file mode 100644 index 00000000..1997ecdf --- /dev/null +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseExpressionExitEmitter.java @@ -0,0 +1,33 @@ +package com.atlassian.clover.instr.java; + +/** + * A code emitter for lambda case expressions. Rewrites an expression like: + *

+ *    case 0 -> "abc";
+ *              ^    ^
+ * 
+ * into: + *
+ *    case 0 -> "abc");
+ *              ^    ~^
+ * 
+ * + * {@link CaseExpressionEntryEmitter} + */ +public class CaseExpressionExitEmitter extends Emitter { + + private final CaseExpressionEntryEmitter entryEmitter; + + public CaseExpressionExitEmitter(CaseExpressionEntryEmitter entryEmitter) { + this.entryEmitter = entryEmitter; + } + + @Override + protected void init(InstrumentationState state) { + // we must close the wrapped expression only if the start was wrapped, ignoring + // any CLOVER:OFF inside (state.isInstrEnabled() check would be wrong) + if (entryEmitter.stmtInfo != null && state.getCfg().isClassInstrStrategy()) { + setInstr(")"); + } + } +} diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionEntryEmitter.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionEntryEmitter.java new file mode 100644 index 00000000..f985b7db --- /dev/null +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionEntryEmitter.java @@ -0,0 +1,74 @@ +package com.atlassian.clover.instr.java; + +import com.atlassian.clover.context.ContextSet; +import com.atlassian.clover.registry.FixedSourceRegion; +import com.atlassian.clover.registry.entities.FullStatementInfo; +import com.atlassian.clover.spi.lang.LanguageConstruct; + +import static com.atlassian.clover.instr.Bindings.$CoverageRecorder$inc; + +/** + * A code emitter for lambda case expressions with the {@code throw} keyword. + * Rewrites an expression like: + *
+ *    case 0 -> throw new Exception();
+ *              ^                    ^
+ * 
+ * into: + *
+ *    case 0 -> { R.inc(); throw new Exception();
+ *              ~~~~~~~~~~ ^                    ^
+ * 
+ * + * {@link CaseThrowExpressionExitEmitter} + */ +public class CaseThrowExpressionEntryEmitter extends Emitter { + + /** A line number where expression ends (position of a semicolon) */ + private final int endLine; + + /** A column number where expression ends (position of a semicolon) */ + private final int endCol; + + /** + * A complexity of the entire expression. + * It can be non-0 if a ternary expression or another switch expression is present in it. + */ + private final int complexity; + + /** + * A statement registered for this case throw expression. + */ + FullStatementInfo stmtInfo; + + public CaseThrowExpressionEntryEmitter(ContextSet context, int line, int column, int endLine, int endCol, int complexity) { + super(context, line, column); + this.endLine = endLine; + this.endCol = endCol; + this.complexity = complexity; + } + + @Override + protected void init(InstrumentationState state) { + if (state.isInstrEnabled()) { + state.setDirty(); + // record the statement + stmtInfo = + state.getSession().addStatement( + getElementContext(), + new FixedSourceRegion(getLine(), getColumn(), endLine, endCol), + complexity, + LanguageConstruct.Builtin.STATEMENT); + + if (state.getCfg().isClassInstrStrategy()) { + // emit text like [{__CLRxxxxxxxx.inc(123);] + final String instr = "{" + + $CoverageRecorder$inc(state.getRecorderPrefix(), Integer.toString(stmtInfo.getDataIndex())) + + ";"; + setInstr(instr); + } + } + } + + +} diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionExitEmitter.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionExitEmitter.java new file mode 100644 index 00000000..7e80864a --- /dev/null +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/CaseThrowExpressionExitEmitter.java @@ -0,0 +1,36 @@ +package com.atlassian.clover.instr.java; + +/** + * A code emitter for lambda case expressions with the {@code throw} keyword. + * Rewrites an expression like: + *
+ *    case 0 -> throw new Exception();
+ *              ^                    ^
+ * 
+ * into: + *
+ *    case 0 -> throw new Exception();}
+ *              ^                    ^~
+ * 
+ * + * {@link CaseThrowExpressionEntryEmitter} + */ +public class CaseThrowExpressionExitEmitter extends Emitter { + + private CaseThrowExpressionEntryEmitter entryEmitter; + + public CaseThrowExpressionExitEmitter(CaseThrowExpressionEntryEmitter entryEmitter) { + this.entryEmitter = entryEmitter; + } + + @Override + protected void init(InstrumentationState state) { + // we must close the wrapped expression only if the start was wrapped, ignoring + // any CLOVER:OFF inside (state.isInstrEnabled() check would be wrong) + if (entryEmitter.stmtInfo != null && state.getCfg().isClassInstrStrategy()) { + setInstr("}"); + } + } + + +} From 76ccc0f44b71a17b484ab1f927d8911103ca4faf Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Thu, 18 Jan 2024 00:30:14 +0100 Subject: [PATCH 18/29] OC-126: wire code emitters to grammar rules (DRAFT) --- .../com/atlassian/clover/instr/java/java.g | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 97031a6b..dff06546 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -386,16 +386,42 @@ tokens { } } - private void instrEnterCaseExpression(CloverToken insertionPoint, ContextSet context) { - // TODO - // insertionPoint.addPreEmitter( - // new CaseExpressionEntryEmitter(insertionPoint.getLine(), insertionPoint.getColumn()+tok.getText().length())); + private CaseExpressionEntryEmitter instrEnterCaseExpression(CloverToken insertionPoint, CloverToken endToken, ContextSet context, int complexity) { + // we add "caseInc(123,()->" AFTER the "->" + final CaseExpressionEntryEmitter entryEmitter = new CaseExpressionEntryEmitter( + context, + insertionPoint.getLine(), + insertionPoint.getColumn(), + endToken.getLine(), + endToken.getColumn(), + complexity); + insertionPoint.addPostEmitter(entryEmitter); + return entryEmitter; } - private void instrExitCaseExpression(CloverToken insertionPoint, ContextSet context) { - // TODO - // insertionPoint.addPostEmitter( - // new CaseExpressionExitEmitter(insertionPoint.getLine(), insertionPoint.getColumn()+tok.getText().length())); + private void instrExitCaseExpression(CaseExpressionEntryEmitter entryEmitter, CloverToken insertionPoint) { + // we add closing ")" BEFORE the ";" + insertionPoint.addPreEmitter( + new CaseExpressionExitEmitter(entryEmitter)); + } + + private CaseThrowExpressionEntryEmitter instrEnterCaseThrowExpression(CloverToken insertionPoint, CloverToken endToken, ContextSet context, int complexity) { + // we add "{ R.inc();" AFTER the "->" + final CaseThrowExpressionEntryEmitter entryEmitter = new CaseThrowExpressionEntryEmitter( + context, + insertionPoint.getLine(), + insertionPoint.getColumn(), + endToken.getLine(), + endToken.getColumn(), + complexity); + insertionPoint.addPostEmitter(entryEmitter); + return entryEmitter; + } + + private void instrExitCaseThrowExpression(CaseThrowExpressionEntryEmitter entryEmitter, CloverToken insertionPoint) { + // we add closing "}" AFTER the ";" + insertionPoint.addPostEmitter( + new CaseThrowExpressionExitEmitter(entryEmitter)); } private CloverToken maybeAddFlushInstr(CloverToken last) { @@ -3331,6 +3357,8 @@ lambdaCase[ContextSet context] returns [int complexity] { CloverToken endTok = null; Token pos = null; + CaseExpressionEntryEmitter expressionEntryEmitter = null; + CaseThrowExpressionEntryEmitter throwEntryEmitter = null; complexity = 1; } : @@ -3352,14 +3380,22 @@ lambdaCase[ContextSet context] returns [int complexity] ) t:LAMBDA! ( + // throwing an exception must be instrumented differently + (THROW expression SEMI) => + THROW expression SEMI { - instrEnterCaseExpression(lt(1), context); + /* TODO calculate and pass expression's complexity */ + throwEntryEmitter = instrEnterCaseThrowExpression(ct(t), lt(0), context, 0); + instrExitCaseThrowExpression(throwEntryEmitter, lt(0)); } - expression + | + // void and value-returning expressions + expression SEMI { - instrExitCaseExpression(lt(0), context); + /* TODO calculate and pass expression's complexity */ + expressionEntryEmitter = instrEnterCaseExpression(ct(t), lt(0), context, 0); + instrExitCaseExpression(expressionEntryEmitter, lt(0)); } - SEMI! | // no need for special instrumentation, we will instrument it like a simple { } block, inside endTok=compoundStatement From 956e6b1bcce3c3fd9c22248251ce8ab2ee6396fe Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Fri, 19 Jan 2024 00:41:34 +0100 Subject: [PATCH 19/29] OC-126: fix tests --- .../clover/JavaSyntax14CompilationTest.groovy | 143 ++++++++++-------- .../JavaSyntaxCompilationTestBase.groovy | 8 +- 2 files changed, 83 insertions(+), 68 deletions(-) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index 61f04231..a79a9aca 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -1,12 +1,15 @@ package com.atlassian.clover import com.atlassian.clover.util.JavaEnvUtils +import org.apache.tools.ant.BuildException import org.junit.Before -import org.junit.Ignore import org.junit.Test import static java.util.regex.Pattern.quote +import static org.hamcrest.MatcherAssert.assertThat +import static org.hamcrest.Matchers.isA import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail import static org.junit.Assume.assumeTrue /** @@ -27,18 +30,20 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { } @Test - void switchExpressionWithCaseWithColonCanUseYield() { + void switchStatementWithCaseWithColonCannotUseYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14SwitchExpressionWithCaseColon.java" - instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - - // using yield in switch expressions is allowed also for "case X:" form - assertFileMatches(fileName, quote("case 0:") + "\\w*" + R_INC + quote("yield 10;")) - assertFileMatches(fileName, quote("default:") + "\\w*" + R_INC + quote("yield 11;")) - assertFileMatches(fileName, quote("case 10:") + "\\w*" + R_INC + quote("yield 20;")) - assertFileMatches(fileName, quote("case 11:") + "\\w*" + R_INC + quote("yield 21;")) - assertFileMatches(fileName, quote("default:") + "\\w*" + R_INC + quote("yield 22;")) + final String fileName = "Java14SwitchStatementWithCaseColonFailed.java" + final File srcFile = new File(srcDir, fileName) + try { + // it's not allowed to use yield in switch statements (a value returned cannot be ignored) + // OpenClover grammar parser is simplified and allows 'yield' in place where any statement is allowed + // but the javac catches this + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + fail("Compilation should have failed") + } catch (Exception ex) { + assertThat(ex, isA(BuildException.class)) + } } @Test @@ -47,38 +52,59 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { final String fileName = "Java14SwitchExpressionWithCaseColonFailed.java" final File srcFile = new File(srcDir, fileName) + try { + // using break in switch expressions is NOT allowed for "case X:" form + // a reason is that a switch expression must return a value and break returns no value + // OpenClover is not so restrictive and instrumentation succeeds but javac catches this + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) + fail("Compilation should have failed") + } catch (Exception ex) { + assertThat(ex, isA(BuildException.class)) + } + } + + @Test + void testSwitchCaseWithColonsMixedWithExpressions() { + assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) + + final String fileName = "Java14SwitchExpressionMixedCasesFailed.java" + final File srcFile = new File(srcDir, fileName) int returnCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_14, [] as String[]) - // using break in switch expressions is NOT allowed for "case X:" form - // a reason is that a switch expression must return a value and break returns no value + // it's not allowed to mix "case X:" with "case X ->" in the same switch statement + // OpenClover's parser should catch this assertEquals(1, returnCode) } @Test - void switchStatementWithCaseWithColonCanUseBreak() { + void switchExpressionWithCaseWithColonCanUseYield() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14SwitchStatementWithCaseColon.java" + final String fileName = "Java14SwitchExpressionWithCaseColon.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - // this is a regression test, using break in switch statements must be allowed - assertFileMatches(fileName, R_INC + quote("k++;") + R_INC + quote("break;")) - assertFileMatches(fileName, quote("default:") + ".*" + R_INC + quote("break;")) + // using yield in switch expressions is allowed also for "case X:" form + // notice there are two R_INC - one for entering the case, one for the statement + assertFileMatches(fileName, quote("case 0:") + "\\s*" + R_INC + "\\s*" + R_INC + quote("yield 10;")) + assertFileMatches(fileName, quote("default:") + "\\s*" + R_INC + "\\s*" + R_INC + quote("yield 11;")) + assertFileMatches(fileName, quote("case 10:") + "\\s*" + R_INC + "\\s*" + R_INC + quote("yield 20;")) + assertFileMatches(fileName, quote("case 11:") + "\\s*" + R_INC + "\\s*" + R_INC + quote("yield 21;")) + assertFileMatches(fileName, quote("default:") + "\\s*" + R_INC + "\\s*" + R_INC + quote("yield 22;")) } - @Ignore("Current grammar rule is too loose, and allows use of 'yield' in place where any statement is allowed") @Test - void switchStatementWithCaseWithColonCannotUseYield() { + void switchStatementWithCaseWithColonCanUseBreak() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - final String fileName = "Java14SwitchStatementWithCaseColonFailed.java" - final File srcFile = new File(srcDir, fileName) - int returnCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_14, [] as String[]) + final String fileName = "Java14SwitchStatementWithCaseColon.java" + instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - // it's not allowed to use yield in switch statements (a value returned cannot be ignored) - assertEquals(1, returnCode) + // this is a regression test, using break in switch statements must be allowed + assertFileMatches(fileName, R_INC + quote("k++;") + R_INC + quote("break;")) + assertFileMatches(fileName, quote("default:") + ".*" + R_INC + quote("break;")) } + @Test void switchExpressionWithCaseAndDefaultCanUseLambdasReturningValues() { assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) @@ -87,12 +113,12 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // switch expression with value-returning lambdas - assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT + quote("\"red\";") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT + quote("\"green\";") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT + quote("\"blue\";") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.EVEN;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.ODD;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.UNKNOWN;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT + quote("\"red\";") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT + quote("\"green\";") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT + quote("\"blue\";") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.EVEN;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.ODD;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.UNKNOWN;") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -103,17 +129,17 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // switch expression with void lambdas - assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT_VOID + + assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT + quote("System.out.println(\"red\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT_VOID + + assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT + quote("System.out.println(\"green\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT_VOID + + assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT + quote("System.out.println(\"blue\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT_VOID + + assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("System.out.println(EvenOrOdd.EVEN);") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT_VOID + + assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT + quote("System.out.println(EvenOrOdd.ODD);") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT_VOID + + assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("System.out.println(EvenOrOdd.UNKNOWN);") + R_CASE_EXPRESSION_RIGHT) } @@ -124,17 +150,17 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { final String fileName = "Java14SwitchExpressionCaseAndDefaultWithThrows.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_THROW_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_THROW_EXPRESSION_RIGHT) assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("0;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"zero\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"anything\");") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_THROW_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_THROW_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_THROW_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"zero\");") + R_CASE_THROW_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default -> ") + R_CASE_THROW_EXPRESSION_LEFT + + quote("throw new IllegalArgumentException(\"anything\");") + R_CASE_THROW_EXPRESSION_RIGHT) } @Test @@ -221,28 +247,15 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // a standalone statement in method - assertFileMatches(fileName, quote("") + R_CASE_EXPRESSION_LEFT + - quote("") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, R_INC + quote("switch (kk) {") + "\\s+" + + quote("case 77 ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(\"77\")") + R_CASE_EXPRESSION_RIGHT + ";") // instance initializer block - assertFileMatches(fileName, quote("{") + "\\b+" + - R_INC + quote("switch (kkk) {") + R_CASE_EXPRESSION_LEFT + - quote("") + R_CASE_EXPRESSION_RIGHT) - - // static block - assertFileMatches(fileName, quote("") + R_CASE_EXPRESSION_LEFT + - quote("") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, + R_INC + quote("switch (kkk) {") + "\\s+" + + quote("case 88 ->") + R_CASE_EXPRESSION_LEFT + quote(" System.out.println(\"88\")") + R_CASE_EXPRESSION_RIGHT + ";" + "\\s+" + + quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" System.out.println(\"not 88\")") + R_CASE_EXPRESSION_RIGHT + ";") } - @Test - void testSwitchCaseWithColonsMixedWithExpressions() { - assumeTrue(JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_14)) - - final String fileName = "Java14SwitchExpressionMixedCasesFailed.java" - final File srcFile = new File(srcDir, fileName) - int returnCode = instrumentSourceFileNoAssert(srcFile, JavaEnvUtils.JAVA_14, [] as String[]) - - // it's not allowed to mix "case X:" with "case X ->" in the same switch statement - assertEquals(1, returnCode) - } } diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy index 8a1703bd..ae95e86f 100755 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy @@ -62,9 +62,11 @@ abstract class JavaSyntaxCompilationTestBase { protected final String R_LAMBDA_INC_RIGHT = ",[0-9]+\\)" /** Regular expression for case expression returning value */ - protected final String R_CASE_EXPRESSION_LEFT = "\\{" + R_INC + "yield " - protected final String R_CASE_EXPRESSION_LEFT_VOID = "\\{" + R_INC - protected final String R_CASE_EXPRESSION_RIGHT = "\\}" + protected final String R_CASE_EXPRESSION_LEFT = "__CLR[a-zA-Z0-9_]+\\.caseInc\\([0-9]+,\\(\\)->" + protected final String R_CASE_EXPRESSION_RIGHT = "\\)" + + protected final String R_CASE_THROW_EXPRESSION_LEFT = "\\{" + R_INC + protected final String R_CASE_THROW_EXPRESSION_RIGHT = "\\}" protected File mTestcasesSrcDir private File mOutputDir From de4c1fa68c661d40e64e470f0006eefd487b14ba Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Fri, 19 Jan 2024 00:58:49 +0100 Subject: [PATCH 20/29] OC-126: fix tests --- .../clover/JavaSyntax14CompilationTest.groovy | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index a79a9aca..c0d62a59 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -207,11 +207,10 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) assertFileMatches(fileName, quote("case 0, 1, 2*3 -> ") + R_CASE_EXPRESSION_LEFT + quote("10;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("11;") + R_CASE_EXPRESSION_RIGHT) assertFileMatches(fileName, quote("case 0, 1, 2 -> ") + R_CASE_EXPRESSION_LEFT + quote("20;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case null -> ") + R_CASE_EXPRESSION_LEFT + quote("21;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0, 1, 2 -> ") + R_CASE_EXPRESSION_LEFT + quote("30;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case null, default -> ") + R_CASE_EXPRESSION_LEFT + quote("31;") + R_CASE_EXPRESSION_RIGHT) +// TODO pattern matching since JDK21 +// assertFileMatches(fileName, quote("case null -> ") + R_CASE_EXPRESSION_LEFT + quote("21;") + R_CASE_EXPRESSION_RIGHT) +// assertFileMatches(fileName, quote("case null, default -> ") + R_CASE_EXPRESSION_LEFT + quote("31;") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -226,16 +225,17 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { // argument of a method call assertFileMatches(fileName, R_INC + quote("foo(switch (k) {")) // no R_INC before switch - assertFileMatches(fileName, quote("case 10 -> ") + R_CASE_EXPRESSION_LEFT + "100;" + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + "200;" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 10 ->") + R_CASE_EXPRESSION_LEFT + " 100" + R_CASE_EXPRESSION_RIGHT + ";") + assertFileMatches(fileName, quote("default ->") + R_CASE_EXPRESSION_LEFT + " 200" + R_CASE_EXPRESSION_RIGHT + ";") // part of an expression - assertFileMatches(fileName, quote("") + R_CASE_EXPRESSION_LEFT + - quote("if (switch (j) {") + ".*" + - quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("30;") + R_CASE_EXPRESSION_RIGHT + - ".*" + + assertFileMatches(fileName, + // we have '((((' because of the branch evaluation + R_INC + quote("if ((((switch (j) {") + "\\s+" + + quote("case 0 ->") + R_CASE_EXPRESSION_LEFT + quote(" 30") + R_CASE_EXPRESSION_RIGHT + ";" + "\\s+" + + quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" 31") + R_CASE_EXPRESSION_RIGHT + ";" + "\\s+" + quote("} % 10 == 0)") + - quote(")&&") + R_IGET_TRUE // part of the branch coverage expression + quote("&&(") + R_IGET // part of the branch coverage expression ) } From 698c1239cf94eb3cb958f6e68c5f7f51506a2b27 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Fri, 19 Jan 2024 09:40:43 +0100 Subject: [PATCH 21/29] OC-126: java.g - allow to use expressions in 'case' label and not just single values (JLS specifies it shall be a constant expression, e.g. without method calls, but we're not restricting this); JavaSyntaxCompilationTestBase - change R_CASE_EXPRESSION_RIGHT to simplify asserts; Java14SwitchExpressionWithMultiValueCase - correct test case as it failed on not covering all cases in switch blocks; JavaSyntax14CompilationTest - correct assertions in test cases (mainly placement of space characters) --- .../com/atlassian/clover/instr/java/java.g | 4 +- .../clover/JavaSyntax14CompilationTest.groovy | 76 +++++++++---------- .../JavaSyntaxCompilationTestBase.groovy | 2 +- ...a14SwitchExpressionWithMultiValueCase.java | 2 + 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index dff06546..babc32af 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -3405,7 +3405,9 @@ lambdaCase[ContextSet context] returns [int complexity] patternMatch : // just constants, string literals, true/false, null etc - primaryExpressionPart + // can be more than one, separated by comma + conditionalExpression + (COMMA conditionalExpression)* ; /** diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy index c0d62a59..f7e39af1 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntax14CompilationTest.groovy @@ -113,12 +113,12 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // switch expression with value-returning lambdas - assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT + quote("\"red\";") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT + quote("\"green\";") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT + quote("\"blue\";") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.EVEN;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.ODD;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + quote("EvenOrOdd.UNKNOWN;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case R ->") + R_CASE_EXPRESSION_LEFT + quote(" \"red\"") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case G ->") + R_CASE_EXPRESSION_LEFT + quote(" \"green\"") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case B ->") + R_CASE_EXPRESSION_LEFT + quote(" \"blue\"") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 ->") + R_CASE_EXPRESSION_LEFT + quote(" EvenOrOdd.EVEN") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 1 ->") + R_CASE_EXPRESSION_LEFT + quote(" EvenOrOdd.ODD") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" EvenOrOdd.UNKNOWN") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -129,18 +129,18 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) // switch expression with void lambdas - assertFileMatches(fileName, quote("case R -> ") + R_CASE_EXPRESSION_LEFT + - quote("System.out.println(\"red\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case G -> ") + R_CASE_EXPRESSION_LEFT + - quote("System.out.println(\"green\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case B -> ") + R_CASE_EXPRESSION_LEFT + - quote("System.out.println(\"blue\");") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_EXPRESSION_LEFT + - quote("System.out.println(EvenOrOdd.EVEN);") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 1 -> ") + R_CASE_EXPRESSION_LEFT + - quote("System.out.println(EvenOrOdd.ODD);") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + - quote("System.out.println(EvenOrOdd.UNKNOWN);") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case R ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(\"red\")") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case G ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(\"green\")") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case B ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(\"blue\")") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(EvenOrOdd.EVEN)") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 1 ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(EvenOrOdd.ODD)") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default ->") + R_CASE_EXPRESSION_LEFT + + quote(" System.out.println(EvenOrOdd.UNKNOWN)") + R_CASE_EXPRESSION_RIGHT) } @Test @@ -150,17 +150,17 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { final String fileName = "Java14SwitchExpressionCaseAndDefaultWithThrows.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_THROW_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_THROW_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_EXPRESSION_LEFT + - quote("0;") + R_CASE_EXPRESSION_RIGHT) - - assertFileMatches(fileName, quote("case -1 -> ") + R_CASE_THROW_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"negative\");") + R_CASE_THROW_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0 -> ") + R_CASE_THROW_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"zero\");") + R_CASE_THROW_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("default -> ") + R_CASE_THROW_EXPRESSION_LEFT + - quote("throw new IllegalArgumentException(\"anything\");") + R_CASE_THROW_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case -1 ->") + R_CASE_THROW_EXPRESSION_LEFT + + quote(" throw new IllegalArgumentException(\"negative\");") + R_CASE_THROW_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default ->") + R_CASE_EXPRESSION_LEFT + + quote(" 0") + R_CASE_EXPRESSION_RIGHT) + + assertFileMatches(fileName, quote("case -1 ->") + R_CASE_THROW_EXPRESSION_LEFT + + quote(" throw new IllegalArgumentException(\"negative\");") + R_CASE_THROW_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0 ->") + R_CASE_THROW_EXPRESSION_LEFT + + quote(" throw new IllegalArgumentException(\"zero\");") + R_CASE_THROW_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default ->") + R_CASE_THROW_EXPRESSION_LEFT + + quote(" throw new IllegalArgumentException(\"anything\");") + R_CASE_THROW_EXPRESSION_RIGHT) } @Test @@ -206,8 +206,8 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { final String fileName = "Java14SwitchExpressionWithMultiValueCase.java" instrumentAndCompileSourceFile(srcDir, mGenSrcDir, fileName, JavaEnvUtils.JAVA_14) - assertFileMatches(fileName, quote("case 0, 1, 2*3 -> ") + R_CASE_EXPRESSION_LEFT + quote("10;") + R_CASE_EXPRESSION_RIGHT) - assertFileMatches(fileName, quote("case 0, 1, 2 -> ") + R_CASE_EXPRESSION_LEFT + quote("20;") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0, 1, 2*3 ->") + R_CASE_EXPRESSION_LEFT + quote(" 10") + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("case 0, 1, 2 ->") + R_CASE_EXPRESSION_LEFT + quote(" 20") + R_CASE_EXPRESSION_RIGHT) // TODO pattern matching since JDK21 // assertFileMatches(fileName, quote("case null -> ") + R_CASE_EXPRESSION_LEFT + quote("21;") + R_CASE_EXPRESSION_RIGHT) // assertFileMatches(fileName, quote("case null, default -> ") + R_CASE_EXPRESSION_LEFT + quote("31;") + R_CASE_EXPRESSION_RIGHT) @@ -225,15 +225,15 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { // argument of a method call assertFileMatches(fileName, R_INC + quote("foo(switch (k) {")) // no R_INC before switch - assertFileMatches(fileName, quote("case 10 ->") + R_CASE_EXPRESSION_LEFT + " 100" + R_CASE_EXPRESSION_RIGHT + ";") - assertFileMatches(fileName, quote("default ->") + R_CASE_EXPRESSION_LEFT + " 200" + R_CASE_EXPRESSION_RIGHT + ";") + assertFileMatches(fileName, quote("case 10 ->") + R_CASE_EXPRESSION_LEFT + " 100" + R_CASE_EXPRESSION_RIGHT) + assertFileMatches(fileName, quote("default ->") + R_CASE_EXPRESSION_LEFT + " 200" + R_CASE_EXPRESSION_RIGHT) // part of an expression assertFileMatches(fileName, // we have '((((' because of the branch evaluation R_INC + quote("if ((((switch (j) {") + "\\s+" + - quote("case 0 ->") + R_CASE_EXPRESSION_LEFT + quote(" 30") + R_CASE_EXPRESSION_RIGHT + ";" + "\\s+" + - quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" 31") + R_CASE_EXPRESSION_RIGHT + ";" + "\\s+" + + quote("case 0 ->") + R_CASE_EXPRESSION_LEFT + quote(" 30") + R_CASE_EXPRESSION_RIGHT + "\\s+" + + quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" 31") + R_CASE_EXPRESSION_RIGHT + "\\s+" + quote("} % 10 == 0)") + quote("&&(") + R_IGET // part of the branch coverage expression ) @@ -249,13 +249,13 @@ class JavaSyntax14CompilationTest extends JavaSyntaxCompilationTestBase { // a standalone statement in method assertFileMatches(fileName, R_INC + quote("switch (kk) {") + "\\s+" + quote("case 77 ->") + R_CASE_EXPRESSION_LEFT + - quote(" System.out.println(\"77\")") + R_CASE_EXPRESSION_RIGHT + ";") + quote(" System.out.println(\"77\")") + R_CASE_EXPRESSION_RIGHT) // instance initializer block assertFileMatches(fileName, R_INC + quote("switch (kkk) {") + "\\s+" + - quote("case 88 ->") + R_CASE_EXPRESSION_LEFT + quote(" System.out.println(\"88\")") + R_CASE_EXPRESSION_RIGHT + ";" + "\\s+" + - quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" System.out.println(\"not 88\")") + R_CASE_EXPRESSION_RIGHT + ";") + quote("case 88 ->") + R_CASE_EXPRESSION_LEFT + quote(" System.out.println(\"88\")") + R_CASE_EXPRESSION_RIGHT + "\\s+" + + quote("default ->") + R_CASE_EXPRESSION_LEFT + quote(" System.out.println(\"not 88\")") + R_CASE_EXPRESSION_RIGHT) } } diff --git a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy index ae95e86f..b9051a95 100755 --- a/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/JavaSyntaxCompilationTestBase.groovy @@ -63,7 +63,7 @@ abstract class JavaSyntaxCompilationTestBase { /** Regular expression for case expression returning value */ protected final String R_CASE_EXPRESSION_LEFT = "__CLR[a-zA-Z0-9_]+\\.caseInc\\([0-9]+,\\(\\)->" - protected final String R_CASE_EXPRESSION_RIGHT = "\\)" + protected final String R_CASE_EXPRESSION_RIGHT = "\\);" protected final String R_CASE_THROW_EXPRESSION_LEFT = "\\{" + R_INC protected final String R_CASE_THROW_EXPRESSION_RIGHT = "\\}" diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java index 6f8cb1de..fc007c6f 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionWithMultiValueCase.java @@ -16,6 +16,7 @@ static void switchExpressionWithNullAsCase(int i) { // note: pattern matching can be used since Java 17 preview // TODO: enable this test for Java 21 // case null -> 21; + default -> 21; }; } @@ -27,6 +28,7 @@ static void switchExpressionWithNullAndDefaultAsCase(int i) { // note: pattern matching can be used since Java 17 preview // TODO: enable this test for Java 21 // case null, default -> 31; + default -> 31; }; } } From 5db65cc8cafca4160ae82790bf3e715e5e3c953d Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Sat, 20 Jan 2024 17:40:58 +0100 Subject: [PATCH 22/29] OC-126: create a grammar to handle 'constant expressions' (see java language specification), they're used in case labels in both forms of the switch expression/statement --- .../com/atlassian/clover/instr/java/java.g | 116 ++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index babc32af..a3222730 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -1961,6 +1961,7 @@ constructorBody[MethodSignature signature, CloverToken start, CloverToken endSig explicitConstructorInvocation returns [CloverToken t] { t = null; + ContextSetAndComplexity cc = null; } : ( @@ -1986,7 +1987,7 @@ explicitConstructorInvocation returns [CloverToken t] | // (new Outer()).super() (create enclosing instance) - primaryExpressionPart + cc=primaryExpressionPart (DOT! THIS)? // HACK see CCD-264 - explicit ctor invocation can have form ClassName.this.super(..) DOT! pos3:SUPER! lp3:LPAREN^ argList RPAREN! t3:SEMI! { @@ -2584,7 +2585,7 @@ colonCase[FlagDeclEmitter flag] returns [int complexity] { constExpr = true; } - expression + constantExpression { constExpr = false; pos = si1; @@ -2994,7 +2995,6 @@ expressionList : expression (COMMA! expression)* ; - // assignment expression (level 13) assignmentExpression : @@ -3199,13 +3199,14 @@ postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] * reference). */ CloverToken startMethodReference = null; + ContextSetAndComplexity cc = null; } : { // we might start a method reference, remember this token startMethodReference = lt(1); } - primaryExpressionPart // start with a primary part like constant or identifier + cc=primaryExpressionPart // start with a primary part like constant or identifier supplementaryExpressionPart[classCastStart, classCastEnd, startMethodReference] // add extra stuff like array indexes, this/super (optional) ( (methodReferencePredicate) => @@ -3404,10 +3405,10 @@ lambdaCase[ContextSet context] returns [int complexity] patternMatch : - // just constants, string literals, true/false, null etc + // just constants, string literals, true/false, null etc; also constant expressions // can be more than one, separated by comma - conditionalExpression - (COMMA conditionalExpression)* + constantExpression + (COMMA constantExpression)* ; /** @@ -3602,6 +3603,107 @@ constant | NUM_DOUBLE ; +///////////////////////////////////////////////// +// constant expressions +///////////////////////////////////////////////// + +constantExpression + : + constantConditionalExpression + ; + +constantConditionalExpression + : + constantLogicalOrExpression + ( + QUESTION + constantConditionalExpression COLON! constantConditionalExpression + )? + ; + +constantLogicalOrExpression + : constantLogicalAndExpression (LOR constantLogicalAndExpression)* + ; + +constantLogicalAndExpression + : constantInclusiveOrExpression (LAND constantInclusiveOrExpression)* + ; + +constantInclusiveOrExpression + : constantExclusiveOrExpression (BOR constantExclusiveOrExpression)* + ; + +constantExclusiveOrExpression + : constantAndExpression (BXOR constantAndExpression)* + ; + +constantAndExpression + : constantEqualityExpression (BAND constantEqualityExpression)* + ; + +constantEqualityExpression + : constantRelationalExpression ((NOT_EQUAL | EQUAL) constantRelationalExpression)* + ; + +constantRelationalExpression + : constantShiftExpression ( ( LT | GT | LE | GE ) constantShiftExpression )* + ; + +constantShiftExpression + : constantAdditiveExpression ( (SL | SR | BSR) constantAdditiveExpression )* + ; + +constantAdditiveExpression + : constantMultiplicativeExpression ( (PLUS | MINUS) constantMultiplicativeExpression )* + ; + +constantMultiplicativeExpression + : constantUnaryExpression ( (STAR | DIV | MOD ) constantUnaryExpression )* + ; + +constantUnaryExpression + : MINUS constantUnaryExpression + | PLUS constantUnaryExpression + | constantUnaryExpressionNotPlusMinus + ; + +constantUnaryExpressionNotPlusMinus +{ + String type = null; +} + : + BNOT constantUnaryExpression + | + LNOT constantUnaryExpression + | + ( + // subrule allows option to shut off warnings + //options { + // // "(int" ambig with postfixExpr due to lack of sequence + // // info in linear approximate LL(k). It's ok. Shut up. + // generateAmbigWarnings=false; + //}: + // only casts to primitive types and type String are allowed in constant expressions + (LPAREN (builtInTypeSpec | {isNextKeyword("String")}? IDENT) RPAREN constantUnaryExpression)=> + LPAREN (type=builtInTypeSpec | {isNextKeyword("String")}? IDENT) RPAREN + constantUnaryExpression + | + constantPostfixExpression + ) + ; + +constantPostfixExpression + : + // start with a primary part like constant or identifier + ( + IDENT | constant | TRUE | FALSE | THIS | NULL | SUPER | DEFAULT + ) + // qualified id (id.id.id.id) -- build the name + ( + DOT ( IDENT | THIS | SUPER | CLASS ) + )* + ; + ///////////////////////////////////////////////// // annotations (JSR-175: Metadata facility) From 695a671045160dc689adc127e2410ab84a8c5cf9 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Mon, 22 Jan 2024 00:05:44 +0100 Subject: [PATCH 23/29] OC-126: cyclomatic complexity everywhere!! because it's allowed to use ternary expressions and since JDK14 also switch expressions in practically every place, a single expression might not have a cyclomatic complexity equal to 0, as somewhere in the expression tree there might be ?: or switch; this non-zero value must be propagated through all possible levels of expressions, even in crazy places like inside array size initializer (see lambdaSwitchExpressionInsideArrayInitializer example), variable declaration, for loop init/condition/iterator, while and do-while conditions, even a throw statement or an expression lambda can have a switch inside --- .../instr/java/ContextSetAndComplexity.java | 34 +- .../com/atlassian/clover/instr/java/java.g | 639 +++++++++++++----- ...va14SwitchExpressionInVariousContexts.java | 7 + 3 files changed, 500 insertions(+), 180 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java b/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java index 575f07d2..1bf0145c 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/ContextSetAndComplexity.java @@ -6,6 +6,36 @@ * A helper class used in java.g */ class ContextSetAndComplexity { - ContextSet context; - int complexity; + private ContextSet context; + private int complexity; + + public static ContextSetAndComplexity empty() { + return new ContextSetAndComplexity(null, 0); + } + + public static ContextSetAndComplexity ofComplexity(int complexity) { + return new ContextSetAndComplexity(null, complexity); + } + + public void setContext(ContextSet context) { + this.context = context; + } + + public void addComplexity(int increment) { + complexity += increment; + } + + public ContextSet getContext() { + return context; + } + + public int getComplexity() { + return complexity; + } + + private ContextSetAndComplexity(ContextSet context, int complexity) { + this.context = context; + this.complexity = complexity; + } + } diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index a3222730..323a68dc 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -862,13 +862,13 @@ typeDefinition2[Modifiers mods, CloverToken first, boolean nested] * A declaration is the creation of a reference or primitive-type variable * Create a separate Type/Var tree for each var in the var list. */ -declaration! +declaration! returns [int complexity] { Modifiers mods = null; String type = null; } : - mods=fieldModifiers[false] type=typeSpec variableDefinitions + mods=fieldModifiers[false] type=typeSpec complexity=variableDefinitions ; // A type specification is a type name with possible brackets afterwards @@ -1716,6 +1716,7 @@ annotationTypeBody [ClassEntryNode classEntry] returns [CloverToken t] t = null; Modifiers mods = null; String type = null; + int ignoredComplexity; } : ip:LCURLY! @@ -1738,7 +1739,8 @@ annotationTypeBody [ClassEntryNode classEntry] returns [CloverToken t] mods=fieldModifiers[false] type=typeSpec - variableDefinitions SEMI + ignoredComplexity=variableDefinitions // it's a constant so complexity is 0 + SEMI | // a nested type declaration @@ -1833,6 +1835,7 @@ field! [ClassEntryNode containingClass] Parameter [] parameters = null; Map> tags = null; String typename = null; + int ignoredComplexity; } : // read javadocs written before the member (if any) @@ -1865,7 +1868,7 @@ field! [ClassEntryNode containingClass] mods=fieldModifiers[false] { deprecated = maybeEnterDeprecated(tags, mods); } returnType=typeSpec - variableDefinitions + ignoredComplexity=variableDefinitions // we don't instrument fields, so ignore the returned value SEMI { maybeExitDeprecated(deprecated); } @@ -1996,21 +1999,32 @@ explicitConstructorInvocation returns [CloverToken t] ) ; -variableDefinitions +variableDefinitions returns [int complexity] +{ + int tmpComplexity = 0; + complexity = 0; +} : - variableDeclarator (COMMA! variableDeclarator)* + complexity=variableDeclarator + ( + COMMA! + tmpComplexity=variableDeclarator + { + complexity += tmpComplexity; + } + )* ; -/** Declaration of a variable. This can be a class/instance variable, - * or a local variable in a method +/** + * Declaration of a variable. This can be a class/instance variable, or a local variable in a method. * It can also include possible initialization. */ -variableDeclarator! +variableDeclarator! returns [int complexity] { String brackets = null; } : - IDENT brackets=declaratorBrackets varInitializer + IDENT brackets=declaratorBrackets complexity=varInitializer ; declaratorBrackets returns [String brackets] @@ -2025,24 +2039,37 @@ declaratorBrackets returns [String brackets] )* ; -varInitializer +varInitializer returns [int complexity] +{ + complexity = 0; +} : - ( ASSIGN initializer )? + ( ASSIGN complexity=initializer )? ; // This is an initializer used to set up an array. -arrayInitializer +arrayInitializer returns [int complexity] +{ + int initComplexity; + complexity = 0; +} : LCURLY ( - initializer + initComplexity=initializer + { + complexity += initComplexity; + } ( // CONFLICT: does a COMMA after an initializer start a new // initializer or start the option ',' at end? // ANTLR generates proper code by matching // the comma as soon as possible. options { warnWhenFollowAmbig = false; }: - COMMA! initializer + COMMA! initComplexity=initializer + { + complexity += initComplexity; + } )* )? @@ -2053,11 +2080,14 @@ arrayInitializer // The two "things" that can initialize an array element are an expression // and another (nested) array initializer. -initializer +initializer returns [int complexity] +{ + complexity = 0; +} : - expression + complexity=expression | - arrayInitializer + complexity=arrayInitializer ; // This is a list of exception classes that the method is declared to throw @@ -2248,8 +2278,8 @@ statement [CloverToken owningLabel] returns [CloverToken last] boolean instrumentable = !labelled; CloverToken instr = null; // instr point for statement CloverToken flushAfter = null; // if not null, add maybeFlush instr - int complexity= 0; - int tmpCmp = 0; + int complexity = 0; // complexity of the entire statement + int declComplexity, exprComplexity = 0, forInitComp = 0, forCondComp = 0, forIterComp = 0; boolean wasDefault = false; Modifiers mods = null; Parameter parameter = null; @@ -2272,8 +2302,15 @@ statement [CloverToken owningLabel] returns [CloverToken last] { tmp=lt(1); } - expression - ( colon:COLON! {instrBoolExpr(tmp, ct(colon)); assertColonPart=true;} expression )? + exprComplexity=expression + ( + colon:COLON! + { + instrBoolExpr(tmp, ct(colon)); + assertColonPart=true; + } + exprComplexity=expression + )? semi:SEMI! { if (!assertColonPart) { @@ -2296,8 +2333,9 @@ statement [CloverToken owningLabel] returns [CloverToken last] // predicate to test symbol table to see what the type was coming // up, but that's pretty hard without a symbol table ;) (declaration) => - declaration se1:SEMI! + declComplexity=declaration se1:SEMI! { + complexity += declComplexity; flushAfter = ct(se1); } @@ -2305,7 +2343,10 @@ statement [CloverToken owningLabel] returns [CloverToken last] // note: yield can appear inside traditional case blocks and also in compound statement blocks inside // lambda case; that's why rule is placed here; a drawback is that instrumenter accepts yield in any place ( { isNextKeyword("yield") }? IDENT expression SEMI ) => - { isNextKeyword("yield") }? IDENT expression SEMI! + { isNextKeyword("yield") }? IDENT exprComplexity=expression SEMI! + { + complexity += exprComplexity; + } | // NOTE: we check for records before normal statement as "record" can be recognized as IDENT leading to syntax error @@ -2331,9 +2372,10 @@ statement [CloverToken owningLabel] returns [CloverToken last] // assignment statement, or any other expression evaluated for // side-effects. ( expression SEMI ) => - expression se2:SEMI! + exprComplexity=expression se2:SEMI! { flushAfter = ct(se2); + complexity += exprComplexity; } | @@ -2358,7 +2400,10 @@ statement [CloverToken owningLabel] returns [CloverToken last] { tmp=lt(1); } - expression + exprComplexity=expression + { + complexity += exprComplexity; + } rp1:RPAREN! { instrBoolExpr(tmp, ct(rp1)); @@ -2401,14 +2446,20 @@ statement [CloverToken owningLabel] returns [CloverToken last] (parameterDeclaration COLON) => ( // enhanced for - parameter=parameterDeclaration COLON expression + parameter=parameterDeclaration COLON exprComplexity=expression + { + complexity += exprComplexity; + } ) | ( // traditional for - forInit SEMI! // initializer - forCond // condition test - forIter // updater + forInitComp=forInit SEMI! // initializer + forCondComp=forCond // condition test + forIterComp=forIter // updater + { + complexity += forInitComp + forCondComp + forIterComp; + } ) ) rp:RPAREN! @@ -2432,7 +2483,10 @@ statement [CloverToken owningLabel] returns [CloverToken last] { tmp = lt(1); } - expression + exprComplexity=expression + { + complexity += exprComplexity; + } rp2:RPAREN! { instrBoolExpr(tmp, ct(rp2)); @@ -2461,7 +2515,11 @@ statement [CloverToken owningLabel] returns [CloverToken last] { tmp=lt(1); } - expression rp3:RPAREN! + exprComplexity=expression + { + complexity += exprComplexity; + } + rp3:RPAREN! { instrBoolExpr(tmp, ct(rp3)); } @@ -2480,23 +2538,29 @@ statement [CloverToken owningLabel] returns [CloverToken last] | // Return an expression - RETURN (expression)? SEMI! - + RETURN + ( + exprComplexity=expression + { + complexity += exprComplexity; + } + )? + SEMI! | // a classic switch/case with colons ( SWITCH LPAREN expression RPAREN LCURLY (CASE expression | DEFAULT) COLON) => contextAndComplexity = colonSwitchExpression[owningLabel, false] { - saveContext = contextAndComplexity.context; - complexity += contextAndComplexity.complexity; + saveContext = contextAndComplexity.getContext(); + complexity += contextAndComplexity.getComplexity(); } | // a new switch/case with lambdas ( SWITCH LPAREN expression RPAREN LCURLY (CASE patternMatch | DEFAULT) LAMBDA) => contextAndComplexity = lambdaSwitchExpression[owningLabel] { - saveContext = contextAndComplexity.context; - complexity += contextAndComplexity.complexity; + saveContext = contextAndComplexity.getContext(); + complexity += contextAndComplexity.getComplexity(); } | // exception try-catch block @@ -2508,12 +2572,15 @@ statement [CloverToken owningLabel] returns [CloverToken last] | // throw an exception - THROW expression SEMI! - + THROW exprComplexity=expression SEMI! + { + complexity += exprComplexity; + } | // synchronize a statement - SYNCHRONIZED LPAREN! expression RPAREN! + SYNCHRONIZED LPAREN! exprComplexity=expression RPAREN! { + complexity += exprComplexity; enterContext(ContextStore.CONTEXT_SYNC); saveContext = getCurrentContext(); } @@ -2522,9 +2589,8 @@ statement [CloverToken owningLabel] returns [CloverToken last] exitContext(); } - | - // empty statement + // an empty statement SEMI ) { @@ -2546,6 +2612,7 @@ statement [CloverToken owningLabel] returns [CloverToken last] } ; + /** * A group of one or more "case x:" labels, followed by a list of statements. */ @@ -2620,28 +2687,32 @@ caseStatementsList ; // The initializer for a for loop -forInit +forInit returns [int complexity] +{ + complexity = 0; +} : ( // if it looks like a declaration, it is (declaration) => - declaration + complexity=declaration | // otherwise it could be an expression list... - expressionList + complexity=expressionList )? ; -forCond +forCond returns [int complexity] { CloverToken tmp = null; + complexity = 0; } : ( { tmp=lt(1); } - expression se:SEMI! + complexity=expression se:SEMI! { instrBoolExpr(tmp, ct(se)); } @@ -2650,15 +2721,19 @@ forCond SEMI! ; -forIter +forIter returns [int complexity] +{ + complexity = 0; +} : - (expressionList)? + (complexity=expressionList)? ; // an exception handler try/catch block tryCatchBlock [boolean labelled] returns [CloverToken last] { last = null; + int declComplexity; int complexity = 0; ContextSet saveContext = getCurrentContext(); } @@ -2671,18 +2746,25 @@ tryCatchBlock [boolean labelled] returns [CloverToken last] } ( (IDENT) => - variableDeclarator + declComplexity=variableDeclarator | - declaration + declComplexity=declaration ) { + complexity += declComplexity; complexity++; instrArmDecl((ct(lp)).getNext(), lt(0), saveContext); } ( semi:SEMI - ( (IDENT) => variableDeclarator | declaration ) + ( + (IDENT) => + declComplexity=variableDeclarator + | + declComplexity=declaration + ) { + complexity += declComplexity; complexity++; instrArmDecl((ct(semi)).getNext(), lt(0), saveContext); } @@ -2837,6 +2919,7 @@ lambdaFunction returns [CloverToken last] */ CloverToken classCastStart = null; CloverToken classCastEnd = null; + int tmpComplexity = 0; } : // optional class cast, e.g: "Object o = (Runnable) () -> { ... };" therefore we can have @@ -2877,8 +2960,10 @@ lambdaFunction returns [CloverToken last] classCastStart, classCastEnd); } } - expression - { if (exprToBlockHeuristic) { + // TODO use tmpComplexity in LambdaExpressionEntryEmitter + tmpComplexity=expression + { + if (exprToBlockHeuristic) { instrExitLambdaExprToBlockExpression(exprToBlockStartEntryEmitter, lt(0)); } else { instrExitLambdaExpression(expressionEntryEmitter, lt(0)); @@ -2968,37 +3053,55 @@ lambdaArgs returns [Parameter[] parameters] // // the last two are not usually on a precedence chart; I put them in // to point out that new has a higher precedence than '.', so you -// can validy use +// can validly use // new Frame().show() // // Note that the above precedence levels map to the rules below... // Once you have a precedence chart, writing the appropriate rules as below -// is usually very straightfoward +// is usually very straightforward // the mother of all expressions -expression +// returns cyclomatic complexity (typically 0, unless ternary operator or switch expression is used) +expression returns [int complexity] : - { - pushHeadIdentifierStack(); - } - assignmentExpression - { - popHeadIdentifierStack(); - } + { + pushHeadIdentifierStack(); + } + complexity=assignmentExpression + { + popHeadIdentifierStack(); + } ; // This is a list of expressions. -expressionList - : expression (COMMA! expression)* +expressionList returns [int complexity] +{ + int tmpComplexity = 0; + complexity = 0; +} + : + complexity=expression + ( + COMMA! + tmpComplexity=expression + { + complexity += tmpComplexity; + } + )* ; + // assignment expression (level 13) -assignmentExpression +assignmentExpression returns [int complexity] +{ + int complexity1 = 0, complexity2 = 0; + complexity = 0; +} : - conditionalExpression + complexity1=conditionalExpression ( options { greedy=true; }: ( ASSIGN^ | PLUS_ASSIGN^ @@ -3013,78 +3116,166 @@ assignmentExpression | BXOR_ASSIGN^ | BOR_ASSIGN^ ) - assignmentExpression + complexity2=assignmentExpression )? + { + complexity = complexity1 + complexity2; + } ; // conditional test (level 12) -conditionalExpression +conditionalExpression returns [int complexity] { CloverToken startOfCond = null; CloverToken lf = null; + int complexity1 = 0, complexity2 = 0, complexity3 = 0; + int ternaryComplexity = 0; + complexity = 0; } : - (lambdaFunctionPredicate) => lf=lambdaFunction + (lambdaFunctionPredicate) => + lf=lambdaFunction | (logicalOrExpression (QUESTION)?) => {startOfCond = lt(1);} - logicalOrExpression - ( endOfCond:QUESTION - {if (!constExpr) instrBoolExpr(startOfCond, ct(endOfCond));} - assignmentExpression COLON! conditionalExpression + complexity1=logicalOrExpression + ( + endOfCond:QUESTION + { + if (!constExpr) + instrBoolExpr(startOfCond, ct(endOfCond)); + } + complexity2=assignmentExpression COLON! complexity3=conditionalExpression + { + // as ternary exists, it's complexity is 1 ... + ternaryComplexity = 1; + } )? + { + // ... plus whatever we have in branches + complexity = complexity1 + complexity2 + complexity3 + ternaryComplexity; + } ; // logical or (||) (level 11) -logicalOrExpression - : logicalAndExpression (LOR logicalAndExpression)* +logicalOrExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=logicalAndExpression + ( + LOR + tmpComplexity=logicalAndExpression + { + complexity += tmpComplexity; + } + )* ; // logical and (&&) (level 10) -logicalAndExpression - : inclusiveOrExpression (LAND inclusiveOrExpression)* +logicalAndExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=inclusiveOrExpression + ( + LAND + tmpComplexity=inclusiveOrExpression + { + complexity += tmpComplexity; + } + )* ; // bitwise or non-short-circuiting or (|) (level 9) -inclusiveOrExpression - : exclusiveOrExpression (BOR exclusiveOrExpression)* +inclusiveOrExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=exclusiveOrExpression + ( + BOR + tmpComplexity=exclusiveOrExpression + { + complexity += tmpComplexity; + } + )* ; // exclusive or (^) (level 8) -exclusiveOrExpression - : andExpression (BXOR andExpression)* +exclusiveOrExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=andExpression + ( + BXOR + tmpComplexity=andExpression + { + complexity += tmpComplexity; + } + )* ; // bitwise or non-short-circuiting and (&) (level 7) -andExpression - : equalityExpression (BAND equalityExpression)* +andExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=equalityExpression + ( + BAND + tmpComplexity=equalityExpression + { + complexity += tmpComplexity; + } + )* ; // equality/inequality (==/!=) (level 6) -equalityExpression - : relationalExpression ((NOT_EQUAL | EQUAL) relationalExpression)* +equalityExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=relationalExpression + ( + (NOT_EQUAL | EQUAL) + tmpComplexity=relationalExpression + { + complexity += tmpComplexity; + } + )* ; // boolean relational expressions (level 5) -relationalExpression +relationalExpression returns [int complexity] { - String type = null; + String type = null; + int tmpComplexity = 0; } - : shiftExpression - ( ( ( LT - | GT - | LE - | GE - ) - shiftExpression + : + complexity=shiftExpression + ( + ( + ( LT | GT | LE | GE ) + tmpComplexity=shiftExpression + { + complexity += tmpComplexity; + } )* | (INSTANCEOF type=typeSpec IDENT) => @@ -3096,28 +3287,66 @@ relationalExpression // bit shift expressions (level 4) -shiftExpression - : additiveExpression ((SL | SR | BSR) additiveExpression)* +shiftExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=additiveExpression + ( + (SL | SR | BSR) + tmpComplexity=additiveExpression + { + complexity += tmpComplexity; + } + )* ; // binary addition/subtraction (level 3) -additiveExpression - : multiplicativeExpression ((PLUS | MINUS) multiplicativeExpression)* +additiveExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=multiplicativeExpression + ( + (PLUS | MINUS) + tmpComplexity=multiplicativeExpression + { + complexity += tmpComplexity; + } + )* ; // multiplication/division/modulo (level 2) -multiplicativeExpression - : unaryExpression ((STAR | DIV | MOD ) unaryExpression)* +multiplicativeExpression returns [int complexity] +{ + int tmpComplexity = 0; +} + : + complexity=unaryExpression + ( + (STAR | DIV | MOD ) + tmpComplexity=unaryExpression + { + complexity += tmpComplexity; + } + )* ; -unaryExpression - : INC unaryExpression - | DEC unaryExpression - | MINUS unaryExpression - | PLUS unaryExpression - | unaryExpressionNotPlusMinus[null, null] +unaryExpression returns [int complexity] + : + INC complexity=unaryExpression + | + DEC complexity=unaryExpression + | + MINUS complexity=unaryExpression + | + PLUS complexity=unaryExpression + | + complexity=unaryExpressionNotPlusMinus[null, null] ; /** @@ -3150,14 +3379,17 @@ unaryExpression * postfixExpression[start, end] * */ -unaryExpressionNotPlusMinus[CloverToken classCastStart, CloverToken classCastEnd] +unaryExpressionNotPlusMinus[CloverToken classCastStart, CloverToken classCastEnd] returns [int complexity] { String type = null; + complexity = 0; } - : BNOT unaryExpression - | LNOT unaryExpression - - | ( + : + BNOT complexity=unaryExpression + | + LNOT complexity=unaryExpression + | + ( // subrule allows option to shut off warnings options { // "(int" ambig with postfixExpr due to lack of sequence @@ -3169,7 +3401,7 @@ unaryExpressionNotPlusMinus[CloverToken classCastStart, CloverToken classCastEnd // Have to backtrack to see if operator follows (LPAREN type=builtInTypeSpec RPAREN unaryExpression)=> LPAREN type=builtInTypeSpec RPAREN - unaryExpression + complexity=unaryExpression | // Have to backtrack to see if operator follows. If no operator // follows, it's a typecast. No semantic checking needed to parse. @@ -3178,9 +3410,9 @@ unaryExpressionNotPlusMinus[CloverToken classCastStart, CloverToken classCastEnd { classCastStart = lt(1); } LPAREN type=classTypeSpec (BAND type=classOrInterfaceType)* RPAREN { classCastEnd = lt(0); } - unaryExpressionNotPlusMinus[classCastStart, classCastEnd] + complexity=unaryExpressionNotPlusMinus[classCastStart, classCastEnd] | - postfixExpression[classCastStart, classCastEnd] + complexity=postfixExpression[classCastStart, classCastEnd] ) ; @@ -3190,7 +3422,7 @@ unaryExpressionNotPlusMinus[CloverToken classCastStart, CloverToken classCastEnd * @param classCastStart - used for instrumentation of a method reference with type cast * @param classCastEnd - used for instrumentation of a method reference with type cast */ -postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] +postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] returns [int complexity] { /* * A marker token to remember where the method reference starts (like "Math::abs" or "String::new" or "int[]::new" @@ -3200,14 +3432,24 @@ postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] */ CloverToken startMethodReference = null; ContextSetAndComplexity cc = null; + int tmpComplexity = 0; + complexity = 0; } : { // we might start a method reference, remember this token startMethodReference = lt(1); } - cc=primaryExpressionPart // start with a primary part like constant or identifier - supplementaryExpressionPart[classCastStart, classCastEnd, startMethodReference] // add extra stuff like array indexes, this/super (optional) + // start with a primary part like constant or identifier + cc=primaryExpressionPart + { + complexity += cc.getComplexity(); + } + // add extra stuff like array indexes, this/super (optional) + tmpComplexity=supplementaryExpressionPart[classCastStart, classCastEnd, startMethodReference] + { + complexity += tmpComplexity; + } ( (methodReferencePredicate) => methodReferencePart // add a method reference or @@ -3231,7 +3473,8 @@ postfixExpression[CloverToken classCastStart, CloverToken classCastEnd] primaryExpressionPart returns [ContextSetAndComplexity ret] { String type = null; - ret = null; + int tmpComplexity = 0; + ret = ContextSetAndComplexity.empty(); } : IDENT @@ -3252,9 +3495,15 @@ primaryExpressionPart returns [ContextSetAndComplexity ret] | NULL | - newExpression + tmpComplexity=newExpression + { + ret = ContextSetAndComplexity.ofComplexity(tmpComplexity); + } | - LPAREN! assignmentExpression RPAREN! + LPAREN! tmpComplexity=assignmentExpression RPAREN! + { + ret = ContextSetAndComplexity.ofComplexity(tmpComplexity); + } | SUPER { @@ -3292,8 +3541,8 @@ colonSwitchExpression [CloverToken owningLabel, boolean isInsideExpression] retu CloverToken tmp = null; boolean labelled = (owningLabel != null); FlagDeclEmitter flag = null; - ret = new ContextSetAndComplexity(); - int casesGroupComplexity; + ret = ContextSetAndComplexity.empty(); + int casesGroupComplexity, exprComplexity; } : sw:SWITCH @@ -3306,13 +3555,16 @@ colonSwitchExpression [CloverToken owningLabel, boolean isInsideExpression] retu flag = declareFlagBefore(tmp); } enterContext(ContextStore.CONTEXT_SWITCH); - ret.context = getCurrentContext(); + ret.setContext(getCurrentContext()); + } + LPAREN! exprComplexity=expression RPAREN! LCURLY! + { + ret.addComplexity(exprComplexity); } - LPAREN! expression RPAREN! LCURLY! ( casesGroupComplexity = colonCasesGroup[flag] { - ret.complexity += casesGroupComplexity; + ret.addComplexity(casesGroupComplexity); } )* { @@ -3326,23 +3578,26 @@ colonSwitchExpression [CloverToken owningLabel, boolean isInsideExpression] retu */ lambdaSwitchExpression [CloverToken owningLabel] returns [ContextSetAndComplexity ret] { - int caseComplexity = 0; + int caseComplexity = 0, exprComplexity = 0; ContextSet saveContext = getCurrentContext(); CloverToken tmp = null; - ret = new ContextSetAndComplexity(); + ret = ContextSetAndComplexity.empty(); } : sw:SWITCH { tmp = ct(sw); enterContext(ContextStore.CONTEXT_SWITCH); - ret.context = getCurrentContext(); + ret.setContext(getCurrentContext()); + } + LPAREN! exprComplexity=expression RPAREN! LCURLY! + { + ret.addComplexity(exprComplexity); } - LPAREN! expression RPAREN! LCURLY! ( caseComplexity = lambdaCase[saveContext] { - ret.complexity += caseComplexity; + ret.addComplexity(caseComplexity); } )+ { @@ -3360,17 +3615,14 @@ lambdaCase[ContextSet context] returns [int complexity] Token pos = null; CaseExpressionEntryEmitter expressionEntryEmitter = null; CaseThrowExpressionEntryEmitter throwEntryEmitter = null; + int exprComplexity = 0; complexity = 1; } : ( si1:CASE - { - constExpr = true; - } patternMatch { - constExpr = false; pos = si1; } | @@ -3383,18 +3635,16 @@ lambdaCase[ContextSet context] returns [int complexity] ( // throwing an exception must be instrumented differently (THROW expression SEMI) => - THROW expression SEMI + THROW exprComplexity=expression SEMI { - /* TODO calculate and pass expression's complexity */ - throwEntryEmitter = instrEnterCaseThrowExpression(ct(t), lt(0), context, 0); + throwEntryEmitter = instrEnterCaseThrowExpression(ct(t), lt(0), context, exprComplexity); instrExitCaseThrowExpression(throwEntryEmitter, lt(0)); } | // void and value-returning expressions - expression SEMI + exprComplexity=expression SEMI { - /* TODO calculate and pass expression's complexity */ - expressionEntryEmitter = instrEnterCaseExpression(ct(t), lt(0), context, 0); + expressionEntryEmitter = instrEnterCaseExpression(ct(t), lt(0), context, exprComplexity); instrExitCaseExpression(expressionEntryEmitter, lt(0)); } | @@ -3418,21 +3668,33 @@ patternMatch * @param classCastStart - used for instrumentation of a method reference with type cast * @param classCastEnd - used for instrumentation of a method reference with type cast */ -supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd, CloverToken startMethodReference] +supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd, CloverToken startMethodReference] returns [int complexity] +{ + int tmpComplexity = 0; + complexity = 0; +} : ( // qualified id (id.id.id.id...) -- build the name - DOT ( - (typeArguments)? // a prefix to a generic class to supply type arguments - IDENT - { - pushIdentifierToHeadStack(LT(0).getText()); - } - | THIS - | CLASS - | newExpression - | SUPER // ClassName.super.field - ) + DOT + ( + (typeArguments)? // a prefix to a generic class to supply type arguments + IDENT + { + pushIdentifierToHeadStack(LT(0).getText()); + } + | + THIS + | + CLASS + | + tmpComplexity=newExpression + { + complexity += tmpComplexity; + } + | + SUPER // ClassName.super.field + ) // the above line needs a semantic check to make sure "class" // is the _last_ qualifier. | @@ -3456,7 +3718,10 @@ supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd ) | // an array indexing operation - LBRACK expression RBRACK! + LBRACK tmpComplexity=expression RBRACK! + { + complexity += tmpComplexity; + } | // method invocation // The next line is not strictly proper; it allows x(3)(4) or @@ -3547,12 +3812,15 @@ supplementaryPostIncDecPart * 2 * */ -newExpression +newExpression returns [int complexity] { CloverToken endOfBlock = null; String typeParam = null; + int declComplexity = 0, initComplexity = 0; + complexity = 0; } - : NEW (typeParam=typeParameters)? type + : + NEW (typeParam=typeParameters)? type ( LPAREN! argList RPAREN! (endOfBlock=classBlock[null])? //java 1.1 @@ -3563,19 +3831,33 @@ newExpression // a) [ expr ] and [ ] are not mixed // b) [ expr ] and an init are not used together - | newArrayDeclarator (arrayInitializer)? + | + declComplexity=newArrayDeclarator + ( + initComplexity=arrayInitializer + )? + { + complexity = declComplexity + initComplexity; + } ) ; -argList - : ( expressionList - | /*nothing*/ +argList returns [int complexity] +{ + complexity = 0; +} + : + ( + complexity=expressionList + | + /*nothing*/ ) ; -newArrayDeclarator +newArrayDeclarator returns [int complexity] { AnnotationImpl ann = null; + complexity = 0; } : ( // CONFLICT: @@ -3589,7 +3871,7 @@ newArrayDeclarator : (ann=annotation)* LBRACK - (expression)? + (complexity=expression)? RBRACK! )+ ; @@ -3677,12 +3959,6 @@ constantUnaryExpressionNotPlusMinus LNOT constantUnaryExpression | ( - // subrule allows option to shut off warnings - //options { - // // "(int" ambig with postfixExpr due to lack of sequence - // // info in linear approximate LL(k). It's ok. Shut up. - // generateAmbigWarnings=false; - //}: // only casts to primitive types and type String are allowed in constant expressions (LPAREN (builtInTypeSpec | {isNextKeyword("String")}? IDENT) RPAREN constantUnaryExpression)=> LPAREN (type=builtInTypeSpec | {isNextKeyword("String")}? IDENT) RPAREN @@ -3811,27 +4087,34 @@ annMemberValue2 [AnnotationValueCollection anno, String key, boolean isSuppressW | annMemberValueArrayInitializer [anno, key, isSuppressWarnings] ; + protected conditionalExpression2 returns [String asString] { CloverToken start = lt(1); CloverToken end = null; + int tmpComplexity; asString = null; } : - conditionalExpression { end = lt(0); asString = TokenListUtil.getNormalisedSequence(start, end); } + tmpComplexity=conditionalExpression + { + end = lt(0); + asString = TokenListUtil.getNormalisedSequence(start, end); + } ; + protected annMemberValueArrayInitializer [AnnotationValueCollection anno, String key, boolean isSuppressWarnings] { - boolean emitComma = false; - boolean seenFallthrough = false; - CloverToken last = null; - ArrayAnnotationValue annoArray = (anno == null) ? null : new ArrayAnnotationValue(); - if (anno != null) { - anno.put(key, annoArray); - } + boolean emitComma = false; + boolean seenFallthrough = false; + CloverToken last = null; + ArrayAnnotationValue annoArray = (anno == null) ? null : new ArrayAnnotationValue(); + if (anno != null) { + anno.put(key, annoArray); + } } : LCURLY diff --git a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java index d58dbd8a..d9c6bf5c 100644 --- a/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java +++ b/clover-core/src/test/resources/javasyntax14/Java14SwitchExpressionInVariousContexts.java @@ -43,6 +43,13 @@ static void lambdsSwitchExpressionAsMethodStatement(int kk) { } } + static void lambdaSwitchExpressionInsideArrayInitializer(int kk) { + int[] array = new int[switch (kk) { + case 0 -> 0; + default -> kk; + }]; + } + // lambda switch expression in an initializer block int kkk = 88; { From 5d7c41dd535a0a95b7c4ff762a1d77f5d6326570 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Mon, 22 Jan 2024 09:57:49 +0100 Subject: [PATCH 24/29] OC-126: split humongous InstrumentationTest into InstrumentationTestBase and over a dozen of test classes; add a test of complexityOfTernaryOperator in ExpressionComplexityCounterTest; remove counting ternaryComplexity=1 in conditionalExpression as it's already accounted for in the ExpressionInfo.fromTokens counter --- .../com/atlassian/clover/instr/java/java.g | 10 +- .../ExpressionComplexityCounterTest.groovy | 25 +- .../java/InstrumentationAssertsTest.groovy | 17 + .../InstrumentationCloverOnOffTest.groovy | 54 ++ .../java/InstrumentationDetectionTest.groovy | 42 + .../InstrumentationExpressionsTest.groovy | 51 ++ .../java/InstrumentationFlushingTest.groovy | 31 + .../java/InstrumentationForLoopTest.groovy | 23 + .../java/InstrumentationInitStringTest.groovy | 60 ++ .../java/InstrumentationLambdasTest.groovy | 26 + .../java/InstrumentationLiteralsTest.groovy | 40 + .../InstrumentationMethodEntriesTest.groovy | 31 + .../InstrumentationMethodLevelTest.groovy | 55 ++ .../InstrumentationMethodMetricsTest.groovy | 120 +++ .../java/InstrumentationStatementsTest.groovy | 22 + ...InstrumentationSwitchStatementsTest.groovy | 88 ++ .../instr/java/InstrumentationTest.groovy | 853 ------------------ .../instr/java/InstrumentationTestBase.groovy | 176 ++++ .../InstrumentationTestMethodsTest.groovy | 94 ++ .../InstrumentationTryStatementsTest.groovy | 37 + 20 files changed, 993 insertions(+), 862 deletions(-) create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationAssertsTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationCloverOnOffTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationDetectionTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationExpressionsTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationFlushingTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationForLoopTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationInitStringTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLambdasTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLiteralsTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodEntriesTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodLevelTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationStatementsTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationSwitchStatementsTest.groovy delete mode 100755 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestBase.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestMethodsTest.groovy create mode 100644 clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTryStatementsTest.groovy diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 323a68dc..63b29729 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -3130,7 +3130,6 @@ conditionalExpression returns [int complexity] CloverToken startOfCond = null; CloverToken lf = null; int complexity1 = 0, complexity2 = 0, complexity3 = 0; - int ternaryComplexity = 0; complexity = 0; } : @@ -3147,14 +3146,11 @@ conditionalExpression returns [int complexity] instrBoolExpr(startOfCond, ct(endOfCond)); } complexity2=assignmentExpression COLON! complexity3=conditionalExpression - { - // as ternary exists, it's complexity is 1 ... - ternaryComplexity = 1; - } )? { - // ... plus whatever we have in branches - complexity = complexity1 + complexity2 + complexity3 + ternaryComplexity; + // as ternary exists, it's complexity is 1, but this is already accounted for in ExpressionInfo.fromTokens + // sum whatever we have in branches + complexity = complexity1 + complexity2 + complexity3; } ; diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/ExpressionComplexityCounterTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/ExpressionComplexityCounterTest.groovy index 35eef41d..b25ffc14 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/ExpressionComplexityCounterTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/ExpressionComplexityCounterTest.groovy @@ -3,12 +3,16 @@ package com.atlassian.clover.instr.java import org.junit.Test import static com.atlassian.clover.instr.java.JavaTokenTypes.BOR +import static com.atlassian.clover.instr.java.JavaTokenTypes.COLON import static com.atlassian.clover.instr.java.JavaTokenTypes.EQUAL import static com.atlassian.clover.instr.java.JavaTokenTypes.IDENT import static com.atlassian.clover.instr.java.JavaTokenTypes.INT_LITERAL import static com.atlassian.clover.instr.java.JavaTokenTypes.LAND import static com.atlassian.clover.instr.java.JavaTokenTypes.LOR +import static com.atlassian.clover.instr.java.JavaTokenTypes.LT import static com.atlassian.clover.instr.java.JavaTokenTypes.LPAREN +import static com.atlassian.clover.instr.java.JavaTokenTypes.MINUS +import static com.atlassian.clover.instr.java.JavaTokenTypes.QUESTION import static com.atlassian.clover.instr.java.JavaTokenTypes.RPAREN import static org.junit.Assert.assertEquals @@ -18,7 +22,7 @@ class ExpressionComplexityCounterTest { void complexityOfOne() { ExpressionComplexityCounter counter = new ExpressionComplexityCounter(1) - // "a == 5 | 12" - bit OR + // "a == 5 | 12" - bit OR is not a branch counter.accept(new CloverToken(IDENT, "a")) counter.accept(new CloverToken(EQUAL, "==")) counter.accept(new CloverToken(INT_LITERAL, "5")) @@ -32,7 +36,7 @@ class ExpressionComplexityCounterTest { void complexityOfThree() { ExpressionComplexityCounter counter = new ExpressionComplexityCounter(1) - // "a || (b && c)" - bit OR + // "a || (b && c)" - logical AND / OR are branches counter.accept(new CloverToken(IDENT, "a")) counter.accept(new CloverToken(LOR, "||")) counter.accept(new CloverToken(LPAREN, "(")) @@ -44,4 +48,21 @@ class ExpressionComplexityCounterTest { assertEquals(3, counter.getComplexity()) } + @Test + void complexityOfTernaryOperator() { + ExpressionComplexityCounter counter = new ExpressionComplexityCounter(1) + + // "a < 0 ? -1 : 1" - ternary has two branches so one cycle + counter.accept(new CloverToken(IDENT, "a")) + counter.accept(new CloverToken(LT, "<")) + counter.accept(new CloverToken(INT_LITERAL, "0")) + counter.accept(new CloverToken(QUESTION, "?")) + counter.accept(new CloverToken(MINUS, "-")) + counter.accept(new CloverToken(INT_LITERAL, "1")) + counter.accept(new CloverToken(COLON, ":")) + counter.accept(new CloverToken(INT_LITERAL, "1")) + + assertEquals(1, counter.getComplexity()) + } + } \ No newline at end of file diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationAssertsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationAssertsTest.groovy new file mode 100644 index 00000000..be703815 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationAssertsTest.groovy @@ -0,0 +1,17 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationAssertsTest extends InstrumentationTestBase { + + @Test + void testAssertStatements() + throws Exception { + checkStatement( + " assert arg > 0;", + " assert (((arg > 0)&&(RECORDER.iget(1)!=0|true))||(RECORDER.iget(2)==0&false));") + checkStatement( + " assert arg > 0 : \"arg must be greater than zero\";", + " assert (((arg > 0 )&&(RECORDER.iget(1)!=0|true))||(RECORDER.iget(2)==0&false)): \"arg must be greater than zero\";") + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationCloverOnOffTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationCloverOnOffTest.groovy new file mode 100644 index 00000000..38b8aa2f --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationCloverOnOffTest.groovy @@ -0,0 +1,54 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationCloverOnOffTest extends InstrumentationTestBase { + + @Test + void testCloverOnOff() throws Exception { + checkInstrumentation([ + ["/*///CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}", + "/*///CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}"], + ["package A; /*///CLOVER:OFF*/ class B { public B() {int i = 0;}}", + "package A; /*///CLOVER:OFF*/ class B { public B() {int i = 0;}}"], + ["/*CLOVER:OFF*/ class B { public B() {int i = 0;}}", + "/*CLOVER:OFF*/ class B { public B() {int i = 0;}}"], + // test second directive prefix, added for eclipse auto-format support + ["/*// /CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}", + "/*// /CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}"], + ["package A; /*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}", + "package A; /*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}"], + ["/*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}", + "/*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}"], + + ["class B { ///CLOVER:OFF\nprivate B() {int i = 0;}}", + "class B { ///CLOVER:OFF\nprivate B() {int i = 0;}}"], + ["class B { ///CLOVER:OFF\nprivate B() {}}", + "class B { ///CLOVER:OFF\nprivate B() {}}"], + ["class B { ///CLOVER:OFF\nprivate B() {}\n///CLOVER:ON\n}", + "class B { ///CLOVER:OFF\nprivate B() {}\n///CLOVER:ON\n}"], + ["class B { private B() {///CLOVER:OFF\n}\n///CLOVER:ON\n}", + "class B {" + snifferField + " private B() {RECORDER.inc(0);///CLOVER:OFF\n}\n///CLOVER:ON\n}"], + ["class B { private B() {///CLOVER:OFF\nint i = 0;///CLOVER:ON\n}}", + "class B {" + snifferField + " private B() {RECORDER.inc(0);///CLOVER:OFF\nint i = 0;///CLOVER:ON\n}}"], + ["class B { private B() {///CLOVER:OFF\nhashCode();///CLOVER:ON\n}}", + "class B {" + snifferField + " private B() {RECORDER.inc(0);///CLOVER:OFF\nhashCode();///CLOVER:ON\n}}"], + + ] as String[][]) + + checkInstrumentation([ + ["/*///CLOVER:OFF*/ public class MyTest { @Test public void foo(){} }", + "/*///CLOVER:OFF*/ public class MyTest { @Test public void foo(){} }"], + ["public class MyTest {/*///CLOVER:OFF*/ @Test public void foo(){} }", + "public class MyTest {/*///CLOVER:OFF*/ @Test public void foo(){} }"], + ["public class MyTest { @Test public void foo()/*///CLOVER:OFF*/{} }", + "public class MyTest { @Test public void foo()/*///CLOVER:OFF*/{} }"], + ["/*///CLOVER:OFF*/ public class MyTest { public void testFoo(){} }", + "/*///CLOVER:OFF*/ public class MyTest { public void testFoo(){} }"], + ["public class MyTest {/*///CLOVER:OFF*/ public void testFoo(){} }", + "public class MyTest {/*///CLOVER:OFF*/ public void testFoo(){} }"], + ["public class MyTest { public void testFoo()/*///CLOVER:OFF*/{} }", + "public class MyTest { public void testFoo()/*///CLOVER:OFF*/{} }"] + ] as String[][]) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationDetectionTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationDetectionTest.groovy new file mode 100644 index 00000000..02553f84 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationDetectionTest.groovy @@ -0,0 +1,42 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.api.CloverException +import org.junit.Test + +import static org.junit.Assert.fail + +class InstrumentationDetectionTest extends InstrumentationTestBase { + + // tests that "empty" classes don't get a recorder member when they don't need one + @Test + void testDirtyDetection() throws Exception { + final String recMember = "" + checkInstrumentation(recMember, [ + ["class A {}", "class A {}"], + ["interface A {}", "interface A {}"], + ["@interface A {}", "@interface A {}"], + ["enum A {}", "enum A {}"], + ["enum A { apple,banana,pear }", "enum A { apple,banana,pear }"], + ["enum A { apple,banana,pear }", "enum A { apple,banana,pear }"], + // second top-level class. + ["class A {public A(){}} class B {}","class A {" + recMember + snifferField + "public A(){RECORDER.inc(0);}} class B {}"] + ] as String[][], true) + } + + @Test + void testDoubleInstrDetection() throws Exception { + checkInstrumentation([ + [ "class A {}", "class A {}" ], //input is smaller than the marker + [ "", "" ] // empty source file + ] as String[][]); + try { + checkInstrumentation([ + [ CloverTokenStreamFilter.MARKER +"*/" ] + ] as String[][]) + fail("instrumentation marker not detected") + } + catch (CloverException e) { + + } + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationExpressionsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationExpressionsTest.groovy new file mode 100644 index 00000000..f3b5e2d5 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationExpressionsTest.groovy @@ -0,0 +1,51 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationExpressionsTest extends InstrumentationTestBase { + + @Test + void testTernaryOperator() + throws Exception { + // just a simple assignment + checkStatement("int i = arg == 2 ? 3 : 4;", + "RECORDER.inc(1);int i = (((arg == 2 )&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false))? 3 : 4;") + + // two ternary's embeded + checkStatement("int i = arg == (b==2?1:2) ? 3 : 4;", + "RECORDER.inc(1);int i = (((arg == (" + + "(((b==2)&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false))?1:2" + + ") )&&(RECORDER.iget(4)!=0|true))||(RECORDER.iget(5)==0&false))? 3 : 4;") + } + + @Test + void testConstExpr() + throws Exception { + // constant for loop + checkStatement( + "for (;true;) {System.out.println(a[i]);}", + "RECORDER.inc(1);for (;true;) {{RECORDER.inc(2);System.out.println(a[i]);}}") + } + + + @Test + void testConditionalWithAssignment() throws Exception { + // Don't instrument a conditional containing an assignment since this breaks Definite Assignment rules in javac + checkStatement( + "String line; while ((line = in.readLine()) != null) {System.out.println(line);}", + "RECORDER.inc(1);String line; RECORDER.inc(2);while ((line = in.readLine()) != null) {{RECORDER.inc(5);System.out.println(line);}}") + + } + + @Test + void testConditionalWithCloverOff() throws Exception { + // Preserve a conditional and don't add extraneous parentheses when CLOVER:OFF is specified as this may trigger + // compiler bugs for generic calls. See http://bugs.sun.com/view_bug.do?bug_id=6608961 + + //Note Clover inserts extraneous curly braces too + checkStatement( + "\n///CLOVER:OFF\nif (a + b == 2) { System.out.println(\"Hello, world\"); }\n///CLOVER:ON\n", + "\n///CLOVER:OFF\nif (a + b == 2) {{ System.out.println(\"Hello, world\"); }\n///CLOVER:ON\n}") + + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationFlushingTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationFlushingTest.groovy new file mode 100644 index 00000000..cbc76838 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationFlushingTest.groovy @@ -0,0 +1,31 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.cfg.instr.InstrumentationConfig +import com.atlassian.clover.cfg.instr.java.JavaInstrumentationConfig +import org.junit.Test + +class InstrumentationFlushingTest extends InstrumentationTestBase { + + @Test + void testThreadedFlushing() throws Exception { + JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, false, false) + config.setFlushPolicy(InstrumentationConfig.THREADED_FLUSHING) + checkInstrumentation([ + ["class B { public B(int arg) {int i = 0;}}", + "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);RECORDER.inc(1);int i = 0;}finally{RECORDER.flushNeeded();}}}"], + ] as String[][], + config) + } + + @Test + void testIntervalFlushing() throws Exception { + JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, false, false) + config.setFlushPolicy(InstrumentationConfig.INTERVAL_FLUSHING) + checkInstrumentation([ + ["class B { public B(int arg) {}}", + "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);}finally{RECORDER.maybeFlush();}}}"], + ] as String[][], + config) + + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationForLoopTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationForLoopTest.groovy new file mode 100644 index 00000000..83e227bf --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationForLoopTest.groovy @@ -0,0 +1,23 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationForLoopTest extends InstrumentationTestBase { + + @Test + void testForLoop() + throws Exception { + // traditional for loop + checkStatement( + "for (int i = 0; i < a.length; i++) {System.out.println(a[i]);}", + "RECORDER.inc(1);for (int i = 0; (((i < a.length)&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false)); i++) {{RECORDER.inc(4);System.out.println(a[i]);}}") + // traditional for loop, no braces + checkStatement( + "for (int i = 0; i < a.length; i++) System.out.println(a[i]);", + "RECORDER.inc(1);for (int i = 0; (((i < a.length)&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false)); i++) {RECORDER.inc(4);System.out.println(a[i]);}") + // enhanced for loop + checkStatement( + "for (int i : a) {System.out.println(i);}", + "RECORDER.inc(1);for (int i : a) {{RECORDER.inc(2);System.out.println(i);}}") + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationInitStringTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationInitStringTest.groovy new file mode 100644 index 00000000..53a8c268 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationInitStringTest.groovy @@ -0,0 +1,60 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.cfg.instr.InstrumentationConfig +import org.junit.Test + +import static org.junit.Assert.assertTrue + +class InstrumentationInitStringTest extends InstrumentationTestBase { + @Test + void testDefaultInitString() throws Exception { + final File dbDir = new File(workingDir, InstrumentationConfig.DEFAULT_DB_DIR) + final File dbFile = new File(dbDir, InstrumentationConfig.DEFAULT_DB_FILE) + + + String defaultInitStr = dbFile.getAbsolutePath() + + String out = getInstrumentedVersion(null, false, "class B { B(int arg) {}}") + + String expectedCharArray = RecorderInstrEmitter.asUnicodeString(defaultInitStr) + + assertTrue(out.contains(expectedCharArray)) + } + + @Test + void testRelativeDefaultInitString() throws Exception { + String defaultRelInitStr = InstrumentationConfig.DEFAULT_DB_DIR + File.separator + InstrumentationConfig.DEFAULT_DB_FILE + + + String out = getInstrumentedVersion(null, true, "class B { B(int arg) {}}") + String expectedCharArray = RecorderInstrEmitter.asUnicodeString(defaultRelInitStr) + + assertTrue(out.contains(expectedCharArray)) + File file = new File(defaultRelInitStr) + assertTrue("Could not delete file referenced by default initstring: " + file.getAbsolutePath(), file.delete()) + } + + @Test + void testRelativeInitString() throws Exception { + File coverageDbFile = File.createTempFile(getClass().getName() +"." + name, ".tmp", workingDir) + coverageDbFile.delete() + + String out = getInstrumentedVersion(coverageDbFile.getPath(), true, "class B { B(int arg) {}}") + + String expectedCharArray = RecorderInstrEmitter.asUnicodeString(coverageDbFile.getPath()) + + assertTrue(out.contains(expectedCharArray)) + } + + @Test + void testInitString() throws Exception { + File coverageDbFile = File.createTempFile(getClass().getName() +"." + name, ".tmp", workingDir) + coverageDbFile.delete() + + String out = getInstrumentedVersion(coverageDbFile.getAbsolutePath(), false, "class B { B(int arg) {}}") + + String expectedCharArray = RecorderInstrEmitter.asUnicodeString(coverageDbFile.getAbsolutePath()) + + assertTrue(out.contains(expectedCharArray)) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLambdasTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLambdasTest.groovy new file mode 100644 index 00000000..b6803825 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLambdasTest.groovy @@ -0,0 +1,26 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.CloverNames +import org.junit.Test + +class InstrumentationLambdasTest extends InstrumentationTestBase { + + @Test + void testLambdaExprToBlockReplace() throws Exception { + checkInstrumentation([ + [ "public class MyTestA { @Test public void foo(){Stream.of(1, 2, 3, 4, 5, 6).map((i) -> \"String:\" + String.valueOf(i));} }", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);RECORDER.inc(1);Stream.of(1, 2, 3, 4, 5, 6).map((i) -> {RECORDER.inc(2);return \"String:\" + String.valueOf(i);});} }" ], + [ "public class MyTestA { @Test public void foo(){map((int i) -> /*CLOVER:VOID*/ System.out.println(i));} }", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);RECORDER.inc(1);map((int i) -> /*CLOVER:VOID*/ {RECORDER.inc(2);System.out.println(i);});} }" ], + [ "public class MyTestA { @Test public void foo(){ map((i) -> /** CLOVER:VOID */ System.out.println(\"String:\" + String.valueOf(i))); }}", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> /** CLOVER:VOID */ {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], + [ "public class MyTestA { @Test public void foo(){ map((i) -> /* \t\n\r CLOVER:VOID */ System.out.println(\"String:\" + String.valueOf(i))); }}", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> /* \t\n\r CLOVER:VOID */ {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], + [ "public class MyTestA { @Test public void foo(){ map((i) -> ///CLOVER:VOID\n System.out.println(\"String:\" + String.valueOf(i))); }}", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> ///CLOVER:VOID\n {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], + [ "public class MyTestA { @Test public void foo(){ map((i) -> // /CLOVER:VOID\n System.out.println(\"String:\" + String.valueOf(i))); }}", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> // /CLOVER:VOID\n {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], + ] as String[][]) + } + +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLiteralsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLiteralsTest.groovy new file mode 100644 index 00000000..1cb4c8e6 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationLiteralsTest.groovy @@ -0,0 +1,40 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +import static org.junit.Assert.assertNotNull + +class InstrumentationLiteralsTest extends InstrumentationTestBase { + + @Test + void testNumericLiterals() throws Exception { + //More an test of language recognition than an instrumentation test + //Tests int, long, float, doubles in hex, binary, decimal, octal, some with underscores, some without + checkInstrumentation([ + [ "class B { static double[] doubles = new double[] { 09, 0_9, 0_0, 0x1_2_3, 1234_5678, 1_2_3_4__5_6_7_8L, 0b0001_0010_0100_1000, 3.141_592_653_589_793d, 0x1.ffff_ffff_ffff_fP1_023, 0x1111_2222_3333_4444L, 0x0.0000000000001P-1f, 0x0.0000000000001P-1_1d }; }", + "class B { static double[] doubles = new double[] { 09, 0_9, 0_0, 0x1_2_3, 1234_5678, 1_2_3_4__5_6_7_8L, 0b0001_0010_0100_1000, 3.141_592_653_589_793d, 0x1.ffff_ffff_ffff_fP1_023, 0x1111_2222_3333_4444L, 0x0.0000000000001P-1f, 0x0.0000000000001P-1_1d }; }"] + ] as String[][]) + } + + @Test + void testBinaryLiterals() throws Exception { + //More an test of language recognition than an instrumentation test + //Test binary literals (int, long with underscores) + checkInstrumentation([ + [ "class B { static double[] doubles = new double[] { 0b0, 0b1, 0b10101010, 0b1010_1010, 0b1010_1010L, 0b10101010L }; }", + "class B { static double[] doubles = new double[] { 0b0, 0b1, 0b10101010, 0b1010_1010, 0b1010_1010L, 0b10101010L }; }"] + ] as String[][]) + } + + /** + * Test for CLOV-669 + */ + @Test + void testUnicodeCharacters() + throws Exception { + + String instr = getInstrumentedVersion("class B { private void a(int arg) {String s = \"\u023a\";}}", false) + assertNotNull(instr) + } + +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodEntriesTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodEntriesTest.groovy new file mode 100644 index 00000000..71587e69 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodEntriesTest.groovy @@ -0,0 +1,31 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationMethodEntriesTest extends InstrumentationTestBase { + @Test + void testMethodEntries() + throws Exception { + checkInstrumentation([ + ["class B { B(int arg) {}}", + "class B {$snifferField B(int arg) {RECORDER.inc(0);}}"], + ["class B { public B(int arg) {}}", + "class B {$snifferField public B(int arg) {RECORDER.inc(0);}}"], + ["class B { protected B(int arg) {}}", + "class B {$snifferField protected B(int arg) {RECORDER.inc(0);}}"], + ["class B { private B(int arg) {}}", + "class B {$snifferField private B(int arg) {RECORDER.inc(0);}}"], + ["class B { void a(int arg) { }}", + "class B {$snifferField void a(int arg) {RECORDER.inc(0); }}"], + ["class B { public void a(int arg) { }}", + "class B {$snifferField public void a(int arg) {RECORDER.inc(0); }}"], + ["class B { protected void a(int arg) { }}", + "class B {$snifferField protected void a(int arg) {RECORDER.inc(0); }}"], + ["class B { private void a(int arg) { }}", + "class B {$snifferField private void a(int arg) {RECORDER.inc(0); }}"], + ["class B { private void a(int arg) {}\nprivate void b() {}\nprivate void c() {}}", + "class B {$snifferField private void a(int arg) {RECORDER.inc(0);}\nprivate" + + " void b() {RECORDER.inc(1);}\nprivate void c() {RECORDER.inc(2);}}"] + ] as String[][]) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodLevelTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodLevelTest.groovy new file mode 100644 index 00000000..0da5e26f --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodLevelTest.groovy @@ -0,0 +1,55 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.cfg.instr.InstrumentationConfig +import com.atlassian.clover.cfg.instr.InstrumentationLevel +import com.atlassian.clover.cfg.instr.java.JavaInstrumentationConfig +import org.junit.Test + +class InstrumentationMethodLevelTest extends InstrumentationTestBase { + + @Test + void testMethodLevelInstr() throws Exception { + checkStatement("int i = 0;", "int i = 0;", InstrumentationLevel.METHOD.ordinal()) + checkStatement("int i = arg == 2 ? 3 : 4;", "int i = arg == 2 ? 3 : 4;", InstrumentationLevel.METHOD.ordinal()) + checkStatement("assert arg > 0;", "assert arg > 0;", InstrumentationLevel.METHOD.ordinal()) + + JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) + config.setFlushPolicy(InstrumentationConfig.INTERVAL_FLUSHING) + config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) + checkInstrumentation([ + ["class B { public B(int arg) {}}", + "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);}finally{RECORDER.maybeFlush();}}}"] + ] as String[][], + config) + + config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) + config.setFlushPolicy(InstrumentationConfig.INTERVAL_FLUSHING) + config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) + + checkInstrumentation([ + ["class B { public B(int arg) {int i = 0;}}", + "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);int i = 0;}finally{RECORDER.maybeFlush();}}}"] + ] as String[][], + config) + + config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) + config.setFlushPolicy(InstrumentationConfig.THREADED_FLUSHING) + config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) + + checkInstrumentation([ + ["class B { public B(int arg) {}}", + "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);}finally{RECORDER.flushNeeded();}}}"] + ] as String[][], + config) + + config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) + config.setFlushPolicy(InstrumentationConfig.THREADED_FLUSHING) + config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) + + checkInstrumentation([ + ["class B { public B(int arg) {int i = 0;}}", + "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);int i = 0;}finally{RECORDER.flushNeeded();}}}"] + ] as String[][], + config) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy new file mode 100644 index 00000000..e4c86689 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy @@ -0,0 +1,120 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.api.registry.StatementInfo +import com.atlassian.clover.registry.Clover2Registry +import com.atlassian.clover.registry.FixedSourceRegion +import com.atlassian.clover.registry.entities.FullMethodInfo +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class InstrumentationMethodMetricsTest extends InstrumentationTestBase { + + @Test + void testMethodMetrics() throws Exception { + checkMethodMetrics("void A() {}",0, 0, 1) + checkMethodMetrics("void A() {a();}",1, 0, 1) + checkMethodMetrics("void A() {a = (6 < 7);}",1 ,0 ,1) + checkMethodMetrics("void A() {a();b();c();}",3, 0, 1) + + // if + checkMethodMetrics("void A() {if (a()) b(); else c();}",3, 2, 2) + checkMethodMetrics("void A() {if (a() || b()) c();}", 2, 2, 3) + checkMethodMetrics("void A() {if (1 + 2 == 4) c();}", 2, 0, 1) + + // for + checkMethodMetrics("void A() {for (;a();) b(); }",2, 2, 2) + checkMethodMetrics("void A() {for (;a() || b();) c();}", 2, 2, 3) + checkMethodMetrics("void A() {for (;1 + 2 == 4;) c();}", 2, 0, 1) + + // while + checkMethodMetrics("void A() {while (a()) b();}",2, 2, 2) + checkMethodMetrics("void A() {while (a() || b()) c();}", 2, 2, 3) + checkMethodMetrics("void A() {while (1 + 2 == 4) c();}", 2, 0, 1) + + // switch with colon cases + checkMethodMetrics("void A() {switch (a()) { case 1: b();}}", 3, 0, 2) + checkMethodMetrics("void A() {switch (a()) { case 1: b(); case 2: c();}}", 5, 0, 3) + + // switch with lambda cases + checkMethodMetrics("void A() {switch (a()) { case 1 -> b();}}", 3, 0, 2) + checkMethodMetrics("void A() {switch (a()) { case 1 -> b(); case 2 -> c();}}", 5, 0, 3) + + // ternary + checkMethodMetrics("void A() {a() ? 1 : 2;}", 1, 2, 2) + checkMethodMetrics("void A() {a() || b()? 1 : 2;}", 1, 2, 3) + checkMethodMetrics("void A() {a() ? b() ? c()? 1 : 2 : 3 : 4;}", 1, 6, 4) + + // nested functions + } + + @Test + void testLambdaMetrics() throws Exception { + Clover2Registry registry + + // empty lambda + registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), + "class A{void A() { Runnable r = () -> { }; }}", 1, 2, 1, 0, 2) + assertEquals(0, getLambda(registry).getStatements().size()) + + // block lambda with one statement + registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), + "class A{void A() { Runnable r = () -> { return; }; }}", 1, 2, 2, 0, 2) + assertEquals(1, getLambda(registry).getStatements().size()) + assertSourceRegion(new FixedSourceRegion(1, 41, 1, 48), getLambda(registry).getStatements().get(0)) + + // expression lambda with one statement + registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), + "class A{void A() { Produce i = (x) -> 123; }}", 1, 2, 2, 0, 2) + assertEquals(1, getLambda(registry).getStatements().size()) + assertSourceRegion(new FixedSourceRegion(1, 48, 1, 51), getLambda(registry).getStatements().get(0)) + + // expression lambda with one statement and a branch condition + registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), + "class A{void A() { Produce i = (x) -> x < 0 ? x * x : -x; }}", 1, 2, 2, 2, 3) + assertEquals(1, getLambda(registry).getStatements().size()) + assertEquals(1, getLambda(registry).getBranches().size()) + assertSourceRegion(new FixedSourceRegion(1, 48, 1, 66), getLambda(registry).getStatements().get(0)) + + // lambda inside a lambda + registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), + "class A{void A() { Callable call = () -> () -> { return; }; }}", 1, 3, 3, 0, 3) + assertEquals(1, getLambda(registry).getStatements().size()); // outer lambda + assertEquals(1, getLambda(registry).getMethods().size()); // outer lambda + assertEquals(1, getLambda(registry).getMethods().get(0).getStatements().size()); // inner lambda + assertSourceRegion( + new FixedSourceRegion(1, 52, 1, 69), + getLambda(registry).getStatements().get(0)); // outer is "() -> { return; } + assertSourceRegion( + new FixedSourceRegion(1, 60, 1, 67), + getLambda(registry).getMethods().get(0).getStatements().get(0)); // inner is "return;" + + // method reference + registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), + "class A{void A() { Integer i = Math::abs; }}", 1, 2, 2, 0, 2) + assertEquals(1, getLambda(registry).getStatements().size()) + assertSourceRegion( + new FixedSourceRegion(1, 32, 1, 41), + getLambda(registry).getStatements().get(0)); // "Math::abs" + } + + private void checkMethodMetrics(String methodSrc,int numStatements, int numBranches, int methodComplexity) throws Exception { + checkMetrics("class A{"+methodSrc+"}", 1, 1, numStatements, numBranches, methodComplexity) + } + + private void checkMetrics(String src, int numClasses, int numMethods, int numStatements, int numBranches, int totalComplexity) throws Exception { + checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false), + src, numClasses, numMethods, numStatements, numBranches, totalComplexity) + } + + private static FullMethodInfo getLambda(Clover2Registry registry) { + return (FullMethodInfo) registry.getProject().findClass("A").getMethods().get(0).getMethods().get(0) + } + + private static void assertSourceRegion(FixedSourceRegion fixedSourceRegion, StatementInfo statementInfo) { + assertEquals(fixedSourceRegion.getStartLine(), statementInfo.getStartLine()) + assertEquals(fixedSourceRegion.getStartColumn(), statementInfo.getStartColumn()) + assertEquals(fixedSourceRegion.getEndLine(), statementInfo.getEndLine()) + assertEquals(fixedSourceRegion.getEndColumn(), statementInfo.getEndColumn()) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationStatementsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationStatementsTest.groovy new file mode 100644 index 00000000..12dc40f8 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationStatementsTest.groovy @@ -0,0 +1,22 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationStatementsTest extends InstrumentationTestBase { + + @Test + void testDiamondTypeArgs() throws Exception { + checkInstrumentation([ + [ "class B { private void a(int arg) { List l = new ArrayList<>(); new Foo<>(); }}", + "class B {$snifferField private void a(int arg) {RECORDER.inc(0); RECORDER.inc(1);List l = new ArrayList<>(); RECORDER.inc(2);new Foo<>(); }}"] + ] as String[][]) + } + + @Test + void testStatements() throws Exception { + checkInstrumentation([ + [ "class B { private void a(int arg) {hashCode();}}", + "class B {$snifferField private void a(int arg) {RECORDER.inc(0);RECORDER.inc(1);hashCode();}}" ] + ] as String[][]) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationSwitchStatementsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationSwitchStatementsTest.groovy new file mode 100644 index 00000000..d238815c --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationSwitchStatementsTest.groovy @@ -0,0 +1,88 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.api.registry.ClassInfo +import com.atlassian.clover.registry.Clover2Registry +import com.atlassian.clover.registry.entities.FullProjectInfo +import com_atlassian_clover.CloverVersionInfo +import org.junit.Test + +import static org.junit.Assert.assertTrue + +class InstrumentationSwitchStatementsTest extends InstrumentationTestBase { + + @Test + void testSwitchStatement() throws Exception { + String bp = "__CLB" + CloverVersionInfo.SANITIZED_RN + String src = "{int i = 0;switch(i){case 0:break;case 1:case 2:break;case 3:hashCode();break;}}}" + String instr = "{RECORDER.inc(0);RECORDER.inc(1);int i = 0;boolean "+bp+"_bool0=false;RECORDER.inc(2);switch(i){case 0:if (!"+bp+"_bool0)" + + " {RECORDER.inc(3);"+bp+"_bool0=true;}RECORDER.inc(4);break;case 1:if (!"+bp+"_bool0) {RECORDER.inc(5);"+bp+"_bool0=true;}" + + "case 2:if (!"+bp+"_bool0) {RECORDER.inc(6);"+bp+"_bool0=true;}RECORDER.inc(7);break;case 3:if (!"+bp+"_bool0) {RECORDER.inc(8);"+ + bp+"_bool0=true;}RECORDER.inc(9);hashCode();RECORDER.inc(10);break;}}}" + String instrWithSniffer = snifferField + instr + + // test suppress warnings injection + checkInstrumentation([ + // no existing suppression + [ "class A {" + src, + "@java.lang.SuppressWarnings({\"fallthrough\"}) class A {" + instrWithSniffer], + + // existing, empty, no brackets + [ "@SuppressWarnings class B {" + src, + "@SuppressWarnings({\"fallthrough\"}) class B {" + instrWithSniffer], + + // existing, empty + [ "@SuppressWarnings() class C {" + src, + "@SuppressWarnings({\"fallthrough\"}) class C {" + instrWithSniffer], + + // existing, single element in array init + [ "@SuppressWarnings({\"unchecked\"}) class D {" + src, + "@SuppressWarnings({\"unchecked\",\"fallthrough\"}) class D {" + instrWithSniffer], + + // existing, more than one array element + [ "@SuppressWarnings({\"unchecked\",\"fallthrough\"}) class E {" + src, + "@SuppressWarnings({\"unchecked\",\"fallthrough\"}) class E {" + instrWithSniffer], + + //existing, single value + [ "@SuppressWarnings(\"fallthrough\") class F {" + src, + "@SuppressWarnings(\"fallthrough\") class F {" + instrWithSniffer], + + //existing, full syntax + [ "@SuppressWarnings(value={\"unchecked\"}) class G {" + src, + "@SuppressWarnings(value={\"unchecked\",\"fallthrough\"}) class G {" + instrWithSniffer], + + //existing, full syntax without array init + [ "@SuppressWarnings(value=\"unchecked\") class H {" + src, + "@SuppressWarnings(value={\"unchecked\",\"fallthrough\"}) class H {" + instrWithSniffer], + + // no existing suppression, other annotation + [ "@MyAnnotation class I {" + src, + "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation class I {" + instrWithSniffer], + + // no existing suppression, other annotations + [ "@MyAnnotation @MyOtherAnno class J {" + src, + "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation @MyOtherAnno class J {" + instrWithSniffer], + + // no existing suppression, other annotations + [ "@MyAnnotation @MyOtherAnno class K { @YetMoreAnno public L() " + src, + "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation @MyOtherAnno class K {" + snifferField + " @YetMoreAnno public L() " + instr], + + // no existing suppression, other annotations + [ "@MyAnnotation @MyOtherAnno class M { @B @SuppressWarnings(\"fallthrough\") public N() " + src, + "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation @MyOtherAnno class M {" + snifferField + " @B @SuppressWarnings(\"fallthrough\") public N() " + instr], + ] as String[][]) + } + + @Test + void testSwitchCaseStatementPositions() throws Exception { + + Clover2Registry registry = performInstrumentation("class A { A() {/*1*/\nswitch(i) {/*2*/\ncase 1:return;/*3*/\ncase 2:return;/*4*/\ndefault:break;/*5*/\n}\n}}") + + FullProjectInfo proj = registry.getProject() + ClassInfo c = proj.findClass("A") + + for (com.atlassian.clover.api.registry.StatementInfo info : c.getMethods().get(0).getStatements()) { + assertTrue(info.getStartLine() <= info.getEndLine()) + assertTrue(info.getStartColumn() < info.getEndColumn()) + } + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTest.groovy deleted file mode 100755 index 366ec497..00000000 --- a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTest.groovy +++ /dev/null @@ -1,853 +0,0 @@ -package com.atlassian.clover.instr.java - -import com.atlassian.clover.api.CloverException -import com.atlassian.clover.api.registry.ClassInfo -import com.atlassian.clover.api.registry.StatementInfo -import com.atlassian.clover.cfg.instr.InstrumentationConfig -import com.atlassian.clover.cfg.instr.InstrumentationLevel -import com.atlassian.clover.CloverNames -import com.atlassian.clover.cfg.instr.java.JavaInstrumentationConfig -import com.atlassian.clover.cfg.instr.java.LambdaInstrumentation -import com.atlassian.clover.cfg.instr.java.SourceLevel -import com.atlassian.clover.registry.Clover2Registry -import com.atlassian.clover.registry.FixedSourceRegion -import com.atlassian.clover.registry.entities.FullMethodInfo -import com.atlassian.clover.registry.entities.FullProjectInfo -import com.atlassian.clover.registry.metrics.ProjectMetrics -import com.atlassian.clover.util.FileUtils -import com_atlassian_clover.CloverVersionInfo -import com_atlassian_clover.CoverageRecorder -import com_atlassian_clover.TestNameSniffer -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestName - -import static org.hamcrest.CoreMatchers.equalTo -import static org.hamcrest.MatcherAssert.assertThat -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertTrue -import static org.junit.Assert.fail - -public class InstrumentationTest { - private File workingDir - - private String snifferField = "public static final " + TestNameSniffer.class.getName() + " SNIFFER=" + - TestNameSniffer.class.getName() + ".NULL_INSTANCE;" - - @Rule - public TestName name = new TestName() - - @Before - public void setUp() - throws Exception { - workingDir = File.createTempFile(getClass().getName() + "." + name, ".tmp") - workingDir.delete() - workingDir.mkdir() - } - - @After - public void tearDown() - throws Exception { - FileUtils.deltree(workingDir) - } - - @Test - public void testAssertStatements() - throws Exception { - checkStatement( - " assert arg > 0;", - " assert (((arg > 0)&&(RECORDER.iget(1)!=0|true))||(RECORDER.iget(2)==0&false));") - checkStatement( - " assert arg > 0 : \"arg must be greater than zero\";", - " assert (((arg > 0 )&&(RECORDER.iget(1)!=0|true))||(RECORDER.iget(2)==0&false)): \"arg must be greater than zero\";") - } - - @Test - public void testMethodEntries() - throws Exception { - checkInstrumentation([ - ["class B { B(int arg) {}}", - "class B {$snifferField B(int arg) {RECORDER.inc(0);}}"], - ["class B { public B(int arg) {}}", - "class B {$snifferField public B(int arg) {RECORDER.inc(0);}}"], - ["class B { protected B(int arg) {}}", - "class B {$snifferField protected B(int arg) {RECORDER.inc(0);}}"], - ["class B { private B(int arg) {}}", - "class B {$snifferField private B(int arg) {RECORDER.inc(0);}}"], - ["class B { void a(int arg) { }}", - "class B {$snifferField void a(int arg) {RECORDER.inc(0); }}"], - ["class B { public void a(int arg) { }}", - "class B {$snifferField public void a(int arg) {RECORDER.inc(0); }}"], - ["class B { protected void a(int arg) { }}", - "class B {$snifferField protected void a(int arg) {RECORDER.inc(0); }}"], - ["class B { private void a(int arg) { }}", - "class B {$snifferField private void a(int arg) {RECORDER.inc(0); }}"], - ["class B { private void a(int arg) {}\nprivate void b() {}\nprivate void c() {}}", - "class B {$snifferField private void a(int arg) {RECORDER.inc(0);}\nprivate" + - " void b() {RECORDER.inc(1);}\nprivate void c() {RECORDER.inc(2);}}"] - ] as String[][]) - } - - @Test - public void testStatements() throws Exception { - checkInstrumentation([ - [ "class B { private void a(int arg) {hashCode();}}", - "class B {$snifferField private void a(int arg) {RECORDER.inc(0);RECORDER.inc(1);hashCode();}}" ] - ] as String[][]) - } - - @Test - public void testTryResourceStatements() throws Exception { - checkInstrumentation([ - [ "class B { private void a(int arg) { try(A a = new A()) { } catch(Exception e) { } finally{ } }}", - "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A()) { } catch(Exception e) { } finally{ } }}'] - ] as String[][]) - checkInstrumentation([ - [ "class B { private void a(int arg) { try(A a = new A();) { } catch(Exception e) { } finally{ } }}", - "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A();) { } catch(Exception e) { } finally{ } }}'] - ] as String[][]) - checkInstrumentation([ - [ "class B { private void a(int arg) { try(A a = new A(); B b = new B()) { } catch(Exception e) { } finally{ } }}", - "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A(); RECORDER$AC0 CLR$ACI1=new RECORDER$AC0(){{RECORDER.inc(3);}};B b = new B()) { } catch(Exception e) { } finally{ } }}'] - ] as String[][]) - checkInstrumentation([ - [ "class B { private void a(int arg) { try(A a = new A(); B b = new B();) { } catch(Exception e) { } finally{ } }}", - "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A(); RECORDER$AC0 CLR$ACI1=new RECORDER$AC0(){{RECORDER.inc(3);}};B b = new B();) { } catch(Exception e) { } finally{ } }}'] - ] as String[][]) - } - - @Test - public void testMultiCatchBlocks() throws Exception { - checkInstrumentation([ - [ "class B { private void a(int arg) { try { } catch(FooException|BarException e) { } }}", - "class B {$snifferField private void a(int arg) {RECORDER.inc(0); RECORDER.inc(1);try { } catch(FooException|BarException e) { } }}"] - ] as String[][]) - checkInstrumentation([ - [ "class B { private void a(int arg) { try { } catch(final FooException|BarException|BazException e) { } }}", - "class B {$snifferField private void a(int arg) {RECORDER.inc(0); RECORDER.inc(1);try { } catch(final FooException|BarException|BazException e) { } }}"] - ] as String[][]) - } - - @Test - public void testDiamondTypeArgs() throws Exception { - checkInstrumentation([ - [ "class B { private void a(int arg) { List l = new ArrayList<>(); new Foo<>(); }}", - "class B {$snifferField private void a(int arg) {RECORDER.inc(0); RECORDER.inc(1);List l = new ArrayList<>(); RECORDER.inc(2);new Foo<>(); }}"] - ] as String[][]) - } - - @Test - public void testNumericLiterals() throws Exception { - //More an test of language recognition than an instrumentation test - //Tests int, long, float, doubles in hex, binary, decimal, octal, some with underscores, some without - checkInstrumentation([ - [ "class B { static double[] doubles = new double[] { 09, 0_9, 0_0, 0x1_2_3, 1234_5678, 1_2_3_4__5_6_7_8L, 0b0001_0010_0100_1000, 3.141_592_653_589_793d, 0x1.ffff_ffff_ffff_fP1_023, 0x1111_2222_3333_4444L, 0x0.0000000000001P-1f, 0x0.0000000000001P-1_1d }; }", - "class B { static double[] doubles = new double[] { 09, 0_9, 0_0, 0x1_2_3, 1234_5678, 1_2_3_4__5_6_7_8L, 0b0001_0010_0100_1000, 3.141_592_653_589_793d, 0x1.ffff_ffff_ffff_fP1_023, 0x1111_2222_3333_4444L, 0x0.0000000000001P-1f, 0x0.0000000000001P-1_1d }; }"] - ] as String[][]) - } - - @Test - public void testBinaryLiterals() throws Exception { - //More an test of language recognition than an instrumentation test - //Test binary literals (int, long with underscores) - checkInstrumentation([ - [ "class B { static double[] doubles = new double[] { 0b0, 0b1, 0b10101010, 0b1010_1010, 0b1010_1010L, 0b10101010L }; }", - "class B { static double[] doubles = new double[] { 0b0, 0b1, 0b10101010, 0b1010_1010, 0b1010_1010L, 0b10101010L }; }"] - ] as String[][]) - } - - /** - * Test for CLOV-669 - */ - @Test - public void testUnicodeCharacters() - throws Exception { - - String instr = getInstrumentedVersion("class B { private void a(int arg) {String s = \"\u023a\";}}", false) - assertNotNull(instr) - } - - @Test - public void testTernaryOperator() - throws Exception { - // just a simple assignment - checkStatement("int i = arg == 2 ? 3 : 4;", - "RECORDER.inc(1);int i = (((arg == 2 )&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false))? 3 : 4;") - - // two ternary's embeded - checkStatement("int i = arg == (b==2?1:2) ? 3 : 4;", - "RECORDER.inc(1);int i = (((arg == (" + - "(((b==2)&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false))?1:2" + - ") )&&(RECORDER.iget(4)!=0|true))||(RECORDER.iget(5)==0&false))? 3 : 4;") - } - - @Test - public void testSwitchStatement() throws Exception { - String bp = "__CLB" + CloverVersionInfo.SANITIZED_RN - String src = "{int i = 0;switch(i){case 0:break;case 1:case 2:break;case 3:hashCode();break;}}}" - String instr = "{RECORDER.inc(0);RECORDER.inc(1);int i = 0;boolean "+bp+"_bool0=false;RECORDER.inc(2);switch(i){case 0:if (!"+bp+"_bool0)" + - " {RECORDER.inc(3);"+bp+"_bool0=true;}RECORDER.inc(4);break;case 1:if (!"+bp+"_bool0) {RECORDER.inc(5);"+bp+"_bool0=true;}" + - "case 2:if (!"+bp+"_bool0) {RECORDER.inc(6);"+bp+"_bool0=true;}RECORDER.inc(7);break;case 3:if (!"+bp+"_bool0) {RECORDER.inc(8);"+ - bp+"_bool0=true;}RECORDER.inc(9);hashCode();RECORDER.inc(10);break;}}}" - String instrWithSniffer = snifferField + instr - - // test suppress warnings injection - checkInstrumentation([ - // no existing suppression - [ "class A {" + src, - "@java.lang.SuppressWarnings({\"fallthrough\"}) class A {" + instrWithSniffer], - - // existing, empty, no brackets - [ "@SuppressWarnings class B {" + src, - "@SuppressWarnings({\"fallthrough\"}) class B {" + instrWithSniffer], - - // existing, empty - [ "@SuppressWarnings() class C {" + src, - "@SuppressWarnings({\"fallthrough\"}) class C {" + instrWithSniffer], - - // existing, single element in array init - [ "@SuppressWarnings({\"unchecked\"}) class D {" + src, - "@SuppressWarnings({\"unchecked\",\"fallthrough\"}) class D {" + instrWithSniffer], - - // existing, more than one array element - [ "@SuppressWarnings({\"unchecked\",\"fallthrough\"}) class E {" + src, - "@SuppressWarnings({\"unchecked\",\"fallthrough\"}) class E {" + instrWithSniffer], - - //existing, single value - [ "@SuppressWarnings(\"fallthrough\") class F {" + src, - "@SuppressWarnings(\"fallthrough\") class F {" + instrWithSniffer], - - //existing, full syntax - [ "@SuppressWarnings(value={\"unchecked\"}) class G {" + src, - "@SuppressWarnings(value={\"unchecked\",\"fallthrough\"}) class G {" + instrWithSniffer], - - //existing, full syntax without array init - [ "@SuppressWarnings(value=\"unchecked\") class H {" + src, - "@SuppressWarnings(value={\"unchecked\",\"fallthrough\"}) class H {" + instrWithSniffer], - - // no existing suppression, other annotation - [ "@MyAnnotation class I {" + src, - "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation class I {" + instrWithSniffer], - - // no existing suppression, other annotations - [ "@MyAnnotation @MyOtherAnno class J {" + src, - "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation @MyOtherAnno class J {" + instrWithSniffer], - - // no existing suppression, other annotations - [ "@MyAnnotation @MyOtherAnno class K { @YetMoreAnno public L() " + src, - "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation @MyOtherAnno class K {" + snifferField + " @YetMoreAnno public L() " + instr], - - // no existing suppression, other annotations - [ "@MyAnnotation @MyOtherAnno class M { @B @SuppressWarnings(\"fallthrough\") public N() " + src, - "@java.lang.SuppressWarnings({\"fallthrough\"}) @MyAnnotation @MyOtherAnno class M {" + snifferField + " @B @SuppressWarnings(\"fallthrough\") public N() " + instr], - ] as String[][]) - } - - @Test - public void testConstExpr() - throws Exception { - // constant for loop - checkStatement( - "for (;true;) {System.out.println(a[i]);}", - "RECORDER.inc(1);for (;true;) {{RECORDER.inc(2);System.out.println(a[i]);}}") - } - - @Test - public void testForLoop() - throws Exception { - // traditional for loop - checkStatement( - "for (int i = 0; i < a.length; i++) {System.out.println(a[i]);}", - "RECORDER.inc(1);for (int i = 0; (((i < a.length)&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false)); i++) {{RECORDER.inc(4);System.out.println(a[i]);}}") - // traditional for loop, no braces - checkStatement( - "for (int i = 0; i < a.length; i++) System.out.println(a[i]);", - "RECORDER.inc(1);for (int i = 0; (((i < a.length)&&(RECORDER.iget(2)!=0|true))||(RECORDER.iget(3)==0&false)); i++) {RECORDER.inc(4);System.out.println(a[i]);}") - // enhanced for loop - checkStatement( - "for (int i : a) {System.out.println(i);}", - "RECORDER.inc(1);for (int i : a) {{RECORDER.inc(2);System.out.println(i);}}") - } - - @Test - public void testConditionalWithAssignment() throws Exception { - // Don't instrument a conditional containing an assignment since this breaks Definite Assignment rules in javac - checkStatement( - "String line; while ((line = in.readLine()) != null) {System.out.println(line);}", - "RECORDER.inc(1);String line; RECORDER.inc(2);while ((line = in.readLine()) != null) {{RECORDER.inc(5);System.out.println(line);}}") - - } - - @Test - public void testConditionalWithCloverOff() throws Exception { - // Preserve a conditional and don't add extraneous parentheses when CLOVER:OFF is specified as this may trigger - // compiler bugs for generic calls. See http://bugs.sun.com/view_bug.do?bug_id=6608961 - - //Note Clover inserts extraneous curly braces too - checkStatement( - "\n///CLOVER:OFF\nif (a + b == 2) { System.out.println(\"Hello, world\"); }\n///CLOVER:ON\n", - "\n///CLOVER:OFF\nif (a + b == 2) {{ System.out.println(\"Hello, world\"); }\n///CLOVER:ON\n}") - - } - - @Test - public void testMethodLevelInstr() throws Exception { - checkStatement("int i = 0;", "int i = 0;", InstrumentationLevel.METHOD.ordinal()) - checkStatement("int i = arg == 2 ? 3 : 4;", "int i = arg == 2 ? 3 : 4;", InstrumentationLevel.METHOD.ordinal()) - checkStatement("assert arg > 0;", "assert arg > 0;", InstrumentationLevel.METHOD.ordinal()) - - JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) - config.setFlushPolicy(InstrumentationConfig.INTERVAL_FLUSHING) - config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) - checkInstrumentation([ - ["class B { public B(int arg) {}}", - "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);}finally{RECORDER.maybeFlush();}}}"] - ] as String[][], - config) - - config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) - config.setFlushPolicy(InstrumentationConfig.INTERVAL_FLUSHING) - config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) - - checkInstrumentation([ - ["class B { public B(int arg) {int i = 0;}}", - "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);int i = 0;}finally{RECORDER.maybeFlush();}}}"] - ] as String[][], - config) - - config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) - config.setFlushPolicy(InstrumentationConfig.THREADED_FLUSHING) - config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) - - checkInstrumentation([ - ["class B { public B(int arg) {}}", - "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);}finally{RECORDER.flushNeeded();}}}"] - ] as String[][], - config) - - config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) - config.setFlushPolicy(InstrumentationConfig.THREADED_FLUSHING) - config.setInstrLevel(InstrumentationLevel.METHOD.ordinal()) - - checkInstrumentation([ - ["class B { public B(int arg) {int i = 0;}}", - "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);int i = 0;}finally{RECORDER.flushNeeded();}}}"] - ] as String[][], - config) - } - - @Test - public void testTestMethod() throws Exception { - checkInstrumentation([ - [ "public class MyTest { @Test public Class foo(){} }", - "public class MyTest {" + snifferField + " @Test public Class foo(){" + "RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER();"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTest { @Test public static Class foo(){} }", - "public class MyTest {" + snifferField + " @Test public static Class foo(){RECORDER.globalSliceStart(MyTest.class.getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER();"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(MyTest.class.getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private static Class RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTest { @Test public Class foo(int a, boolean b){} }", - "public class MyTest {" + snifferField + " @Test public Class foo(int a, boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(int a, boolean b){RECORDER.inc(0);} }" ], - - [ "public class MyTest { @Test public Class foo(int a[], boolean b){} }", - "public class MyTest {" + snifferField + " @Test public Class foo(int a[], boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(int a[], boolean b){RECORDER.inc(0);} }" ], - - [ "public class MyTest { @Test public Class foo(int a[], boolean b){} }", - "public class MyTest {" + snifferField + " @Test public Class foo(int a[], boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(int a[], boolean b){RECORDER.inc(0);} }" ], - - [ "public class MyTest { @Test public T foo(int a[], boolean b){} }", - "public class MyTest {" + snifferField + " @Test public T foo(int a[], boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{T "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private T RECORDER(int a[], boolean b){RECORDER.inc(0);} }" ], - ] as String[][]) - } - - @Test - public void testTestMethodWithNoRewrite() throws Exception { - checkInstrumentation([ - ["public class MyTest { @Test public void testFoo(){} }", - "public class MyTest {" + snifferField + " @Test public void testFoo(){try{RECORDER.globalSliceStart(getClass().getName(),0);RECORDER.inc(0);}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.testFoo\",SNIFFER.getTestName(),0);}} }" ], - - ["public class MyTest { @Test public static void testFoo(){} }", - "public class MyTest {" + snifferField + " @Test public static void testFoo(){try{RECORDER.globalSliceStart(MyTest.class.getName(),0);RECORDER.inc(0);}finally{RECORDER.globalSliceEnd(MyTest.class.getName(),\"MyTest.testFoo\",SNIFFER.getTestName(),0);}} }"] - ] as String[][], false) - } - - @Test - public void testExpectedExceptions() throws Exception { - checkInstrumentation([ - [ "public class MyTestA { @Test public void foo(){} }", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestB { /** @testng.test */public void foo(){} }", - "public class MyTestB {" + snifferField + " /** @testng.test */public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestB.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestC { @Test(expectedExceptions={}) public void foo(){} }", - "public class MyTestC {" + snifferField + " @Test(expectedExceptions={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestC.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestD { /** @testng.test expectedExceptions=\"\" */public void foo(){} }", - "public class MyTestD {" + snifferField + " /** @testng.test expectedExceptions=\"\" */public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestD.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestE { @org.testng.annotations.Test(expectedExceptions={}) public void foo(){} }", - "public class MyTestE {" + snifferField + " @org.testng.annotations.Test(expectedExceptions={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestE.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestF { @Test(expected={}) public void foo(){} }", - "public class MyTestF {" + snifferField + " @Test(expected={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestF.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestG { @org.junit.Test(expected={}) public void foo(){} }", - "public class MyTestG {" + snifferField + " @org.junit.Test(expected={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestG.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestH { @Test @ExpectedExceptions({}) public void foo(){} }", - "public class MyTestH {" + snifferField + " @Test @ExpectedExceptions({}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestH.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestI { /** @testng.test\n* @testng.expected-exceptions value=\"\" */\npublic void foo(){} }", - "public class MyTestI {" + snifferField + " /** @testng.test\n* @testng.expected-exceptions value=\"\" */\npublic void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestI.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestJ { @Test @org.testng.annotations.ExpectedExceptions({}) public void foo(){} }", - "public class MyTestJ {" + snifferField + " @Test @org.testng.annotations.ExpectedExceptions({}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestJ.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestK { @org.testng.annotations.Test(expectedExceptions={Foo.class,Bar.class}) public void foo(){} }", - "public class MyTestK {" + snifferField + " @org.testng.annotations.Test(expectedExceptions={Foo.class,Bar.class}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestK.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestL { /** @testng.test expectedExceptions = \"Foo Bar\" */public void foo(){} }", - "public class MyTestL {" + snifferField + " /** @testng.test expectedExceptions = \"Foo Bar\" */public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestL.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestM { @Test @ExpectedExceptions({Foo.class,Bar.class}) public void foo(){} }", - "public class MyTestM {" + snifferField + " @Test @ExpectedExceptions({Foo.class,Bar.class}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestM.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestN { @org.testng.annotations.Test @org.testng.annotations.ExpectedExceptions({Foo.class,Bar.class}) public void foo(){} }", - "public class MyTestN {" + snifferField + " @org.testng.annotations.Test @org.testng.annotations.ExpectedExceptions({Foo.class,Bar.class}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestN.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestO { /** @testng.test\n * @testng.expected-exceptions value = \"Foo Bar\"\n*/\npublic void foo(){} }", - "public class MyTestO {" + snifferField + " /** @testng.test\n * @testng.expected-exceptions value = \"Foo Bar\"\n*/\npublic void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestO.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], - - [ "public class MyTestP { @Test(expected=Foo.class) public void foo() throws Foo, Bar {} }", - "public class MyTestP {" + snifferField + " @Test(expected=Foo.class) public void foo() throws Foo, Bar {RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestP.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER() throws Foo, Bar{RECORDER.inc(0);} }" ], - ] as String[][]) - } - - @Test - public void testLambdaExprToBlockReplace() throws Exception { - checkInstrumentation([ - [ "public class MyTestA { @Test public void foo(){Stream.of(1, 2, 3, 4, 5, 6).map((i) -> \"String:\" + String.valueOf(i));} }", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);RECORDER.inc(1);Stream.of(1, 2, 3, 4, 5, 6).map((i) -> {RECORDER.inc(2);return \"String:\" + String.valueOf(i);});} }" ], - [ "public class MyTestA { @Test public void foo(){map((int i) -> /*CLOVER:VOID*/ System.out.println(i));} }", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);RECORDER.inc(1);map((int i) -> /*CLOVER:VOID*/ {RECORDER.inc(2);System.out.println(i);});} }" ], - [ "public class MyTestA { @Test public void foo(){ map((i) -> /** CLOVER:VOID */ System.out.println(\"String:\" + String.valueOf(i))); }}", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> /** CLOVER:VOID */ {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], - [ "public class MyTestA { @Test public void foo(){ map((i) -> /* \t\n\r CLOVER:VOID */ System.out.println(\"String:\" + String.valueOf(i))); }}", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> /* \t\n\r CLOVER:VOID */ {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], - [ "public class MyTestA { @Test public void foo(){ map((i) -> ///CLOVER:VOID\n System.out.println(\"String:\" + String.valueOf(i))); }}", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> ///CLOVER:VOID\n {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], - [ "public class MyTestA { @Test public void foo(){ map((i) -> // /CLOVER:VOID\n System.out.println(\"String:\" + String.valueOf(i))); }}", - "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0); RECORDER.inc(1);map((i) -> // /CLOVER:VOID\n {RECORDER.inc(2);System.out.println(\"String:\" + String.valueOf(i));}); }}" ], - ] as String[][]) - } - - @Test - public void testCloverOnOff() throws Exception { - checkInstrumentation([ - ["/*///CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}", - "/*///CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}"], - ["package A; /*///CLOVER:OFF*/ class B { public B() {int i = 0;}}", - "package A; /*///CLOVER:OFF*/ class B { public B() {int i = 0;}}"], - ["/*CLOVER:OFF*/ class B { public B() {int i = 0;}}", - "/*CLOVER:OFF*/ class B { public B() {int i = 0;}}"], - // test second directive prefix, added for eclipse auto-format support - ["/*// /CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}", - "/*// /CLOVER:OFF*/ package A; class B { public B() {int i = 0;}}"], - ["package A; /*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}", - "package A; /*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}"], - ["/*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}", - "/*// /CLOVER:OFF*/ class B { public B() {int i = 0;}}"], - - ["class B { ///CLOVER:OFF\nprivate B() {int i = 0;}}", - "class B { ///CLOVER:OFF\nprivate B() {int i = 0;}}"], - ["class B { ///CLOVER:OFF\nprivate B() {}}", - "class B { ///CLOVER:OFF\nprivate B() {}}"], - ["class B { ///CLOVER:OFF\nprivate B() {}\n///CLOVER:ON\n}", - "class B { ///CLOVER:OFF\nprivate B() {}\n///CLOVER:ON\n}"], - ["class B { private B() {///CLOVER:OFF\n}\n///CLOVER:ON\n}", - "class B {" + snifferField + " private B() {RECORDER.inc(0);///CLOVER:OFF\n}\n///CLOVER:ON\n}"], - ["class B { private B() {///CLOVER:OFF\nint i = 0;///CLOVER:ON\n}}", - "class B {" + snifferField + " private B() {RECORDER.inc(0);///CLOVER:OFF\nint i = 0;///CLOVER:ON\n}}"], - ["class B { private B() {///CLOVER:OFF\nhashCode();///CLOVER:ON\n}}", - "class B {" + snifferField + " private B() {RECORDER.inc(0);///CLOVER:OFF\nhashCode();///CLOVER:ON\n}}"], - - ] as String[][]) - - checkInstrumentation([ - ["/*///CLOVER:OFF*/ public class MyTest { @Test public void foo(){} }", - "/*///CLOVER:OFF*/ public class MyTest { @Test public void foo(){} }"], - ["public class MyTest {/*///CLOVER:OFF*/ @Test public void foo(){} }", - "public class MyTest {/*///CLOVER:OFF*/ @Test public void foo(){} }"], - ["public class MyTest { @Test public void foo()/*///CLOVER:OFF*/{} }", - "public class MyTest { @Test public void foo()/*///CLOVER:OFF*/{} }"], - ["/*///CLOVER:OFF*/ public class MyTest { public void testFoo(){} }", - "/*///CLOVER:OFF*/ public class MyTest { public void testFoo(){} }"], - ["public class MyTest {/*///CLOVER:OFF*/ public void testFoo(){} }", - "public class MyTest {/*///CLOVER:OFF*/ public void testFoo(){} }"], - ["public class MyTest { public void testFoo()/*///CLOVER:OFF*/{} }", - "public class MyTest { public void testFoo()/*///CLOVER:OFF*/{} }"] - ] as String[][]) - } - - @Test - public void testMethodMetrics() throws Exception { - checkMethodMetrics("void A() {}",0, 0, 1) - checkMethodMetrics("void A() {a();}",1, 0, 1) - checkMethodMetrics("void A() {a = (6 < 7);}",1 ,0 ,1) - checkMethodMetrics("void A() {a();b();c();}",3, 0, 1) - - // if - checkMethodMetrics("void A() {if (a()) b(); else c();}",3, 2, 2) - checkMethodMetrics("void A() {if (a() || b()) c();}", 2, 2, 3) - checkMethodMetrics("void A() {if (1 + 2 == 4) c();}", 2, 0, 1) - - // for - checkMethodMetrics("void A() {for (;a();) b(); }",2, 2, 2) - checkMethodMetrics("void A() {for (;a() || b();) c();}", 2, 2, 3) - checkMethodMetrics("void A() {for (;1 + 2 == 4;) c();}", 2, 0, 1) - - // while - checkMethodMetrics("void A() {while (a()) b();}",2, 2, 2) - checkMethodMetrics("void A() {while (a() || b()) c();}", 2, 2, 3) - checkMethodMetrics("void A() {while (1 + 2 == 4) c();}", 2, 0, 1) - - // switch - checkMethodMetrics("void A() {switch (a()) { case 1: b();}}", 3, 0, 2) - checkMethodMetrics("void A() {switch (a()) { case 1: b(); case 2: c();}}", 5, 0, 3) - - // ternary - checkMethodMetrics("void A() {a() ? 1 : 2;}", 1, 2, 2) - checkMethodMetrics("void A() {a() || b()? 1 : 2;}", 1, 2, 3) - checkMethodMetrics("void A() {a() ? b() ? c()? 1 : 2 : 3 : 4;}", 1, 6, 4) - - // nested functions - } - - @Test - public void testLambdaMetrics() throws Exception { - Clover2Registry registry - - // empty lambda - registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), - "class A{void A() { Runnable r = () -> { }; }}", 1, 2, 1, 0, 2) - assertEquals(0, getLambda(registry).getStatements().size()) - - // block lambda with one statement - registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), - "class A{void A() { Runnable r = () -> { return; }; }}", 1, 2, 2, 0, 2) - assertEquals(1, getLambda(registry).getStatements().size()) - assertSourceRegion(new FixedSourceRegion(1, 41, 1, 48), getLambda(registry).getStatements().get(0)) - - // expression lambda with one statement - registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), - "class A{void A() { Produce i = (x) -> 123; }}", 1, 2, 2, 0, 2) - assertEquals(1, getLambda(registry).getStatements().size()) - assertSourceRegion(new FixedSourceRegion(1, 48, 1, 51), getLambda(registry).getStatements().get(0)) - - // expression lambda with one statement and a branch condition - registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), - "class A{void A() { Produce i = (x) -> x < 0 ? x * x : -x; }}", 1, 2, 2, 2, 3) - assertEquals(1, getLambda(registry).getStatements().size()) - assertEquals(1, getLambda(registry).getBranches().size()) - assertSourceRegion(new FixedSourceRegion(1, 48, 1, 66), getLambda(registry).getStatements().get(0)) - - // lambda inside a lambda - registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), - "class A{void A() { Callable call = () -> () -> { return; }; }}", 1, 3, 3, 0, 3) - assertEquals(1, getLambda(registry).getStatements().size()); // outer lambda - assertEquals(1, getLambda(registry).getMethods().size()); // outer lambda - assertEquals(1, getLambda(registry).getMethods().get(0).getStatements().size()); // inner lambda - assertSourceRegion( - new FixedSourceRegion(1, 52, 1, 69), - getLambda(registry).getStatements().get(0)); // outer is "() -> { return; } - assertSourceRegion( - new FixedSourceRegion(1, 60, 1, 67), - getLambda(registry).getMethods().get(0).getStatements().get(0)); // inner is "return;" - - // method reference - registry = checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, true), - "class A{void A() { Integer i = Math::abs; }}", 1, 2, 2, 0, 2) - assertEquals(1, getLambda(registry).getStatements().size()) - assertSourceRegion( - new FixedSourceRegion(1, 32, 1, 41), - getLambda(registry).getStatements().get(0)); // "Math::abs" - } - - private FullMethodInfo getLambda(Clover2Registry registry) { - return (FullMethodInfo) registry.getProject().findClass("A").getMethods().get(0).getMethods().get(0) - } - - private void assertSourceRegion(FixedSourceRegion fixedSourceRegion, StatementInfo statementInfo) { - assertEquals(fixedSourceRegion.getStartLine(), statementInfo.getStartLine()) - assertEquals(fixedSourceRegion.getStartColumn(), statementInfo.getStartColumn()) - assertEquals(fixedSourceRegion.getEndLine(), statementInfo.getEndLine()) - assertEquals(fixedSourceRegion.getEndColumn(), statementInfo.getEndColumn()) - } - - @Test - public void testThreadedFlushing() throws Exception { - JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, false, false) - config.setFlushPolicy(InstrumentationConfig.THREADED_FLUSHING) - checkInstrumentation([ - ["class B { public B(int arg) {int i = 0;}}", - "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);RECORDER.inc(1);int i = 0;}finally{RECORDER.flushNeeded();}}}"], - ] as String[][], - config) - } - - @Test - public void testIntervalFlushing() throws Exception { - JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, false, false) - config.setFlushPolicy(InstrumentationConfig.INTERVAL_FLUSHING) - checkInstrumentation([ - ["class B { public B(int arg) {}}", - "class B {" + snifferField + " public B(int arg) {try{RECORDER.inc(0);}finally{RECORDER.maybeFlush();}}}"], - ] as String[][], - config) - - } - - // tests that "empty" classes don't get a recorder member when they don't need one - @Test - public void testDirtyDetection() throws Exception { - final String recMember = "" - checkInstrumentation(recMember, [ - ["class A {}", "class A {}"], - ["interface A {}", "interface A {}"], - ["@interface A {}", "@interface A {}"], - ["enum A {}", "enum A {}"], - ["enum A { apple,banana,pear }", "enum A { apple,banana,pear }"], - ["enum A { apple,banana,pear }", "enum A { apple,banana,pear }"], - // second top-level class. - ["class A {public A(){}} class B {}","class A {" + recMember + snifferField + "public A(){RECORDER.inc(0);}} class B {}"] - ] as String[][], true) - } - - @Test - public void testDoubleInstrDetection() throws Exception { - checkInstrumentation([ - [ "class A {}", "class A {}" ], //input is smaller than the marker - [ "", "" ] // empty source file - ] as String[][]); - try { - checkInstrumentation([ - [ CloverTokenStreamFilter.MARKER +"*/" ] - ] as String[][]) - fail("instrumentation marker not detected") - } - catch (CloverException e) { - - } - } - - @Test - public void testSwitchCaseStatementPositions() throws Exception { - - Clover2Registry registry = performInstrumentation("class A { A() {/*1*/\nswitch(i) {/*2*/\ncase 1:return;/*3*/\ncase 2:return;/*4*/\ndefault:break;/*5*/\n}\n}}") - - FullProjectInfo proj = registry.getProject() - ClassInfo c = proj.findClass("A") - - for (com.atlassian.clover.api.registry.StatementInfo info : c.getMethods().get(0).getStatements()) { - assertTrue(info.getStartLine() <= info.getEndLine()) - assertTrue(info.getStartColumn() < info.getEndColumn()) - } - } - - @Test - public void testDefaultInitString() throws Exception { - final File dbDir = new File(workingDir, InstrumentationConfig.DEFAULT_DB_DIR) - final File dbFile = new File(dbDir, InstrumentationConfig.DEFAULT_DB_FILE) - - - String defaultInitStr = dbFile.getAbsolutePath() - - String out = getInstrumentedVersion(null, false, "class B { B(int arg) {}}") - - String expectedCharArray = RecorderInstrEmitter.asUnicodeString(defaultInitStr) - - assertTrue(out.contains(expectedCharArray)) - } - - @Test - public void testRelativeDefaultInitString() throws Exception { - String defaultRelInitStr = InstrumentationConfig.DEFAULT_DB_DIR + File.separator + InstrumentationConfig.DEFAULT_DB_FILE - - - String out = getInstrumentedVersion(null, true, "class B { B(int arg) {}}") - String expectedCharArray = RecorderInstrEmitter.asUnicodeString(defaultRelInitStr) - - assertTrue(out.contains(expectedCharArray)) - File file = new File(defaultRelInitStr) - assertTrue("Could not delete file referenced by default initstring: " + file.getAbsolutePath(), file.delete()) - } - - @Test - public void testRelativeInitString() throws Exception { - File coverageDbFile = File.createTempFile(getClass().getName() +"." + name, ".tmp", workingDir) - coverageDbFile.delete() - - String out = getInstrumentedVersion(coverageDbFile.getPath(), true, "class B { B(int arg) {}}") - - String expectedCharArray = RecorderInstrEmitter.asUnicodeString(coverageDbFile.getPath()) - - assertTrue(out.contains(expectedCharArray)) - } - - @Test - public void testInitString() throws Exception { - File coverageDbFile = File.createTempFile(getClass().getName() +"." + name, ".tmp", workingDir) - coverageDbFile.delete() - - String out = getInstrumentedVersion(coverageDbFile.getAbsolutePath(), false, "class B { B(int arg) {}}") - - String expectedCharArray = RecorderInstrEmitter.asUnicodeString(coverageDbFile.getAbsolutePath()) - - assertTrue(out.contains(expectedCharArray)) - } - - private void checkStatement(String test, String expected, int instrumentationlevel) throws Exception { - JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) - config.setInstrLevel(instrumentationlevel) - checkInstrumentation([ - ["class B { private void a(int arg) {" + test + "}}", - "class B {" + snifferField + " private void a(int arg) {RECORDER.inc(0);" + expected + "}}"] - ] as String[][], config) - } - - private void checkStatement(String test, String expected) throws Exception { - checkStatement(test, expected, InstrumentationLevel.STATEMENT.ordinal()) - } - - // array of {input, expected output} - to prevent the prefix of the string being compared, - // mark the start point for comparison with ST_POINT - private void checkInstrumentation(String[][] testcases, boolean testRewriting) throws Exception { - checkInstrumentation("", testcases, testRewriting) - } - - private void checkInstrumentation(String[][] testcases) throws Exception { - checkInstrumentation("", testcases, true) - } - - // check array of {input, expected output} - replacing the recorder member with recorderStr. - private void checkInstrumentation(String recorderStr, String[][] testcases, boolean testRewriting) throws Exception { - for (String[] testcase : testcases) { - String instr = getInstrumentedVersion(testcase[0], testRewriting) - checkStringSuffix(recorderStr, CloverTokenStreamFilter.MARKER + testcase[1], instr) - } - } - - // check array of {input, expected output} - replacing the recorder member with recorderStr. - private void checkInstrumentation(String[][] testcases, JavaInstrumentationConfig config) throws Exception { - for (String[] testcase : testcases) { - String instr = getInstrumentedVersion(testcase[0], config) - checkStringSuffix("", CloverTokenStreamFilter.MARKER + testcase[1], instr) - } - } - - private static final String SNIFFER_REGEX = CloverNames.CLOVER_PREFIX + "[_0-9]+_TEST_NAME_SNIFFER" - private static final String RECORDER_REGEX = CloverNames.CLOVER_PREFIX + "[_A-Za-z0-9]+" - private static final String CLR_REGEX = CloverNames.CLOVER_PREFIX - private static final String RECORDER_INNER_MEMBER_REGEX = "public static " + CoverageRecorder.class.getName() + " " + RECORDER_REGEX + "=[^;]+;" - - private void checkStringSuffix(String recorder, String s1, String s2) { - String t2 = s2.replaceAll(SNIFFER_REGEX, "SNIFFER") - .replaceAll(RECORDER_INNER_MEMBER_REGEX, recorder) - .replaceAll(RECORDER_REGEX, "RECORDER") - .replaceAll(CLR_REGEX, "CLR") - assertThat(t2, equalTo(s1)) - } - - private String getInstrumentedVersion(String input, boolean testRewriting) throws Exception { - File coverageDbFile = newDbTempFile() - coverageDbFile.delete() - return getInstrumentedVersion(coverageDbFile.getAbsolutePath(), false, input, testRewriting) - } - - private File newDbTempFile() throws IOException { - File tempFile = File.createTempFile(getClass().getName() + "." + name, ".tmp", workingDir) - tempFile.delete() - return tempFile - } - - private String getInstrumentedVersion(String initString, boolean relativeIS, String input) throws Exception { - return getInstrumentedVersion(initString, relativeIS, input, true) - } - - private String getInstrumentedVersion(String initString, boolean relativeIS, String input, boolean testRewriting) throws Exception { - JavaInstrumentationConfig cfg = getInstrConfig(initString, relativeIS, testRewriting, false) - return getInstrumentedVersion(input, cfg) - } - - private String getInstrumentedVersion(final String sourceCode, final JavaInstrumentationConfig cfg) throws Exception { - final File tempFile = newDbTempFile() - final StringWriter out = new StringWriter() - final InstrumentationSource input = new StringInstrumentationSource(tempFile, sourceCode) - - performInstrumentation(cfg, input, out) - tempFile.delete() - - return out.toString() - } - - private JavaInstrumentationConfig getInstrConfig(String initString, boolean relativeIS, boolean testRewriting, boolean classInstrStrategy) { - JavaInstrumentationConfig cfg = new JavaInstrumentationConfig() - cfg.setDefaultBaseDir(workingDir) - if (initString != null) { - cfg.setInitstring(initString) - } - cfg.setRelative(relativeIS) - cfg.setProjectName(name.toString()) - cfg.setClassInstrStragegy(classInstrStrategy) - cfg.setSourceLevel(SourceLevel.JAVA_8) - cfg.setReportInitErrors(false) - cfg.setRecordTestResults(testRewriting) - cfg.setEncoding("ISO-88591") - cfg.setInstrumentLambda(LambdaInstrumentation.ALL) - return cfg - } - - - private void checkMethodMetrics(String methodSrc,int numStatements, int numBranches, int methodComplexity) throws Exception { - checkMetrics("class A{"+methodSrc+"}", 1, 1, numStatements, numBranches, methodComplexity) - } - - private void checkMetrics(String src, int numClasses, int numMethods, int numStatements, int numBranches, int totalComplexity) throws Exception { - checkMetrics(getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false), - src, numClasses, numMethods, numStatements, numBranches, totalComplexity) - } - - private Clover2Registry checkMetrics(JavaInstrumentationConfig instrConfig, String src, int numClasses, int numMethods, int numStatements, int numBranches, int totalComplexity) throws Exception { - final InstrumentationSource input = new StringInstrumentationSource(newDbTempFile(), src) - final Clover2Registry registry = performInstrumentation(instrConfig, - input, new StringWriter()) - final ProjectMetrics pm = (ProjectMetrics) registry.getProject().getMetrics() - - assertEquals("num classes",numClasses, pm.getNumClasses()) - assertEquals("num methods",numMethods, pm.getNumMethods()) - assertEquals("num statements", numStatements, pm.getNumStatements()) - assertEquals("num branches", numBranches, pm.getNumBranches()) - assertEquals("total complexity", totalComplexity, pm.getComplexity()) - - return registry - } - - /** - * convenience method that instruments but discards the instrumentation output - */ - private Clover2Registry performInstrumentation(final String sourceCode) throws Exception { - final InstrumentationSource input = new StringInstrumentationSource(newDbTempFile(), sourceCode) - return performInstrumentation(getInstrConfig(null, false, true, false), input, new StringWriter()) - } - - private Clover2Registry performInstrumentation(final JavaInstrumentationConfig cfg, final InstrumentationSource input, - final Writer out) throws Exception { - final Instrumenter instr = new Instrumenter(cfg) - instr.startInstrumentation() - instr.instrument(input, out, null) - return instr.endInstrumentation() - } -} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestBase.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestBase.groovy new file mode 100644 index 00000000..f1f71605 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestBase.groovy @@ -0,0 +1,176 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.CloverNames +import com.atlassian.clover.cfg.instr.InstrumentationLevel +import com.atlassian.clover.cfg.instr.java.JavaInstrumentationConfig +import com.atlassian.clover.cfg.instr.java.LambdaInstrumentation +import com.atlassian.clover.cfg.instr.java.SourceLevel +import com.atlassian.clover.registry.Clover2Registry +import com.atlassian.clover.registry.metrics.ProjectMetrics +import com.atlassian.clover.util.FileUtils +import com_atlassian_clover.CoverageRecorder +import com_atlassian_clover.TestNameSniffer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.rules.TestName + +import static org.hamcrest.CoreMatchers.equalTo +import static org.hamcrest.MatcherAssert.assertThat +import static org.junit.Assert.assertEquals + +class InstrumentationTestBase { + protected File workingDir + + protected String snifferField = "public static final " + TestNameSniffer.class.getName() + " SNIFFER=" + + TestNameSniffer.class.getName() + ".NULL_INSTANCE;" + + @Rule + public TestName name = new TestName() + + @Before + void setUp() + throws Exception { + workingDir = File.createTempFile(getClass().getName() + "." + name, ".tmp") + workingDir.delete() + workingDir.mkdir() + } + + @After + void tearDown() + throws Exception { + FileUtils.deltree(workingDir) + } + + protected void checkStatement(String test, String expected, int instrumentationlevel) throws Exception { + JavaInstrumentationConfig config = getInstrConfig(newDbTempFile().getAbsolutePath(), false, true, false) + config.setInstrLevel(instrumentationlevel) + checkInstrumentation([ + ["class B { private void a(int arg) {" + test + "}}", + "class B {" + snifferField + " private void a(int arg) {RECORDER.inc(0);" + expected + "}}"] + ] as String[][], config) + } + + protected void checkStatement(String test, String expected) throws Exception { + checkStatement(test, expected, InstrumentationLevel.STATEMENT.ordinal()) + } + + // array of {input, expected output} - to prevent the prefix of the string being compared, + // mark the start point for comparison with ST_POINT + protected void checkInstrumentation(String[][] testcases, boolean testRewriting) throws Exception { + checkInstrumentation("", testcases, testRewriting) + } + + protected void checkInstrumentation(String[][] testcases) throws Exception { + checkInstrumentation("", testcases, true) + } + + // check array of {input, expected output} - replacing the recorder member with recorderStr. + protected void checkInstrumentation(String recorderStr, String[][] testcases, boolean testRewriting) throws Exception { + for (String[] testcase : testcases) { + String instr = getInstrumentedVersion(testcase[0], testRewriting) + checkStringSuffix(recorderStr, CloverTokenStreamFilter.MARKER + testcase[1], instr) + } + } + + // check array of {input, expected output} - replacing the recorder member with recorderStr. + protected void checkInstrumentation(String[][] testcases, JavaInstrumentationConfig config) throws Exception { + for (String[] testcase : testcases) { + String instr = getInstrumentedVersion(testcase[0], config) + checkStringSuffix("", CloverTokenStreamFilter.MARKER + testcase[1], instr) + } + } + + private static final String SNIFFER_REGEX = CloverNames.CLOVER_PREFIX + "[_0-9]+_TEST_NAME_SNIFFER" + private static final String RECORDER_REGEX = CloverNames.CLOVER_PREFIX + "[_A-Za-z0-9]+" + private static final String CLR_REGEX = CloverNames.CLOVER_PREFIX + private static final String RECORDER_INNER_MEMBER_REGEX = "public static " + CoverageRecorder.class.getName() + " " + RECORDER_REGEX + "=[^;]+;" + + private static void checkStringSuffix(String recorder, String s1, String s2) { + String t2 = s2.replaceAll(SNIFFER_REGEX, "SNIFFER") + .replaceAll(RECORDER_INNER_MEMBER_REGEX, recorder) + .replaceAll(RECORDER_REGEX, "RECORDER") + .replaceAll(CLR_REGEX, "CLR") + assertThat(t2, equalTo(s1)) + } + + protected String getInstrumentedVersion(String input, boolean testRewriting) throws Exception { + File coverageDbFile = newDbTempFile() + coverageDbFile.delete() + return getInstrumentedVersion(coverageDbFile.getAbsolutePath(), false, input, testRewriting) + } + + protected File newDbTempFile() throws IOException { + File tempFile = File.createTempFile(getClass().getName() + "." + name, ".tmp", workingDir) + tempFile.delete() + return tempFile + } + + protected String getInstrumentedVersion(String initString, boolean relativeIS, String input) throws Exception { + return getInstrumentedVersion(initString, relativeIS, input, true) + } + + protected String getInstrumentedVersion(String initString, boolean relativeIS, String input, boolean testRewriting) throws Exception { + JavaInstrumentationConfig cfg = getInstrConfig(initString, relativeIS, testRewriting, false) + return getInstrumentedVersion(input, cfg) + } + + protected String getInstrumentedVersion(final String sourceCode, final JavaInstrumentationConfig cfg) throws Exception { + final File tempFile = newDbTempFile() + final StringWriter out = new StringWriter() + final InstrumentationSource input = new StringInstrumentationSource(tempFile, sourceCode) + + performInstrumentation(cfg, input, out) + tempFile.delete() + + return out.toString() + } + + protected JavaInstrumentationConfig getInstrConfig(String initString, boolean relativeIS, boolean testRewriting, boolean classInstrStrategy) { + JavaInstrumentationConfig cfg = new JavaInstrumentationConfig() + cfg.setDefaultBaseDir(workingDir) + if (initString != null) { + cfg.setInitstring(initString) + } + cfg.setRelative(relativeIS) + cfg.setProjectName(name.toString()) + cfg.setClassInstrStragegy(classInstrStrategy) + cfg.setSourceLevel(SourceLevel.JAVA_8) + cfg.setReportInitErrors(false) + cfg.setRecordTestResults(testRewriting) + cfg.setEncoding("ISO-88591") + cfg.setInstrumentLambda(LambdaInstrumentation.ALL) + return cfg + } + + protected Clover2Registry checkMetrics(JavaInstrumentationConfig instrConfig, String src, int numClasses, int numMethods, int numStatements, int numBranches, int totalComplexity) throws Exception { + final InstrumentationSource input = new StringInstrumentationSource(newDbTempFile(), src) + final Clover2Registry registry = performInstrumentation(instrConfig, + input, new StringWriter()) + final ProjectMetrics pm = (ProjectMetrics) registry.getProject().getMetrics() + + assertEquals("num classes",numClasses, pm.getNumClasses()) + assertEquals("num methods",numMethods, pm.getNumMethods()) + assertEquals("num statements", numStatements, pm.getNumStatements()) + assertEquals("num branches", numBranches, pm.getNumBranches()) + assertEquals("total complexity", totalComplexity, pm.getComplexity()) + + return registry + } + + /** + * convenience method that instruments but discards the instrumentation output + */ + protected Clover2Registry performInstrumentation(final String sourceCode) throws Exception { + final InstrumentationSource input = new StringInstrumentationSource(newDbTempFile(), sourceCode) + return performInstrumentation(getInstrConfig(null, false, true, false), input, new StringWriter()) + } + + protected static Clover2Registry performInstrumentation(final JavaInstrumentationConfig cfg, final InstrumentationSource input, + final Writer out) throws Exception { + final Instrumenter instr = new Instrumenter(cfg) + instr.startInstrumentation() + instr.instrument(input, out, null) + return instr.endInstrumentation() + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestMethodsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestMethodsTest.groovy new file mode 100644 index 00000000..75d279c5 --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTestMethodsTest.groovy @@ -0,0 +1,94 @@ +package com.atlassian.clover.instr.java + +import com.atlassian.clover.CloverNames +import org.junit.Test + +class InstrumentationTestMethodsTest extends InstrumentationTestBase { + + @Test + void testTestMethod() throws Exception { + checkInstrumentation([ + [ "public class MyTest { @Test public Class foo(){} }", + "public class MyTest {" + snifferField + " @Test public Class foo(){" + "RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER();"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTest { @Test public static Class foo(){} }", + "public class MyTest {" + snifferField + " @Test public static Class foo(){RECORDER.globalSliceStart(MyTest.class.getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER();"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(MyTest.class.getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private static Class RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTest { @Test public Class foo(int a, boolean b){} }", + "public class MyTest {" + snifferField + " @Test public Class foo(int a, boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(int a, boolean b){RECORDER.inc(0);} }" ], + + [ "public class MyTest { @Test public Class foo(int a[], boolean b){} }", + "public class MyTest {" + snifferField + " @Test public Class foo(int a[], boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(int a[], boolean b){RECORDER.inc(0);} }" ], + + [ "public class MyTest { @Test public Class foo(int a[], boolean b){} }", + "public class MyTest {" + snifferField + " @Test public Class foo(int a[], boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{Class "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private Class RECORDER(int a[], boolean b){RECORDER.inc(0);} }" ], + + [ "public class MyTest { @Test public T foo(int a[], boolean b){} }", + "public class MyTest {" + snifferField + " @Test public T foo(int a[], boolean b){RECORDER.globalSliceStart(getClass().getName(),0);int "+ CloverNames.namespace("p")+"=0;java.lang.Throwable "+ CloverNames.namespace("t")+"=null;try{T "+ CloverNames.namespace("r")+"=RECORDER(a,b);"+ CloverNames.namespace("p")+"=1;return "+ CloverNames.namespace("r")+";}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");return null;}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private T RECORDER(int a[], boolean b){RECORDER.inc(0);} }" ], + ] as String[][]) + } + + @Test + void testTestMethodWithNoRewrite() throws Exception { + checkInstrumentation([ + ["public class MyTest { @Test public void testFoo(){} }", + "public class MyTest {" + snifferField + " @Test public void testFoo(){try{RECORDER.globalSliceStart(getClass().getName(),0);RECORDER.inc(0);}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTest.testFoo\",SNIFFER.getTestName(),0);}} }" ], + + ["public class MyTest { @Test public static void testFoo(){} }", + "public class MyTest {" + snifferField + " @Test public static void testFoo(){try{RECORDER.globalSliceStart(MyTest.class.getName(),0);RECORDER.inc(0);}finally{RECORDER.globalSliceEnd(MyTest.class.getName(),\"MyTest.testFoo\",SNIFFER.getTestName(),0);}} }"] + ] as String[][], false) + } + + @Test + void testExpectedExceptions() throws Exception { + checkInstrumentation([ + [ "public class MyTestA { @Test public void foo(){} }", + "public class MyTestA {" + snifferField + " @Test public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestA.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestB { /** @testng.test */public void foo(){} }", + "public class MyTestB {" + snifferField + " /** @testng.test */public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestB.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestC { @Test(expectedExceptions={}) public void foo(){} }", + "public class MyTestC {" + snifferField + " @Test(expectedExceptions={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestC.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestD { /** @testng.test expectedExceptions=\"\" */public void foo(){} }", + "public class MyTestD {" + snifferField + " /** @testng.test expectedExceptions=\"\" */public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestD.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestE { @org.testng.annotations.Test(expectedExceptions={}) public void foo(){} }", + "public class MyTestE {" + snifferField + " @org.testng.annotations.Test(expectedExceptions={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestE.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestF { @Test(expected={}) public void foo(){} }", + "public class MyTestF {" + snifferField + " @Test(expected={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestF.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestG { @org.junit.Test(expected={}) public void foo(){} }", + "public class MyTestG {" + snifferField + " @org.junit.Test(expected={}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestG.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestH { @Test @ExpectedExceptions({}) public void foo(){} }", + "public class MyTestH {" + snifferField + " @Test @ExpectedExceptions({}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestH.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestI { /** @testng.test\n* @testng.expected-exceptions value=\"\" */\npublic void foo(){} }", + "public class MyTestI {" + snifferField + " /** @testng.test\n* @testng.expected-exceptions value=\"\" */\npublic void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestI.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestJ { @Test @org.testng.annotations.ExpectedExceptions({}) public void foo(){} }", + "public class MyTestJ {" + snifferField + " @Test @org.testng.annotations.ExpectedExceptions({}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=1;}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestJ.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestK { @org.testng.annotations.Test(expectedExceptions={Foo.class,Bar.class}) public void foo(){} }", + "public class MyTestK {" + snifferField + " @org.testng.annotations.Test(expectedExceptions={Foo.class,Bar.class}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestK.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestL { /** @testng.test expectedExceptions = \"Foo Bar\" */public void foo(){} }", + "public class MyTestL {" + snifferField + " /** @testng.test expectedExceptions = \"Foo Bar\" */public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestL.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestM { @Test @ExpectedExceptions({Foo.class,Bar.class}) public void foo(){} }", + "public class MyTestM {" + snifferField + " @Test @ExpectedExceptions({Foo.class,Bar.class}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestM.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestN { @org.testng.annotations.Test @org.testng.annotations.ExpectedExceptions({Foo.class,Bar.class}) public void foo(){} }", + "public class MyTestN {" + snifferField + " @org.testng.annotations.Test @org.testng.annotations.ExpectedExceptions({Foo.class,Bar.class}) public void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestN.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestO { /** @testng.test\n * @testng.expected-exceptions value = \"Foo Bar\"\n*/\npublic void foo(){} }", + "public class MyTestO {" + snifferField + " /** @testng.test\n * @testng.expected-exceptions value = \"Foo Bar\"\n*/\npublic void foo(){RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,66,97,114,44,32,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Bar||"+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestO.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER(){RECORDER.inc(0);} }" ], + + [ "public class MyTestP { @Test(expected=Foo.class) public void foo() throws Foo, Bar {} }", + "public class MyTestP {" + snifferField + " @Test(expected=Foo.class) public void foo() throws Foo, Bar {RECORDER.globalSliceStart(getClass().getName(),0);int " + CloverNames.namespace("p") + "=0;java.lang.Throwable " + CloverNames.namespace("t") + "=null;try{RECORDER();"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"=new java.lang.RuntimeException(new String(new char[] {69,120,112,101,99,116,101,100,32,111,110,101,32,111,102,32,116,104,101,32,102,111,108,108,111,119,105,110,103,32,101,120,99,101,112,116,105,111,110,115,32,116,111,32,98,101,32,116,104,114,111,119,110,32,102,114,111,109,32,116,101,115,116,32,109,101,116,104,111,100,32,102,111,111,58,32,91,70,111,111,93,}));}catch(java.lang.Throwable "+ CloverNames.namespace("t2")+"){if("+ CloverNames.namespace("t2")+" instanceof Foo){"+ CloverNames.namespace("p")+"=1;" + CloverNames.namespace("t") + "=null;}else{"+ CloverNames.namespace("p")+"=0;"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}if("+ CloverNames.namespace("p")+"==0&&"+ CloverNames.namespace("t")+"==null){"+ CloverNames.namespace("t")+"="+ CloverNames.namespace("t2")+";}RECORDER.rethrow("+ CloverNames.namespace("t2")+");}finally{RECORDER.globalSliceEnd(getClass().getName(),\"MyTestP.foo\",SNIFFER.getTestName(),0,"+ CloverNames.namespace("p")+","+ CloverNames.namespace("t")+");}}private void RECORDER() throws Foo, Bar{RECORDER.inc(0);} }" ], + ] as String[][]) + } +} diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTryStatementsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTryStatementsTest.groovy new file mode 100644 index 00000000..f1e03a1a --- /dev/null +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationTryStatementsTest.groovy @@ -0,0 +1,37 @@ +package com.atlassian.clover.instr.java + +import org.junit.Test + +class InstrumentationTryStatementsTest extends InstrumentationTestBase { + @Test + void testTryResourceStatements() throws Exception { + checkInstrumentation([ + [ "class B { private void a(int arg) { try(A a = new A()) { } catch(Exception e) { } finally{ } }}", + "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A()) { } catch(Exception e) { } finally{ } }}'] + ] as String[][]) + checkInstrumentation([ + [ "class B { private void a(int arg) { try(A a = new A();) { } catch(Exception e) { } finally{ } }}", + "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A();) { } catch(Exception e) { } finally{ } }}'] + ] as String[][]) + checkInstrumentation([ + [ "class B { private void a(int arg) { try(A a = new A(); B b = new B()) { } catch(Exception e) { } finally{ } }}", + "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A(); RECORDER$AC0 CLR$ACI1=new RECORDER$AC0(){{RECORDER.inc(3);}};B b = new B()) { } catch(Exception e) { } finally{ } }}'] + ] as String[][]) + checkInstrumentation([ + [ "class B { private void a(int arg) { try(A a = new A(); B b = new B();) { } catch(Exception e) { } finally{ } }}", + "class B {$snifferField " + 'private void a(int arg) {RECORDER.inc(0); class RECORDER$AC0 implements java.lang.AutoCloseable {public void close(){}}; RECORDER.inc(1);try(RECORDER$AC0 CLR$ACI0=new RECORDER$AC0(){{RECORDER.inc(2);}};A a = new A(); RECORDER$AC0 CLR$ACI1=new RECORDER$AC0(){{RECORDER.inc(3);}};B b = new B();) { } catch(Exception e) { } finally{ } }}'] + ] as String[][]) + } + + @Test + void testMultiCatchBlocks() throws Exception { + checkInstrumentation([ + [ "class B { private void a(int arg) { try { } catch(FooException|BarException e) { } }}", + "class B {$snifferField private void a(int arg) {RECORDER.inc(0); RECORDER.inc(1);try { } catch(FooException|BarException e) { } }}"] + ] as String[][]) + checkInstrumentation([ + [ "class B { private void a(int arg) { try { } catch(final FooException|BarException|BazException e) { } }}", + "class B {$snifferField private void a(int arg) {RECORDER.inc(0); RECORDER.inc(1);try { } catch(final FooException|BarException|BazException e) { } }}"] + ] as String[][]) + } +} From d723e10aa8b026ed20066eaa9258173c9b66500b Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Tue, 30 Jan 2024 23:13:08 +0100 Subject: [PATCH 25/29] OC-126: correct cyclomatic complexity calculation for switch statements, rewrite and explain test cases --- .../com/atlassian/clover/instr/java/java.g | 7 +- .../InstrumentationMethodMetricsTest.groovy | 211 ++++++++++++++++-- 2 files changed, 193 insertions(+), 25 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 63b29729..22af92db 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -3574,10 +3574,13 @@ colonSwitchExpression [CloverToken owningLabel, boolean isInsideExpression] retu */ lambdaSwitchExpression [CloverToken owningLabel] returns [ContextSetAndComplexity ret] { - int caseComplexity = 0, exprComplexity = 0; + // every switch must have at least one case/default branch, cyclomatic complexity is number of branches minus 1 + // every case/default increases it by one, cyclomatic complexity of only one branch is zero + int caseComplexity = -1; + int exprComplexity = 0; ContextSet saveContext = getCurrentContext(); CloverToken tmp = null; - ret = ContextSetAndComplexity.empty(); + ret = ContextSetAndComplexity.ofComplexity(caseComplexity); } : sw:SWITCH diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy index e4c86689..3fec8d98 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy @@ -10,42 +10,207 @@ import static org.junit.Assert.assertEquals class InstrumentationMethodMetricsTest extends InstrumentationTestBase { + /** + * Simple one execution path, no cycles. Complexity of 1 "coming" from the method itself. + */ @Test - void testMethodMetrics() throws Exception { - checkMethodMetrics("void A() {}",0, 0, 1) - checkMethodMetrics("void A() {a();}",1, 0, 1) - checkMethodMetrics("void A() {a = (6 < 7);}",1 ,0 ,1) - checkMethodMetrics("void A() {a();b();c();}",3, 0, 1) + void testMethodMetricsForNoCycle() throws Exception { + // an empty method, method call, variable assignment, a few statements + checkMethodMetrics( + """void A() { + }""", 0, 0, 1) + checkMethodMetrics( + """void A() { + a(); + }""", 1, 0, 1) + checkMethodMetrics( + """void A() { + a = (6 < 7); + }""", 1, 0, 1) + checkMethodMetrics( + """void A() { + a(); + b(); + c(); + }""", 3, 0, 1) + } + + /** + * The "if-else" statement might increase cyclomatic complexity and/or number of branches. + */ + @Test + void testMethodMetricsForIfElseStatements() throws Exception { + // if-else with simple method call expression, two branches (true/false) in expression + // two paths of execution (if-path, else-path), so one cycle + checkMethodMetrics( + """void A() { + if (a()) + b(); + else + c(); + }""", 3, 2, 2) - // if - checkMethodMetrics("void A() {if (a()) b(); else c();}",3, 2, 2) - checkMethodMetrics("void A() {if (a() || b()) c();}", 2, 2, 3) - checkMethodMetrics("void A() {if (1 + 2 == 4) c();}", 2, 0, 1) + // if with the boolean expression in condition, two branches (true/false) in expression + // two paths of execution inside expression - 'a()' or 'b()' - so one extra cycle + // two paths of execution (if-path, empty-else-path) - so another one cycle + checkMethodMetrics( + """void A() { + if (a() || b()) + c(); + }""", 2, 2, 3) + + // if with a constant expression, so it always evaluates to the same value, no branches then + // of the two paths of execution (if-path, empty-else-path) only one always runs, so in fact no cycle + checkMethodMetrics( + """void A() { + if (1 + 2 == 4) + c(); + }""", 2, 0, 1) + } + /** + * Similarly as above, 'for' loops might increase cyclomatic complexity and/or number of branches. + */ + @Test + void testMethodMetricsWithForLoops() throws Exception { // for - checkMethodMetrics("void A() {for (;a();) b(); }",2, 2, 2) - checkMethodMetrics("void A() {for (;a() || b();) c();}", 2, 2, 3) - checkMethodMetrics("void A() {for (;1 + 2 == 4;) c();}", 2, 0, 1) + checkMethodMetrics( + """void A() { + for (;a();) + b(); + }""", 2, 2, 2) + checkMethodMetrics( + """void A() { + for (;a() || b();) + c(); + }""", 2, 2, 3) + checkMethodMetrics( + """void A() { + for (;1 + 2 == 4;) + c(); + }""", 2, 0, 1) + } + @Test + void testMethodMetricsWithWhileLoops() throws Exception { // while - checkMethodMetrics("void A() {while (a()) b();}",2, 2, 2) - checkMethodMetrics("void A() {while (a() || b()) c();}", 2, 2, 3) - checkMethodMetrics("void A() {while (1 + 2 == 4) c();}", 2, 0, 1) + checkMethodMetrics( + """void A() { + while (a()) b(); + }""", 2, 2, 2) + checkMethodMetrics( + """void A() { + while (a() || b()) + c(); + }""", 2, 2, 3) + checkMethodMetrics( + """void A() { + while (1 + 2 == 4) + c(); + }""", 2, 0, 1) + } + + @Test + void testMethodMetricsForSwitchStatements() throws Exception { + // NOTICE: code metrics for colon-based and lambda-based switch blocks are different! // switch with colon cases - checkMethodMetrics("void A() {switch (a()) { case 1: b();}}", 3, 0, 2) - checkMethodMetrics("void A() {switch (a()) { case 1: b(); case 2: c();}}", 5, 0, 3) + // because colon cases can be grouped together, each of them represents a separate entry + // point into the case block; therefore, every case is instrumented separately and is treated as + // a statement; statements in a block are also instrumented, even if there is only one case and + // one statement + checkMethodMetrics( + """void A() { + switch (a()) { + case 1: + b(); + } + }""", 3, 0, 2) + + checkMethodMetrics( + """void A() { + switch (a()) { + case 1: + b(); + case 2: + c(); + } + }""", 5, 0, 3) + + // two cases grouped together, still the same number of statements and complexity as above + checkMethodMetrics( + """void A() { + switch (a()) { + case 1: + case 2: + b(); + c(); + } + }""", 5, 0, 3) // switch with lambda cases - checkMethodMetrics("void A() {switch (a()) { case 1 -> b();}}", 3, 0, 2) - checkMethodMetrics("void A() {switch (a()) { case 1 -> b(); case 2 -> c();}}", 5, 0, 3) + // because lambda cases cannot be grouped, each case label is associated with a separate code block + // and each of them is the only entry point to that block; therefore, the case label itself is + // NOT instrumented and does not represent a statement; expressions or code blocks inside the case + // are of course instrumented + checkMethodMetrics( + """void A() { + switch (a()) { + case 1 -> b(); + } + }""", 2, 0, 1) + + checkMethodMetrics( + """void A() { + switch (a()) { + case 1 -> b(); + case 2 -> c(); + } + }""", 3, 0, 2) + + // expressions with switch expressions inside + // notice that both forms have the same cyclomatic complexity, although number of statements can differ + // as explained above + checkMethodMetrics( + """void A() { + int i = switch(j) { + case 0 -> -1; + case 1 -> 1; + default -> throw new IllegalArgumentException(); + }; + }""", 4, 0, 3) + checkMethodMetrics( + """void A() { + int i = switch(j) { + case 0: yield -1; + case 1: yield 1; + default: throw new IllegalArgumentException(); + }; + }""", 7, 0, 3) + } + + @Test + void testMethodMetricsForTernaryExpressions() throws Exception { // ternary - checkMethodMetrics("void A() {a() ? 1 : 2;}", 1, 2, 2) - checkMethodMetrics("void A() {a() || b()? 1 : 2;}", 1, 2, 3) - checkMethodMetrics("void A() {a() ? b() ? c()? 1 : 2 : 3 : 4;}", 1, 6, 4) + checkMethodMetrics( + """void A() { + a() ? 1 : 2; + }""", 1, 2, 2) + + checkMethodMetrics( + """void A() { + a() || b() ? 1 : 2; + }""", 1, 2, 3) - // nested functions + checkMethodMetrics( + """void A() { + a() ? + b() ? + c() ? 1 : 2 + : 3 + : 4; + }""", 1, 6, 4) } @Test From cc540403bf2d5293d6a23917eec84f4ac277a5ec Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Tue, 30 Jan 2024 23:33:06 +0100 Subject: [PATCH 26/29] OC-126: test that cyclomatic complexity for hidden paths in switch statements is also calculated properly --- .../InstrumentationMethodMetricsTest.groovy | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy index 3fec8d98..a1e9cd25 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy @@ -169,8 +169,8 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { }""", 3, 0, 2) // expressions with switch expressions inside - // notice that both forms have the same cyclomatic complexity, although number of statements can differ - // as explained above + // notice that in samples below both forms have the same cyclomatic complexity, + // although number of statements can differ, as explained above checkMethodMetrics( """void A() { int i = switch(j) { @@ -188,6 +188,38 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { default: throw new IllegalArgumentException(); }; }""", 7, 0, 3) + + // colon-based switch statements with hidden branches + // the colon-based switch does not require to cover all possible values, + // the 'default' keyword is also optional, so there might be an "invisible" branch + checkMethodMetrics( + """void A(int j) { + int i; + switch(j) { + case 0: i = -1; break; // 1st path + case 1: i = 1; break; // 2nd path + default: /* no op */; // 3rd path + }; + }""", 10, 0, 3) + + checkMethodMetrics( + """void A(int j) { + int i; + switch(j) { + case 0: i = -1; break; // 1st path + case 1: i = 1; break; // 2nd path + /* "invisible" default path */ // 3rd path + }; + }""", 8, 0, 3) + + checkMethodMetrics( + """void A(int j) { + int i; + switch(j) { + default: i = -1; // 1st path + /* no "invisible" path */ + }; + }""", 4, 0, 1) } @Test From 50e33fb44d1853b745486a32ec76307bc2a3254f Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 31 Jan 2024 10:07:46 +0100 Subject: [PATCH 27/29] OC-229: take into account potential non-zero cyclomatic complexity of argument lists (as they might have switch expressions, for instance) --- .../com/atlassian/clover/instr/java/java.g | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g index 22af92db..6fb58984 100644 --- a/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g +++ b/clover-core/src/main/java/com/atlassian/clover/instr/java/java.g @@ -435,12 +435,12 @@ tokens { return flag; } - private CloverToken instrInlineAfter(CloverToken instr, CloverToken start, CloverToken end) { + private CloverToken instrInlineAfter(CloverToken instr, CloverToken start, CloverToken end, int complexity) { if (cfg.isStatementInstrEnabled()) { instr.addPostEmitter( new StatementInstrEmitter( getCurrentContext(), start.getLine(), start.getColumn(), end.getLine(), - end.getColumn() + end.getText().length())); + end.getColumn() + end.getText().length(), complexity)); instr.addPostEmitter(new DirectedFlushEmitter()); fileInfo.addStatementMarker(start, end); } @@ -448,14 +448,14 @@ tokens { } // same as above, but protected by a flag check - private CloverToken instrInlineAfter(CloverToken tok, CloverToken start, CloverToken end, FlagDeclEmitter flag) { + private CloverToken instrInlineAfter(CloverToken tok, CloverToken start, CloverToken end, FlagDeclEmitter flag, int complexity) { if (cfg.isStatementInstrEnabled()) { tok.addPostEmitter( new FlaggedInstrEmitter( flag, new StatementInstrEmitter( getCurrentContext(), start.getLine(), start.getColumn(), - end.getLine(), end.getColumn() + end.getText().length()))); + end.getLine(), end.getColumn() + end.getText().length(), complexity))); fileInfo.addStatementMarker(start, end); } return tok; @@ -1694,6 +1694,7 @@ enumConstant boolean topLevelSave = topLevelClass; CloverToken endOfBlock = null; AnnotationImpl ann = null; + int argListComplexity = 0; // ignored for enum constant } : { @@ -1701,7 +1702,7 @@ enumConstant topLevelClass = false; } (ann=annotation)* - IDENT ( LPAREN argList RPAREN )? + IDENT ( LPAREN argListComplexity=argList RPAREN )? ( endOfBlock = classBlock[null] )? @@ -1963,8 +1964,9 @@ constructorBody[MethodSignature signature, CloverToken start, CloverToken endSig explicitConstructorInvocation returns [CloverToken t] { - t = null; ContextSetAndComplexity cc = null; + int argListComplexity = 0; + t = null; } : ( @@ -1977,24 +1979,24 @@ explicitConstructorInvocation returns [CloverToken t] generateAmbigWarnings=false; }: - pos1:THIS! LPAREN argList RPAREN! t1:SEMI! + pos1:THIS! LPAREN argListComplexity=argList RPAREN! t1:SEMI! { - t=instrInlineAfter(ct(t1), ct(pos1), ct(t1)); + t=instrInlineAfter(ct(t1), ct(pos1), ct(t1), argListComplexity); } | - pos2:SUPER! lp2:LPAREN^ argList RPAREN! t2:SEMI! + pos2:SUPER! lp2:LPAREN^ argListComplexity=argList RPAREN! t2:SEMI! { - t=instrInlineAfter(ct(t2), ct(pos2), ct(t2)); + t=instrInlineAfter(ct(t2), ct(pos2), ct(t2), argListComplexity); } | // (new Outer()).super() (create enclosing instance) cc=primaryExpressionPart (DOT! THIS)? // HACK see CCD-264 - explicit ctor invocation can have form ClassName.this.super(..) - DOT! pos3:SUPER! lp3:LPAREN^ argList RPAREN! t3:SEMI! + DOT! pos3:SUPER! lp3:LPAREN^ argListComplexity=argList RPAREN! t3:SEMI! { - t=instrInlineAfter(ct(t3), ct(pos3), ct(t3)); + t=instrInlineAfter(ct(t3), ct(pos3), ct(t3), argListComplexity); } ) ; @@ -2666,10 +2668,11 @@ colonCase[FlagDeclEmitter flag] returns [int complexity] ) t:COLON! { + // 0 cyclomatic complexity here as we pass it up to the switch statement if (flag != null) { - instrInlineAfter(ct(t), ct(pos), ct(t), flag); + instrInlineAfter(ct(t), ct(pos), ct(t), flag, 0); } else { - instrInlineAfter(ct(t), ct(pos), ct(t)); + instrInlineAfter(ct(t), ct(pos), ct(t), 0); } fileInfo.setSuppressFallthroughWarnings(true); } @@ -3669,7 +3672,9 @@ patternMatch */ supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd, CloverToken startMethodReference] returns [int complexity] { - int tmpComplexity = 0; + int argListComplexity = 0; + int exprComplexity = 0; + int newComplexity = 0; complexity = 0; } : @@ -3687,9 +3692,9 @@ supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd | CLASS | - tmpComplexity=newExpression + newComplexity=newExpression { - complexity += tmpComplexity; + complexity += newComplexity; } | SUPER // ClassName.super.field @@ -3717,9 +3722,9 @@ supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd ) | // an array indexing operation - LBRACK tmpComplexity=expression RBRACK! + LBRACK exprComplexity=expression RBRACK! { - complexity += tmpComplexity; + complexity += exprComplexity; } | // method invocation @@ -3730,7 +3735,10 @@ supplementaryExpressionPart[CloverToken classCastStart, CloverToken classCastEnd // It also allows ctor invocation like super(3) which is now // handled by the explicit constructor rule, but it would // be hard to syntactically prevent ctor calls here - LPAREN argList RPAREN! + LPAREN argListComplexity=argList RPAREN! + { + complexity += argListComplexity; + } )* ; @@ -3815,12 +3823,18 @@ newExpression returns [int complexity] { CloverToken endOfBlock = null; String typeParam = null; - int declComplexity = 0, initComplexity = 0; + int argListComplexity = 0; + int declComplexity = 0; + int initComplexity = 0; complexity = 0; } : NEW (typeParam=typeParameters)? type - ( LPAREN! argList RPAREN! (endOfBlock=classBlock[null])? + ( + LPAREN! argListComplexity=argList RPAREN! (endOfBlock=classBlock[null])? + { + complexity = argListComplexity; + } //java 1.1 // Note: This will allow bad constructs like From 6db55ef1afc2e39aa0f493a9e8ab7a3a875eda64 Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 31 Jan 2024 15:01:50 +0100 Subject: [PATCH 28/29] OC-126: test for propagation of cyclomatic complexity through method arguments calls --- .../InstrumentationMethodMetricsTest.groovy | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy index a1e9cd25..8f39d94c 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy @@ -111,7 +111,7 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { } @Test - void testMethodMetricsForSwitchStatements() throws Exception { + void testMethodMetricsForColonBasedSwitchStatements() throws Exception { // NOTICE: code metrics for colon-based and lambda-based switch blocks are different! // switch with colon cases @@ -147,7 +147,10 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { c(); } }""", 5, 0, 3) + } + @Test + void testMethodMetricsForLambdaBasedSwitchStatements() throws Exception { // switch with lambda cases // because lambda cases cannot be grouped, each case label is associated with a separate code block // and each of them is the only entry point to that block; therefore, the case label itself is @@ -167,7 +170,10 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { case 2 -> c(); } }""", 3, 0, 2) + } + @Test + void testSwitchStatementsInBothFormsHaveEquivalentCyclomaticComplexity() throws Exception { // expressions with switch expressions inside // notice that in samples below both forms have the same cyclomatic complexity, // although number of statements can differ, as explained above @@ -188,7 +194,10 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { default: throw new IllegalArgumentException(); }; }""", 7, 0, 3) + } + @Test + void testCyclomaticComplexityForColonBasedSwitchStatementsWithHiddenPaths() throws Exception { // colon-based switch statements with hidden branches // the colon-based switch does not require to cover all possible values, // the 'default' keyword is also optional, so there might be an "invisible" branch @@ -222,6 +231,26 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { }""", 4, 0, 1) } + @Test + void testCyclomaticComplexityIsPropagatedForArgumentLists() throws Exception { + checkMethodMetrics( + """void A(int j) { // 1 from method + foo( // statement + switch(j) { // 1 cycle + case 0 -> 10; // statement + default -> 99; // statement + }, + switch(j) { // 2 cycles + case 0 -> 10; // statement + case 1 -> 20; // statement + default -> 99; // statement + }, + switch(j) { // 0 cycles + default -> 99; // statement + }); + }""", 7, 0, 4) + } + @Test void testMethodMetricsForTernaryExpressions() throws Exception { // ternary From 6b95bef56c792b9ae3c20af0f9e85dd8380fe2cc Mon Sep 17 00:00:00 2001 From: Marek Parfianowicz Date: Wed, 31 Jan 2024 17:36:03 +0100 Subject: [PATCH 29/29] OC-126: test for propagation of cyclomatic complexity for argument list in constructor calls --- .../java/InstrumentationMethodMetricsTest.groovy | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy index 8f39d94c..173d678f 100644 --- a/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy +++ b/clover-core/src/test/groovy/com/atlassian/clover/instr/java/InstrumentationMethodMetricsTest.groovy @@ -233,6 +233,7 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { @Test void testCyclomaticComplexityIsPropagatedForArgumentLists() throws Exception { + // arg list in method calls checkMethodMetrics( """void A(int j) { // 1 from method foo( // statement @@ -249,6 +250,17 @@ class InstrumentationMethodMetricsTest extends InstrumentationTestBase { default -> 99; // statement }); }""", 7, 0, 4) + + // arg list in constructor calls + checkMethodMetrics( + """void A(int j) { // 1 from method + super( // statement + switch(j) { // 2 cycles + case 0 -> 10; // statement + case 1 -> 20; // statement + default -> 99; // statement + }); + }""", 4, 0, 3) } @Test