Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package io.sentry.android.replay.viewhierarchy

import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.NodeCoordinator
import io.sentry.util.CompileOnlyCompat.CompileOnlyCall
import java.lang.reflect.Method

/**
Expand Down Expand Up @@ -41,17 +42,13 @@ internal object SentryLayoutNodeHelper {
fun getChildren(node: LayoutNode): List<LayoutNode> {
when (useFallback) {
false -> return node.children
true -> {
return getFallback().getChildren!!.invoke(node) as List<LayoutNode>
}
null -> {
try {
return node.children.also { useFallback = false }
} catch (_: NoSuchMethodError) {
useFallback = true
return getFallback().getChildren!!.invoke(node) as List<LayoutNode>
}
}
true -> return getFallback().getChildren!!.invoke(node) as List<LayoutNode>
null ->
return CompileOnlyCall { node.children.also { useFallback = false } }
.ifAbsent {
useFallback = true
getFallback().getChildren!!.invoke(node) as List<LayoutNode>
}
}
}

Expand All @@ -63,16 +60,16 @@ internal object SentryLayoutNodeHelper {
val coordinator = fb.getOuterCoordinator!!.invoke(node) as NodeCoordinator
return coordinator.isTransparent()
}
null -> {
try {
return node.outerCoordinator.isTransparent().also { useFallback = false }
} catch (_: NoSuchMethodError) {
useFallback = true
val fb = getFallback()
val coordinator = fb.getOuterCoordinator!!.invoke(node) as NodeCoordinator
return coordinator.isTransparent()
}
}
null ->
return CompileOnlyCall {
node.outerCoordinator.isTransparent().also { useFallback = false }
}
.ifAbsent {
useFallback = true
val fb = getFallback()
val coordinator = fb.getOuterCoordinator!!.invoke(node) as NodeCoordinator
coordinator.isTransparent()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.sqlite.SQLiteDriver
import io.sentry.ScopesAdapter
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
import io.sentry.util.CompileOnlyCompat.CompileOnlyCall

/**
* Wraps a [SQLiteDriver] and automatically adds spans for each SQL statement it executes.
Expand Down Expand Up @@ -36,13 +37,7 @@ internal class SentrySQLiteDriver private constructor(private val delegate: SQLi
}

override val hasConnectionPool: Boolean
get() =
try {
delegate.hasConnectionPool
} catch (_: LinkageError) {
// Delegates on androidx.sqlite < 2.6.0 won't have a hasConnectionPool property.
false
}
get() = CompileOnlyCall { delegate.hasConnectionPool }.ifAbsent(false)

@Suppress("TooGenericExceptionCaught")
override fun open(fileName: String): SQLiteConnection {
Expand Down
13 changes: 13 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7628,6 +7628,19 @@ public abstract interface class io/sentry/util/CollectionUtils$Predicate {
public abstract fun test (Ljava/lang/Object;)Z
}

public final class io/sentry/util/CompileOnlyCompat {
}

public abstract interface class io/sentry/util/CompileOnlyCompat$CompileOnlyCall {
public abstract fun call ()Ljava/lang/Object;
public fun ifAbsent (Lio/sentry/util/CompileOnlyCompat$Fallback;)Ljava/lang/Object;
public fun ifAbsent (Ljava/lang/Object;)Ljava/lang/Object;
}

public abstract interface class io/sentry/util/CompileOnlyCompat$Fallback {
public abstract fun call (Ljava/lang/LinkageError;)Ljava/lang/Object;
}

public final class io/sentry/util/DebugMetaPropertiesApplier {
public static field DEBUG_META_PROPERTIES_FILENAME Ljava/lang/String;
public fun <init> ()V
Expand Down
72 changes: 72 additions & 0 deletions sentry/src/main/java/io/sentry/util/CompileOnlyCompat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.sentry.util;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* Helpers for safely calling methods from {@code compileOnly} dependencies that may not exist at
* runtime.
*
* <p>When a dependency is {@code compileOnly}, the app supplies the actual version. That version
* may predate the API we compiled against, which means it may not have methods we call from our
* source code if the API has added methods over time. Calling a missing method throws a {@link
* LinkageError} (e.g., {@link NoSuchMethodError}, {@link AbstractMethodError}, etc.). This class
* lets us centralize the try/catch-with-fallback pattern.
*
* <p>This is not for {@code implementation} dependencies with transitive version conflicts on the
* classpath; use local {@code try/catch} handling for those cases instead.
*/
@ApiStatus.Internal
public final class CompileOnlyCompat {

/**
* Functional interface for a call that returns a non-null value and may throw a {@link
* LinkageError}.
*
* <p>Use it when calling from Kotlin:
*
* <pre>{@code
* CompileOnlyCall { delegate.hasConnectionPool }.ifAbsent(false)
* }</pre>
*/
@FunctionalInterface
public interface CompileOnlyCall<T> {

@NotNull
T call();

/**
* Invokes this callable and returns its result. If the call throws a {@link LinkageError},
* returns {@code fallback} instead.
*/
default @NotNull T ifAbsent(final @NotNull T fallback) {
return run(this, error -> fallback);
}

/**
* Invokes this callable and returns its result. If the call throws a {@link LinkageError},
* invokes {@code fallback} and returns its result instead.
*/
default @NotNull T ifAbsent(final @NotNull Fallback<T> fallback) {
return run(this, fallback);
}
}

/** Fallback that receives the caught {@link LinkageError} and returns a non-null value. */
@FunctionalInterface
public interface Fallback<T> {
@NotNull
T call(@NotNull LinkageError error);
}

private CompileOnlyCompat() {}

private static <T> @NotNull T run(
final @NotNull CompileOnlyCall<T> call, final @NotNull Fallback<T> fallback) {
try {
return call.call();
} catch (LinkageError e) {
return fallback.call(e);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we're not logging. Three reasons for that:

  1. Falling back when the method isn't present is correct behavior and expected from the consumer's side in cases where there's a default implementation for the method and our fallback matches its behavior (this will almost always be the case).
  2. Wiring a logger through this Java API would make it even more ungainly.
  3. Callers can always log themselves via the Fallback lambda.

}
}
}
79 changes: 79 additions & 0 deletions sentry/src/test/java/io/sentry/util/CompileOnlyCompatTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.sentry.util

import io.sentry.util.CompileOnlyCompat.CompileOnlyCall
import io.sentry.util.CompileOnlyCompat.Fallback
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

class CompileOnlyCompatTest {

@Test
fun `ifAbsent returns call result when method exists`() {
val result = CompileOnlyCall { "hello" }.ifAbsent("fallback")
assertEquals("hello", result)
}

@Test
fun `ifAbsent returns constant fallback on LinkageError`() {
val result = CompileOnlyCall<String> { throw NoSuchMethodError() }.ifAbsent("fallback")
assertEquals("fallback", result)
}

@Test
fun `ifAbsent does not catch non-LinkageErrors`() {
assertFailsWith<IllegalStateException> {
CompileOnlyCall<String> { throw IllegalStateException() }.ifAbsent("fallback")
}
}

@Test
fun `ifAbsent with Fallback returns call result when method exists`() {
val result = CompileOnlyCall { "hello" }.ifAbsent(Fallback { _ -> "fallback" })
assertEquals("hello", result)
}

@Test
fun `ifAbsent with Fallback invokes fallback on LinkageError`() {
var fallbackInvoked = false
val result =
CompileOnlyCall<String> { throw NoSuchMethodError() }
.ifAbsent { _ ->
fallbackInvoked = true
"fallback"
}
assertEquals("fallback", result)
assertTrue(fallbackInvoked)
}

@Test
fun `ifAbsent with Fallback passes the LinkageError`() {
var captured: LinkageError? = null
CompileOnlyCall<String> { throw NoSuchMethodError("test") }
.ifAbsent { error ->
captured = error
"fallback"
}
assertTrue(captured is NoSuchMethodError)
assertEquals("test", captured!!.message)
}

@Test
fun `ifAbsent with Fallback does not invoke fallback on success`() {
var fallbackInvoked = false
CompileOnlyCall { "hello" }
.ifAbsent { _ ->
fallbackInvoked = true
"fallback"
}
assertTrue(!fallbackInvoked)
}

@Test
fun `ifAbsent with Fallback does not catch non-LinkageErrors`() {
assertFailsWith<IllegalStateException> {
CompileOnlyCall<String> { throw IllegalStateException() }.ifAbsent { _ -> "fallback" }
}
}
}
Loading