Repository: jacoco/jacoco (2025-08-24T20:38:32Z)

Issue:
KotlinWhenStringFilter assigns incorrect branch indices for replacement branches
Context:
I am a developer for [Bazel](https://github.com/bazelbuild/bazel) and am trying to integrate the changes made in f823d434 with our own branch analyzer. In trying to test my efforts, I believe I have run into a bug with Jacoco's `KotlinWhenStringFilter`.

The branch mapping for Kotlin "when" statements that switch on Strings does not appear to be done correctly. It does not seem possible to have any case except the "default" case be marked as covered.

Reproduced with Jacoco built at a4cee188c95d56f19725144720c1f79004d80527 (parent to 97e7f620 because my Maven version is older).

```
$ cd jacoco_git/org.jacoco.build
$ git checkout 97e7f620~1  # to use older version of Maven
$ ../mvnw clean verify -DskipTests
# extract out jacococli.jar and jacocoagent.jar
```

Write the following to `when.kt`
```
fun stringWhen(s: String) {
  when(s) {
    "a" -> { println("case a") }
    "b" -> { println("case b") }
    "c" -> { println("case c") }
  }
}

fun main() {
  stringWhen("a")
}
```

```
# Kotlin 1.3 - some later versions of Kotlin produce different bytecode that the filter doesn't match
$ kotlinc when.kt -d when.jar -include-runtime

$ java -javaagent:[built/jacoco/path]/jacocoagent.jar=includes=WhenKt,append=false -jar when.jar
case a

$ java -jar [build/jacoco/path]/jacococli.jar report jacoco.exec --csv report.dat --classfiles when.jar --sourcefiles when.kt

$ grep WhenKt report.dat
JaCoCo Coverage Report,default,WhenKt,17,15,4,0,2,5,3,2,0,2
```

That CSV report shows 0 taken branches and 4 missed.
It should be 1 taken branch and 3 missed.

Calling `stringWhen("some_other_string")` instead correctly results 1 taken branch.

```
$ tail -n 4 when.kt

fun main() {
  stringWhen("some_other_string")
}

$ kotlinc when.kt -d when.jar -include-runtime
$ java -javaagent:[built/jacoco/path]/jacocoagent.jar=includes=WhenKt,append=false -jar when.jar
$ java -jar [build/jacoco/path]/jacococli.jar report jacoco.exec --csv report.dat --classfiles when.jar --sourcefiles when.kt
JaCoCo Coverage Report,default,WhenKt,23,9,3,1,3,4,3,2,0,2
```

I _suspect_ the problem lies on this line: https://github.com/jacoco/jacoco/blob/8f24208e22d87e34345c49f19b23d813f45a1815/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilter.java#L88

```
} else if (cursor.getOpcode() == Opcodes.GOTO) {
    // jump to case body
    replacements.add(((JumpInsnNode) cursor).label, cursor,
    0);
```

This sets the replacement branch to be the "0 index" branch of an unconditional jump instruction. This branch will never be taken and has no probe data associated with it: the target of any jump is always at branch index "1".

I can't say for certain though; I'm not still not confident in my mental model to say I have a crystal clear picture of exactly how this all works despite my efforts.
Changing the "0" to a "1" does fix this case at least.


Discussion:
@c-mita thank you for reporting this issue! ❤️ 

> the target of any jump is always at branch index "1"

indeed

https://github.com/jacoco/jacoco/blob/8f24208e22d87e34345c49f19b23d813f45a1815/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Replacements.java#L58-L61

is not consistent with

https://github.com/jacoco/jacoco/blob/8f24208e22d87e34345c49f19b23d813f45a1815/org.jacoco.core/src/org/jacoco/core/internal/analysis/MethodAnalyzer.java#L105-L109

whereas should

https://github.com/jacoco/jacoco/blob/8f24208e22d87e34345c49f19b23d813f45a1815/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Replacements.java#L41-L43

and this affects not only 

https://github.com/jacoco/jacoco/blob/8f24208e22d87e34345c49f19b23d813f45a1815/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilter.java#L86-L89

but also

https://github.com/jacoco/jacoco/blob/8f24208e22d87e34345c49f19b23d813f45a1815/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilter.java#L109-L110

> I'm not still not confident in my mental model to say I have a crystal clear picture of exactly how this all works despite my efforts.

even we overlooked this in #1813 😉 



Patch:
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilter.java
index 2296948335..90b3b88012 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilter.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilter.java
@@ -86,7 +86,7 @@ public void match(final AbstractInsnNode start,
 					} else if (cursor.getOpcode() == Opcodes.GOTO) {
 						// jump to case body
 						replacements.add(((JumpInsnNode) cursor).label, cursor,
-								0);
+								1);
 						if (jump.label == defaultLabel) {
 							// end of comparisons for same hashCode
 							replacements.add(defaultLabel, jump, 1);
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Replacements.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Replacements.java
index 0a7a03c386..1e3cb00707 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Replacements.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Replacements.java
@@ -56,7 +56,7 @@ public final class Replacements {
 	 * </li>
 	 *
 	 * <li>for {@link org.objectweb.asm.tree.JumpInsnNode} with
-	 * {@link org.objectweb.asm.Opcodes#GOTO} there is only branch 0 that
+	 * {@link org.objectweb.asm.Opcodes#GOTO} there is only branch 1 that
 	 * corresponds to continuation of execution at
 	 * {@link org.objectweb.asm.tree.JumpInsnNode#label}</li>
 	 *
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilter.java
index 17d62a802c..3d8b041c7b 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilter.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilter.java
@@ -107,7 +107,7 @@ public void match(final AbstractInsnNode start,
 						// end of comparisons for same hashCode
 						// jump to default
 						nextIs(Opcodes.GOTO);
-						replacements.add(defaultLabel, cursor, 0);
+						replacements.add(defaultLabel, cursor, 1);
 						break;
 					} else if (cursor.getNext() == defaultLabel) {
 						replacements.add(defaultLabel, cursor, 0);
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 03b5876a82..d3855a7255 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -40,10 +40,12 @@ <h3>Fixed bugs</h3>
 <ul>
   <li>Fixed handling of implicit <code>else</code> clause of <code>when</code> with
       <code>String</code> subject in Kotlin
-      (GitHub <a href="https://github.com/jacoco/jacoco/issues/1813">#1813</a>).</li>
+      (GitHub <a href="https://github.com/jacoco/jacoco/issues/1813">#1813</a>,
+      <a href="https://github.com/jacoco/jacoco/issues/1940">#1940</a>).</li>
   <li>Fixed handling of implicit <code>default</code> clause of <code>switch</code>
       by <code>String</code> in Java when compiled by ECJ
-      (GitHub <a href="https://github.com/jacoco/jacoco/issues/1813">#1813</a>).</li>
+      (GitHub <a href="https://github.com/jacoco/jacoco/issues/1813">#1813</a>,
+      <a href="https://github.com/jacoco/jacoco/issues/1940">#1940</a>).</li>
   <li>Fixed handling of exceptions in chains of safe call operators in Kotlin
       (GitHub <a href="https://github.com/jacoco/jacoco/issues/1819">#1819</a>).</li>
 </ul>


Test patch:
diff --git a/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/targets/StringSwitchTarget.java b/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/targets/StringSwitchTarget.java
index 575f370784..127e20b1ac 100644
--- a/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/targets/StringSwitchTarget.java
+++ b/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/targets/StringSwitchTarget.java
@@ -14,6 +14,8 @@
 
 import static org.jacoco.core.test.validation.targets.Stubs.nop;
 
+import org.jacoco.core.test.validation.targets.Stubs;
+
 /**
  * This test target is a switch statement with a String.
  */
@@ -36,6 +38,20 @@ private static void covered(Object s) {
 		}
 	}
 
+	private static void executedWithSameHashCodeAsFirstCase() {
+		switch (Stubs.string("\0a")) { // assertFullyCovered(2, 1)
+		case "a":
+			nop("case a"); // assertNotCovered()
+			break;
+		case "b":
+			nop("case b"); // assertNotCovered()
+			break;
+		default:
+			nop("default"); // assertFullyCovered()
+			break;
+		}
+	}
+
 	private static void notCovered(Object s) {
 		switch (String.valueOf(s)) { // assertNotCovered(4, 0)
 		case "a":
@@ -133,6 +149,8 @@ public static void main(String[] args) {
 		covered("b");
 		covered("\0a");
 
+		executedWithSameHashCodeAsFirstCase();
+
 		implicitDefaultNotExecuted("a");
 		implicitDefaultNotExecuted("b");
 		implicitDefaultNotExecuted("c");
diff --git a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinWhenStringTarget.kt b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinWhenStringTarget.kt
index 2ff0c227c3..9aec8a767a 100644
--- a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinWhenStringTarget.kt
+++ b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinWhenStringTarget.kt
@@ -13,6 +13,7 @@
 package org.jacoco.core.test.validation.kotlin.targets
 
 import org.jacoco.core.test.validation.targets.Stubs.nop
+import org.jacoco.core.test.validation.targets.Stubs.string
 
 /**
  * Test target with `when` expressions and statements with subject of type `String`.
@@ -57,6 +58,15 @@ object KotlinWhenStringTarget {
         } // assertEmpty()
     } // assertFullyCovered()
 
+    private fun executedWithSameHashCodeAsFirstCase() {
+        when (string("\u0000a")) { // assertFullyCovered(3, 1)
+            "a" -> nop("case a") // assertNotCovered()
+            "b" -> nop("case b") // assertNotCovered()
+            "c" -> nop("case c") // assertNotCovered()
+            else -> nop("else") // assertFullyCovered()
+        } // assertEmpty()
+    } // assertFullyCovered()
+
     /**
      * Unlike [whenString]
      * in this example first case is the only case with biggest hashCode value.
@@ -96,6 +106,8 @@ object KotlinWhenStringTarget {
         implicitElseNotExecuted("b")
         implicitElseNotExecuted("c")
 
+        executedWithSameHashCodeAsFirstCase()
+
         whenStringBiggestHashCodeFirst("")
         whenStringBiggestHashCodeFirst("a")
         whenStringBiggestHashCodeFirst("b")
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FilterTestBase.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FilterTestBase.java
index 3a2d7ce4ea..bc3cc215b2 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FilterTestBase.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FilterTestBase.java
@@ -111,41 +111,28 @@ final void assertReplacedBranches(final MethodNode methodNode,
 	private void assertReplacements(final MethodNode methodNode,
 			final AbstractInsnNode source,
 			final List<Replacement> expectedReplacements) {
-
-		Collections.sort(expectedReplacements, new Comparator<Replacement>() {
-			public int compare(final Replacement r1, final Replacement r2) {
-				if (r1.newBranch == r2.newBranch) {
-					return r1.branch - r2.branch;
-				}
-				return r1.newBranch - r2.newBranch;
-			}
-		});
-
-		final StringBuilder expectedStringBuilder = new StringBuilder();
+		final ArrayList<String> expectedStrings = new ArrayList<String>();
 		for (final Replacement replacement : expectedReplacements) {
-			expectedStringBuilder.append(replacement.newBranch)
-					.append(" if branch ").append(replacement.branch)
-					.append(" of instruction ").append(methodNode.instructions
-							.indexOf(replacement.instruction))
-					.append("\n");
+			expectedStrings.add("\n" + replacement.newBranch + " if branch "
+					+ replacement.branch + " of instruction "
+					+ methodNode.instructions.indexOf(replacement.instruction));
 		}
+		Collections.sort(expectedStrings);
 
-		final StringBuilder actualStringBuilder = new StringBuilder();
+		final ArrayList<String> actualStrings = new ArrayList<String>();
 		int newBranch = 0;
 		for (final Collection<Replacements.InstructionBranch> pairs : actualReplacements
 				.get(source)) {
 			for (Replacements.InstructionBranch pair : pairs) {
-				actualStringBuilder.append(newBranch).append(" if branch ")
-						.append(pair.branch).append(" of instruction ")
-						.append(methodNode.instructions
-								.indexOf(pair.instruction))
-						.append("\n");
+				actualStrings.add("\n" + newBranch + " if branch " + pair.branch
+						+ " of instruction "
+						+ methodNode.instructions.indexOf(pair.instruction));
 			}
 			newBranch++;
 		}
+		Collections.sort(actualStrings);
 
-		assertEquals(expectedStringBuilder.toString(),
-				actualStringBuilder.toString());
+		assertEquals(expectedStrings.toString(), actualStrings.toString());
 	}
 
 	static class Replacement {
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilterTest.java
index b1d0ef027a..c9b5b707b2 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilterTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinWhenStringFilterTest.java
@@ -66,7 +66,7 @@ public void should_filter() {
 				"(Ljava/lang/Object;)Z", false);
 		m.visitJumpInsn(Opcodes.IFEQ, sameHash);
 		m.visitJumpInsn(Opcodes.GOTO, case1);
-		replacements.add(new Replacement(1, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(1, m.instructions.getLast(), 1));
 
 		// case "\u0000a"
 		m.visitLabel(sameHash);
@@ -77,7 +77,7 @@ public void should_filter() {
 		m.visitJumpInsn(Opcodes.IFEQ, defaultCase);
 		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, case2);
-		replacements.add(new Replacement(2, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(2, m.instructions.getLast(), 1));
 
 		// case "b"
 		m.visitLabel(h2);
@@ -89,7 +89,7 @@ public void should_filter() {
 		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, case3);
 		final AbstractInsnNode expectedToInclusive = m.instructions.getLast();
-		replacements.add(new Replacement(3, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(3, m.instructions.getLast(), 1));
 
 		m.visitLabel(case1);
 		m.visitInsn(Opcodes.RETURN);
@@ -145,7 +145,7 @@ public void should_filter_when_biggest_hashCode_first() {
 				"(Ljava/lang/Object;)Z", false);
 		m.visitJumpInsn(Opcodes.IFEQ, sameHash);
 		m.visitJumpInsn(Opcodes.GOTO, case2);
-		replacements.add(new Replacement(1, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(1, m.instructions.getLast(), 1));
 
 		m.visitLabel(sameHash);
 		m.visitVarInsn(Opcodes.ALOAD, 2);
@@ -155,7 +155,7 @@ public void should_filter_when_biggest_hashCode_first() {
 		m.visitJumpInsn(Opcodes.IFEQ, defaultCase);
 		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, case3);
-		replacements.add(new Replacement(2, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(2, m.instructions.getLast(), 1));
 
 		m.visitLabel(h2);
 		m.visitVarInsn(Opcodes.ALOAD, 2);
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilterTest.java
index 53e633b0c6..6ea3981d7e 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilterTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchFilterTest.java
@@ -76,7 +76,7 @@ public void should_filter() {
 
 		// goto default case
 		m.visitJumpInsn(Opcodes.GOTO, caseDefault);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(h2);
 
@@ -91,7 +91,7 @@ public void should_filter() {
 		// goto default case
 		m.visitJumpInsn(Opcodes.GOTO, caseDefault);
 		range.toInclusive = m.instructions.getLast();
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(case1);
 		m.visitInsn(Opcodes.RETURN);
@@ -212,7 +212,7 @@ public void should_filter_Kotlin_1_5() {
 		replacements.add(new Replacement(2, m.instructions.getLast(), 1));
 
 		m.visitJumpInsn(Opcodes.GOTO, defaultCase);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(h2);
 		m.visitVarInsn(Opcodes.ALOAD, 1);
@@ -230,7 +230,7 @@ public void should_filter_Kotlin_1_5() {
 		replacements.add(new Replacement(4, m.instructions.getLast(), 1));
 
 		m.visitJumpInsn(Opcodes.GOTO, defaultCase);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(h3);
 		m.visitVarInsn(Opcodes.ALOAD, 1);
@@ -249,7 +249,7 @@ public void should_filter_Kotlin_1_5() {
 
 		m.visitJumpInsn(Opcodes.GOTO, defaultCase);
 		range.toInclusive = m.instructions.getLast();
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(case1);
 		m.visitInsn(Opcodes.RETURN);
@@ -319,7 +319,7 @@ public void should_filter_Kotlin_nullable_else() {
 		m.visitJumpInsn(Opcodes.IFNE, caseA);
 		replacements.add(new Replacement(1, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, caseElse);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(hashB);
 		m.visitVarInsn(Opcodes.ALOAD, 2);
@@ -329,7 +329,7 @@ public void should_filter_Kotlin_nullable_else() {
 		m.visitJumpInsn(Opcodes.IFNE, caseB);
 		replacements.add(new Replacement(2, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, caseElse);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(hashC);
 		m.visitVarInsn(Opcodes.ALOAD, 2);
@@ -340,7 +340,7 @@ public void should_filter_Kotlin_nullable_else() {
 		replacements.add(new Replacement(3, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, caseElse);
 		range.toInclusive = m.instructions.getLast();
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(caseA);
 		m.visitLdcInsn("case a");
@@ -415,7 +415,7 @@ public void should_filter_Kotlin_nullable_case() {
 		m.visitJumpInsn(Opcodes.IFNE, caseA);
 		replacements.add(new Replacement(1, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, caseElse);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(hashB);
 		m.visitVarInsn(Opcodes.ALOAD, 2);
@@ -425,7 +425,7 @@ public void should_filter_Kotlin_nullable_case() {
 		m.visitJumpInsn(Opcodes.IFNE, caseB);
 		replacements.add(new Replacement(2, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, caseElse);
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(hashC);
 		m.visitVarInsn(Opcodes.ALOAD, 2);
@@ -436,7 +436,7 @@ public void should_filter_Kotlin_nullable_case() {
 		replacements.add(new Replacement(3, m.instructions.getLast(), 1));
 		m.visitJumpInsn(Opcodes.GOTO, caseElse);
 		range.toInclusive = m.instructions.getLast();
-		replacements.add(new Replacement(0, m.instructions.getLast(), 0));
+		replacements.add(new Replacement(0, m.instructions.getLast(), 1));
 
 		m.visitLabel(caseA);
 		m.visitLdcInsn("case a");
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java
index 3912639b84..1768557dc9 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java
@@ -119,6 +119,13 @@ public static Enum enumA() {
 		return Enum.A;
 	}
 
+	/**
+	 * @return given argument
+	 */
+	public static String string(final String value) {
+		return value;
+	}
+
 	/**
 	 * Always throws a {@link RuntimeException}.
 	 *