Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 1, 2025

Fixes StackOverflowError when using every/verify with methods returning ArrayList or other concrete collection types on Android.

Root Cause

The MethodDescriptor constructor was using Kotlin collection operations (dropLastWhile, filter) that internally call isEmpty() on collections. When the collection class (like ArrayList) is being mocked, this triggers the mock dispatcher, which calls Advice.getOrigin(), which creates a new MethodDescriptor, causing infinite recursion.

Solution

Rewrote MethodDescriptor.parseParamTypes() to use a two-pass approach that avoids collection operations entirely:

  1. First pass: count the number of parameters using simple string traversal
  2. Second pass: parse parameters into a pre-allocated array

This approach avoids:

  • dropLastWhile { it.isEmpty() } - calls isEmpty() on collections
  • filter { it != "" } - also calls collection methods
  • String.trim() - replaced with custom trimSubstring() to avoid any potential collection operations

Helper functions hasNonWhitespaceContent() and trimSubstring() ensure consistent logic between both passes and avoid triggering mocked methods.

Testing

  • Added ArrayListReturnTypeTest.kt for Android instrumented testing
  • All existing JVM tests pass without regression

Fixes #1450

Original prompt

This section details on the original issue you should resolve

<issue_title>java.lang.StackOverflowError when use every/verify{/a method return ArrayList/} on a mock class after 1.13.8</issue_title>
<issue_description>I update the mockk from 1.12 to 1.13.x/1.14.x to support Android 14, but it will throw StackOverflowError when run some cases, after my testing, find the way to reproduce it.

class ListTest {
    fun getOneItem(v: String): String{
        return v;
    }
    fun getListItem(v: String) : ArrayList<String>{
        return arrayListOf(v)
    }
}

Test code:

    @Test
    fun testList(){
        val mockTest = mockk<ListTest>()
        every { mockTest.getListItem(any()) } returns ArrayList() // arrayListOf()
        // or verify { mockTest.getListItem(any()) }
    }

It always throw this StackOverflowError

Fatal exception when running tests
java.lang.StackOverflowError: stack size 1039KB
	at kotlin.text.MatcherMatchResult$groups$1.<init>(Unknown Source:0)
	at kotlin.text.MatcherMatchResult.<init>(Regex.kt:357)
	at kotlin.text.RegexKt.matchEntire(Regex.kt:347)
	at kotlin.text.RegexKt.access$matchEntire(Regex.kt:1)
	at kotlin.text.Regex.matchEntire(Regex.kt:143)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:8)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)
	at java.util.AbstractCollection.isEmpty(Unknown Source:14)
	at io.mockk.proxy.android.MethodDescriptor.<init>(MethodDescriptor.kt:90)
	at io.mockk.proxy.android.advice.Advice.getOrigin(Advice.kt:29)
	at java.lang.reflect.Method.invoke(Native Method)
	at io.mockk.proxy.android.AndroidMockKDispatcher.getOrigin(AndroidMockKDispatcher.java:117)

Workaround:

class ListTest {
    fun getOneItem(v: String): String{
        return v;
    }
    fun getListItem(v: String) : List<String>{ // change to List
        return arrayListOf(v)
    }
}

Test code:

    @Test
    fun testList(){
        val mockTest = mockk<ListTest>()
        every { mockTest.getListItem(any()) } returns emptyList() // dont use arraylist
    }

But actually, we need mock the class from SDK, we cannot change the return type.
Would anyone have any idea to fix it?</issue_description>

Comments on the Issue (you are @copilot in this section)

Custom agent used: tech-lead
Tech lead expert for MockK - Kotlin's premier mocking library. Specializes in complex bug fixing, bytecode manipulation, DSL design, and multi-platform architecture. Proactively handles recursion issues, signature matching problems, and JVM agent complexities.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

- Refactored MethodDescriptor.parseParamTypes() to avoid using Kotlin
  collection operations (dropLastWhile, filter) that call isEmpty() on
  collections, which could trigger the mock dispatcher and cause infinite
  recursion when mocking classes that extend ArrayList or other collections.
- Used a two-pass approach: first count parameters, then parse into a
  pre-allocated array, avoiding mutable list operations entirely.
- Added ArrayListReturnTypeTest for Android instrumented testing.

Co-authored-by: oleksiyp <[email protected]>
@oleksiyp
Copy link
Collaborator

oleksiyp commented Dec 2, 2025

@copilot continue

- Ensure first and second pass use identical logic via shared
  hasNonWhitespaceContent() helper function
- Add custom trimSubstring() to avoid String.trim() which may
  trigger collection operations
- Improve code consistency and reduce risk of array bounds issues

Co-authored-by: oleksiyp <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

@copilot continue

Done. I've addressed the code review feedback and completed the remaining checks. The changes in commit 73736a6 improve the parseParamTypes() implementation by:

  1. Using identical logic in both passes via shared hasNonWhitespaceContent() helper function
  2. Adding a custom trimSubstring() to avoid String.trim() which may trigger collection operations
  3. All tests pass and security checks are complete

Copilot AI changed the title [WIP] Fix stack overflow error in mockk usage Fix StackOverflowError when mocking methods returning ArrayList Dec 2, 2025
@oleksiyp oleksiyp marked this pull request as ready for review December 2, 2025 11:56
@oleksiyp
Copy link
Collaborator

oleksiyp commented Dec 2, 2025

@Raibaz this looks safe change

@Raibaz Raibaz merged commit 9372ca6 into master Dec 2, 2025
22 of 23 checks passed
@oleksiyp oleksiyp deleted the copilot/fix-stackoverflow-error-mockk branch December 2, 2025 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

java.lang.StackOverflowError when use every/verify{/*a method return ArrayList*/} on a mock class after 1.13.8

3 participants