diff --git a/detekt_custom_safe_calls_android.yml b/detekt_custom_safe_calls_android.yml index 01eaade435..1827b27a2e 100644 --- a/detekt_custom_safe_calls_android.yml +++ b/detekt_custom_safe_calls_android.yml @@ -22,8 +22,8 @@ datadog: - "android.content.ComponentName.resolveViewUrl()" - "android.content.Context.createDeviceProtectedStorageContext()" - "android.content.Context.getSharedPreferences(kotlin.String?, kotlin.Int)" - - "android.content.Context.getSystemService(kotlin.String)" - "android.content.Context.getSystemService(java.lang.Class)" + - "android.content.Context.getSystemService(kotlin.String)" - "android.content.Context.registerComponentCallbacks(android.content.ComponentCallbacks?)" - "android.content.Context.registerReceiver(android.content.BroadcastReceiver?, android.content.IntentFilter?)" - "android.content.Context.registerReceiver(android.content.BroadcastReceiver?, android.content.IntentFilter?, kotlin.Int)" @@ -40,7 +40,6 @@ datadog: - "android.content.IntentFilter.constructor(kotlin.String?)" - "android.content.pm.PackageManager.PackageInfoFlags.of(kotlin.Long)" - "android.content.pm.PackageManager.hasSystemFeature(kotlin.String)" - - "android.content.res.AssetManager.open(kotlin.String, kotlin.Int)" - "android.content.res.ColorStateList.getColor(kotlin.IntArray)" - "android.content.res.ColorStateList.getColorForState(kotlin.IntArray?, kotlin.Int)" - "android.content.res.Resources.Theme.resolveAttribute(kotlin.Int, android.util.TypedValue?, kotlin.Boolean)" @@ -156,12 +155,12 @@ datadog: - "android.view.Window.peekDecorView()" - "android.view.inspector.WindowInspector.getGlobalWindowViews()" - "androidx.collection.LruCache.evictAll()" + - "androidx.collection.LruCache.hitCount()" - "androidx.collection.LruCache.maxSize()" + - "androidx.collection.LruCache.missCount()" - "androidx.collection.LruCache.put(com.datadog.android.flags.internal.ExposureEventsProcessor.CacheKey, kotlin.Boolean)" - "androidx.collection.LruCache.put(com.datadog.android.sessionreplay.internal.recorder.resources.Alpha8CacheKey, kotlin.String)" - "androidx.collection.LruCache.size()" - - "androidx.collection.LruCache.hitCount()" - - "androidx.collection.LruCache.missCount()" - "androidx.collection.LruCache.trimToSize(kotlin.Int)" # endregion # region Android Webview APIs @@ -235,9 +234,9 @@ datadog: - "android.graphics.Rect.height()" - "android.graphics.Rect.width()" - "android.graphics.RectF.constructor()" - - "android.graphics.RectF.width()" - "android.graphics.RectF.height()" - "android.graphics.RectF.toRect()" + - "android.graphics.RectF.width()" - "android.graphics.Typeface.create(android.graphics.Typeface?, kotlin.Int)" - "android.os.Handler.sendMessageAtFrontOfQueue(android.os.Message)" - "android.os.Message.obtain(android.os.Handler?, java.lang.Runnable?)" @@ -251,9 +250,9 @@ datadog: - "androidx.compose.runtime.Composition.takeIf(kotlin.Function1)" - "androidx.compose.runtime.DisposableEffect(kotlin.Any?, kotlin.Any?, kotlin.Function1)" - "androidx.compose.runtime.DisposableEffectScope.onDispose(kotlin.Function0)" - - "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.coroutines.SuspendFunction1)" - - "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.Any?, kotlin.coroutines.SuspendFunction1)" - "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.Any?, kotlin.Any?, kotlin.coroutines.SuspendFunction1)" + - "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.Any?, kotlin.coroutines.SuspendFunction1)" + - "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.coroutines.SuspendFunction1)" - "androidx.compose.runtime.produceState(kotlin.Boolean, kotlin.Any?, kotlin.coroutines.SuspendFunction1)" - "androidx.compose.runtime.ProduceStateScope.awaitDispose(kotlin.Function0)" - "androidx.compose.runtime.remember(kotlin.Any?, kotlin.Any?, kotlin.Function0)" @@ -289,18 +288,15 @@ datadog: - "android.content.ContentResolver.registerContentObserver(android.net.Uri, kotlin.Boolean, android.database.ContentObserver)" - "android.content.ContentResolver.unregisterContentObserver(android.database.ContentObserver)" - "android.content.SharedPreferences.edit(kotlin.Boolean, kotlin.Function1)" - - "androidx.core.content.ContextCompat.getColor(android.content.Context, kotlin.Int)" - "androidx.core.content.ContextCompat.getColor(android.content.Context?, kotlin.Int)" - "androidx.core.os.requestProfiling(android.content.Context, androidx.core.os.ProfilingRequest, java.util.concurrent.Executor?, java.util.function.Consumer?)" - "androidx.core.os.StackSamplingRequestBuilder.build()" - "androidx.core.os.StackSamplingRequestBuilder.constructor()" - "androidx.core.os.StackSamplingRequestBuilder.setBufferSizeKb(kotlin.Int)" - - "androidx.core.os.StackSamplingRequestBuilder.setDurationMs(kotlin.Int)" - "androidx.core.os.StackSamplingRequestBuilder.setCancellationSignal(android.os.CancellationSignal)" + - "androidx.core.os.StackSamplingRequestBuilder.setDurationMs(kotlin.Int)" - "androidx.core.os.StackSamplingRequestBuilder.setSamplingFrequencyHz(kotlin.Int)" - "androidx.core.os.StackSamplingRequestBuilder.setTag(kotlin.String)" - - "androidx.core.view.GestureDetectorCompat.constructor(android.content.Context, android.view.GestureDetector.OnGestureListener)" - - "androidx.core.view.GestureDetectorCompat.onTouchEvent(android.view.MotionEvent)" - "androidx.core.view.GestureDetectorCompat.constructor(android.content.Context?, android.view.GestureDetector.OnGestureListener?)" - "androidx.core.view.GestureDetectorCompat.onTouchEvent(android.view.MotionEvent?)" - "androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks.onFragmentActivityCreated(androidx.fragment.app.FragmentManager, androidx.fragment.app.Fragment, android.os.Bundle?)" diff --git a/detekt_custom_safe_calls_third_party.yml b/detekt_custom_safe_calls_third_party.yml index d2cf5d4fc6..872bb3a38d 100644 --- a/detekt_custom_safe_calls_third_party.yml +++ b/detekt_custom_safe_calls_third_party.yml @@ -15,8 +15,8 @@ datadog: - "org.chromium.net.CronetEngine.addRequestFinishedListener(org.chromium.net.RequestFinishedInfo.Listener?)" - "org.chromium.net.CronetEngine.addRttListener(org.chromium.net.NetworkQualityRttListener?)" - "org.chromium.net.CronetEngine.addThroughputListener(org.chromium.net.NetworkQualityThroughputListener?)" - - "org.chromium.net.CronetEngine.configureNetworkQualityEstimatorForTesting(kotlin.Boolean, kotlin.Boolean, kotlin.Boolean)" - "org.chromium.net.CronetEngine.bindToNetwork(kotlin.Long)" + - "org.chromium.net.CronetEngine.configureNetworkQualityEstimatorForTesting(kotlin.Boolean, kotlin.Boolean, kotlin.Boolean)" - "org.chromium.net.CronetEngine.createURLStreamHandlerFactory()" - "org.chromium.net.CronetEngine.newBidirectionalStreamBuilder(kotlin.String?, org.chromium.net.BidirectionalStream.Callback?, java.util.concurrent.Executor?)" - "org.chromium.net.CronetEngine.newUrlRequestBuilder(kotlin.String?, org.chromium.net.UrlRequest.Callback?, java.util.concurrent.Executor?)" @@ -38,14 +38,14 @@ datadog: - "org.chromium.net.CronetEngine.Builder.enablePublicKeyPinningBypassForLocalTrustAnchors(kotlin.Boolean)" - "org.chromium.net.CronetEngine.Builder.enableQuic(kotlin.Boolean)" - "org.chromium.net.CronetEngine.Builder.enableSdch(kotlin.Boolean)" - - "org.chromium.net.CronetEngine.Builder.setConnectionMigrationOptions(org.chromium.net.ConnectionMigrationOptions?)" - "org.chromium.net.CronetEngine.Builder.setConnectionMigrationOptions(org.chromium.net.ConnectionMigrationOptions.Builder?)" - - "org.chromium.net.CronetEngine.Builder.setDnsOptions(org.chromium.net.DnsOptions?)" + - "org.chromium.net.CronetEngine.Builder.setConnectionMigrationOptions(org.chromium.net.ConnectionMigrationOptions?)" - "org.chromium.net.CronetEngine.Builder.setDnsOptions(org.chromium.net.DnsOptions.Builder?)" + - "org.chromium.net.CronetEngine.Builder.setDnsOptions(org.chromium.net.DnsOptions?)" - "org.chromium.net.CronetEngine.Builder.setLibraryLoader(org.chromium.net.CronetEngine.Builder.LibraryLoader?)" - "org.chromium.net.CronetEngine.Builder.setProxyOptions(org.chromium.net.ProxyOptions?)" - - "org.chromium.net.CronetEngine.Builder.setQuicOptions(org.chromium.net.QuicOptions?)" - "org.chromium.net.CronetEngine.Builder.setQuicOptions(org.chromium.net.QuicOptions.Builder?)" + - "org.chromium.net.CronetEngine.Builder.setQuicOptions(org.chromium.net.QuicOptions?)" - "org.chromium.net.CronetEngine.Builder.setStoragePath(kotlin.String?)" - "org.chromium.net.CronetEngine.Builder.setThreadPriority(kotlin.Int)" - "org.chromium.net.CronetEngine.Builder.setUserAgent(kotlin.String?)" @@ -62,11 +62,11 @@ datadog: - "org.chromium.net.UrlRequest.Builder.setTrafficStatsTag(kotlin.Int)" - "org.chromium.net.UrlRequest.Builder.setTrafficStatsUid(kotlin.Int)" - "org.chromium.net.UrlRequest.Builder.setUploadDataProvider(org.chromium.net.UploadDataProvider?, java.util.concurrent.Executor?)" - - "org.chromium.net.UrlRequest.getStatus(org.chromium.net.UrlRequest.StatusListener?)" - - "org.chromium.net.UrlRequest.start()" - "org.chromium.net.UrlRequest.cancel()" - "org.chromium.net.UrlRequest.followRedirect()" + - "org.chromium.net.UrlRequest.getStatus(org.chromium.net.UrlRequest.StatusListener?)" - "org.chromium.net.UrlRequest.read(java.nio.ByteBuffer?)" + - "org.chromium.net.UrlRequest.start()" # endregion # region Glide - "com.bumptech.glide.GlideBuilder.setDiskCacheExecutor(com.bumptech.glide.load.engine.executor.GlideExecutor?)" @@ -118,18 +118,9 @@ datadog: - "java.util.HashSet.add(kotlin.String)" - "java.util.HashSet.clear()" - "java.util.HashSet.remove(kotlin.String)" - - "java.util.LinkedList.add(android.view.View)" - - "java.util.LinkedList.add(com.datadog.android.privacy.TrackingConsentProviderCallback)" - - "java.util.LinkedList.add(com.datadog.android.sessionreplay.internal.recorder.Node)" - - "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.Add)" - - "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.Remove)" - - "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)" - - "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.WireframeUpdateMutation)" - - "java.util.LinkedList.add(com.datadog.android.sessionreplay.recorder.Node)" - - "java.util.LinkedList.add(kotlin.Any?)" - - "java.util.LinkedList.add(kotlin.Pair)" - - "java.util.LinkedList.addAll(kotlin.collections.Collection)" + - "java.util.LinkedList.add(?)" - "java.util.LinkedList.addAll(kotlin.Int, kotlin.collections.Collection)" + - "java.util.LinkedList.addAll(kotlin.collections.Collection)" - "java.util.LinkedList.addFirst(android.view.View?)" - "java.util.LinkedList.addFirst(com.datadog.android.webview.internal.rum.domain.WebViewNativeRumViewsCache.ViewEntry?)" - "java.util.LinkedList.clear()" @@ -144,7 +135,7 @@ datadog: - "java.util.LinkedList.poll()" - "java.util.LinkedList.remove(com.datadog.android.privacy.TrackingConsentProviderCallback)" - "java.util.LinkedList.remove(com.datadog.android.webview.internal.rum.domain.WebViewNativeRumViewsCache.ViewEntry)" - - "java.util.LinkedHashMap.remove(kotlin.String)" + - "java.util.LinkedHashMap.remove(*)" - "java.util.Deque.add(kotlin.Any?)" - "java.util.Deque.addAll(kotlin.collections.Collection)" - "java.util.Deque.poll()" @@ -169,7 +160,6 @@ datadog: - "java.lang.Thread.currentThread()" - "java.lang.Thread.getDefaultUncaughtExceptionHandler()" - "java.lang.Thread.interrupted()" - - "java.lang.Thread.interrupt()" - "java.lang.Thread.isInterrupted()" - "java.lang.Thread.setDefaultUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler?)" - "java.lang.Thread.threadId()" @@ -183,8 +173,8 @@ datadog: - "java.util.concurrent.ConcurrentHashMap.isEmpty()" - "java.util.concurrent.ConcurrentHashMap.map(kotlin.Function1)" - "java.util.concurrent.ConcurrentHashMap.putIfAbsent(kotlin.String, com.datadog.android.rum.internal.metric.slowframes.DefaultUISlownessMetricDispatcher.SlowFramesTelemetry)" - - "java.util.concurrent.ConcurrentHashMap.remove(okhttp3.Call)" - "java.util.concurrent.ConcurrentHashMap.remove(kotlin.String)" + - "java.util.concurrent.ConcurrentHashMap.remove(okhttp3.Call)" - "java.util.concurrent.ConcurrentHashMap.toMap()" - "java.util.concurrent.ConcurrentLinkedDeque.constructor()" - "java.util.concurrent.ConcurrentLinkedQueue.constructor()" @@ -192,14 +182,10 @@ datadog: - "java.util.concurrent.ConcurrentLinkedQueue.isNotEmpty()" - "java.util.concurrent.ConcurrentLinkedQueue.peek()" - "java.util.concurrent.ConcurrentLinkedQueue.poll()" - - "java.util.concurrent.CopyOnWriteArraySet.add(android.util.Printer?)" - - "java.util.concurrent.CopyOnWriteArraySet.add(kotlin.Any?)" - - "java.util.concurrent.CopyOnWriteArraySet.add(kotlin.String?)" + - "java.util.concurrent.CopyOnWriteArraySet.add(?)" - "java.util.concurrent.CopyOnWriteArraySet.constructor()" - "java.util.concurrent.CopyOnWriteArraySet.forEach(kotlin.Function1)" - - "java.util.concurrent.CopyOnWriteArraySet.remove(android.util.Printer?)" - - "java.util.concurrent.CopyOnWriteArraySet.remove(kotlin.Any?)" - - "java.util.concurrent.CopyOnWriteArraySet.remove(kotlin.String?)" + - "java.util.concurrent.CopyOnWriteArraySet.remove(?)" - "java.util.concurrent.CopyOnWriteArraySet.toTypedArray()" - "java.util.concurrent.CountDownLatch.countDown()" - "java.util.concurrent.ExecutorService.shutdown()" @@ -244,43 +230,11 @@ datadog: - "java.util.concurrent.atomic.AtomicLong.constructor(kotlin.Long)" - "java.util.concurrent.atomic.AtomicLong.get()" - "java.util.concurrent.atomic.AtomicLong.set(kotlin.Long)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(com.datadog.trace.core.CoreTracer?, com.datadog.trace.core.CoreTracer?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(com.datadog.android.api.SdkCore?, com.datadog.android.api.SdkCore?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(com.datadog.android.flags.internal.repository.DefaultFlagsRepository.FlagsState?, com.datadog.android.flags.internal.repository.DefaultFlagsRepository.FlagsState?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI?, com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(com.datadog.android.trace.api.tracer.DatadogTracer?, com.datadog.android.trace.api.tracer.DatadogTracer?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(kotlin.String?, kotlin.String?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(kotlin.collections.Set?, kotlin.collections.Set?)" - - "java.util.concurrent.atomic.AtomicReference.compareAndSet(UNKNOWN, UNKNOWN)" + - "java.util.concurrent.atomic.AtomicReference.compareAndSet(?, ?)" - "java.util.concurrent.atomic.AtomicReference.constructor()" - - "java.util.concurrent.atomic.AtomicReference.constructor(android.app.Application.ActivityLifecycleCallbacks?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.SdkCore?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.feature.FeatureEventReceiver?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.internal.repository.DefaultFlagsRepository.FlagsState?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.FlagsClientState?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.ProviderContext?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.rum.internal.domain.RumContext?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.collections.Map?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.String?)" - - "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.collections.Set?)" + - "java.util.concurrent.atomic.AtomicReference.constructor(?)" - "java.util.concurrent.atomic.AtomicReference.get()" - - "java.util.concurrent.atomic.AtomicReference.set(android.app.Application.ActivityLifecycleCallbacks?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.api.FeatureEventReceiver?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.api.SdkCore?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.api.feature.FeatureEventReceiver?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.cronet.internal.RequestTracingSnapshot?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.rum.internal.domain.RumContext?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.trace.api.tracer.DatadogTracer?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.trace.internal.net.RequestTracingState?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.flags.internal.repository.DefaultFlagsRepository.FlagsState?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.flags.model.ProviderContext?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI?)" - - "java.util.concurrent.atomic.AtomicReference.set(com.datadog.trace.core.CoreTracer?)" - - "java.util.concurrent.atomic.AtomicReference.set(kotlin.collections.Map?)" - - "java.util.concurrent.atomic.AtomicReference.set(kotlin.Nothing?)" - - "java.util.concurrent.atomic.AtomicReference.set(kotlin.String?)" - - "java.util.concurrent.atomic.AtomicReference.set(kotlin.collections.Set?)" - - "java.util.concurrent.atomic.AtomicReference.set(UNKNOWN)" + - "java.util.concurrent.atomic.AtomicReference.set(?)" - "java.util.concurrent.locks.ReadWriteLock.readLock()" - "java.util.concurrent.locks.ReadWriteLock.writeLock()" - "java.util.concurrent.locks.ReentrantLock.constructor()" @@ -311,11 +265,10 @@ datadog: # endregion # region Java misc - "java.lang.Character.isWhitespace(kotlin.Int)" + - "java.lang.Class.getDeclaredFieldSafe(kotlin.String)" - "java.lang.Class.hashCode()" - "java.lang.Class.isInstance(kotlin.Any?)" - - "java.lang.Class.getDeclaredFieldSafe(kotlin.String)" - "java.lang.IllegalArgumentException.constructor(kotlin.String)" - - "java.lang.IllegalStateException.constructor(kotlin.String)" - "java.lang.IllegalStateException.constructor(kotlin.String?)" - "java.lang.Object.constructor()" - "java.lang.Runtime.availableProcessors()" @@ -328,19 +281,11 @@ datadog: - "java.lang.System.nanoTime()" - "java.lang.ref.Reference.get()" - "java.lang.ref.WeakReference.clear()" - - "java.lang.ref.WeakReference.constructor(android.app.Activity?)" - - "java.lang.ref.WeakReference.constructor(android.content.Context?)" - - "java.lang.ref.WeakReference.constructor(android.view.View?)" - - "java.lang.ref.WeakReference.constructor(android.view.Window?)" - - "java.lang.ref.WeakReference.constructor(com.datadog.android.api.SdkCore?)" - - "java.lang.ref.WeakReference.constructor(com.datadog.android.rum.tracking.ViewTarget?)" - - "java.lang.ref.WeakReference.constructor(kotlin.Any?)" - - "java.lang.ref.WeakReference.constructor(kotlin.Nothing?)" - - "java.lang.ref.WeakReference.constructor(kotlin.String?)" + - "java.lang.ref.WeakReference.constructor(?)" - "java.lang.ref.WeakReference.get()" - "java.lang.reflect.isStatic(kotlin.Int)" - - "java.lang.reflect.Field.getSafe(kotlin.Any?)" - "java.lang.reflect.Field.accessible()" + - "java.lang.reflect.Field.getSafe(kotlin.Any?)" - "java.lang.StringBuilder.append(kotlin.Char)" - "java.lang.StringBuilder.append(kotlin.CharArray?)" - "java.lang.StringBuilder.append(kotlin.String?)" @@ -378,14 +323,11 @@ datadog: - "java.util.UUID.randomUUID()" - "java.util.WeakHashMap.clear()" - "java.util.WeakHashMap.constructor()" - - "java.util.WeakHashMap.containsKey(android.view.Window?)" + - "java.util.WeakHashMap.containsKey(?)" - "java.util.WeakHashMap.forEach(kotlin.Function1)" - - "java.util.WeakHashMap.getOrPut(android.app.Activity?, kotlin.Function0)" - - "java.util.WeakHashMap.put(android.app.Activity?, android.view.ViewTreeObserver.OnDrawListener?)" - - "java.util.WeakHashMap.put(android.app.Activity?, com.datadog.android.rum.internal.utils.window.RumWindowCallbackListener?)" - - "java.util.WeakHashMap.remove(android.app.Activity?)" - - "java.util.WeakHashMap.remove(android.view.View?)" - - "java.util.WeakHashMap.remove(android.view.Window?)" + - "java.util.WeakHashMap.getOrPut(?, kotlin.Function0)" + - "java.util.WeakHashMap.put(?, ?)" + - "java.util.WeakHashMap.remove(?)" - "java.util.concurrent.ExecutionException.constructor(kotlin.String?, kotlin.Throwable?)" # endregion # region Java Zip @@ -411,15 +353,14 @@ datadog: # region Kotlin Collections - "kotlin.Array.all(kotlin.Function1)" - "kotlin.Array.any(kotlin.Function1)" - - "kotlin.Array.associateWith(kotlin.Function1)" - "kotlin.Array.associateBy(kotlin.Function1)" + - "kotlin.Array.associateWith(kotlin.Function1)" - "kotlin.Array.constructor(kotlin.Int, kotlin.Function1)" - "kotlin.Array.contentEquals(kotlin.Array?)" - "kotlin.Array.contentHashCode()" - "kotlin.Array.count(kotlin.Function1)" - "kotlin.Array.filter(kotlin.Function1)" - "kotlin.Array.filterNotNull(kotlin.Function1)" - - "kotlin.Array.first(kotlin.Function1)" - "kotlin.Array.firstOrNull(kotlin.Function1)" - "kotlin.Array.forEach(kotlin.Function1)" - "kotlin.Array.forEachIndexed(kotlin.Function2)" @@ -440,8 +381,8 @@ datadog: - "kotlin.ByteArray.isNotEmpty()" - "kotlin.ByteArray.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)" - "kotlin.arrayOf(kotlin.Array)" - - "kotlin.collections.arrayListOf()" - "kotlin.collections.ArrayList(kotlin.collections.MutableCollection?)" + - "kotlin.collections.arrayListOf()" - "kotlin.collections.buildMap(kotlin.Function1)" - "kotlin.collections.Collection.flatten()" - "kotlin.collections.Collection.fold(kotlin.collections.MutableSet, kotlin.Function2)" @@ -470,7 +411,7 @@ datadog: - "kotlin.collections.List.associateBy(kotlin.Function1)" - "kotlin.collections.List.associateWith(kotlin.Function1)" - "kotlin.collections.List.average()" - - "kotlin.collections.List.contains(kotlin.String)" + - "kotlin.collections.List.contains(*)" - "kotlin.collections.List.count()" - "kotlin.collections.List.distinct()" - "kotlin.collections.List.drop(kotlin.Int)" @@ -481,9 +422,9 @@ datadog: - "kotlin.collections.List.filterNot(kotlin.Function1)" - "kotlin.collections.List.filterNotNull(kotlin.Function1)" - "kotlin.collections.List.findFirstForType(java.lang.Class)" + - "kotlin.collections.List.firstNotNullOfOrNull(kotlin.Function1)" - "kotlin.collections.List.firstOrNull()" - "kotlin.collections.List.firstOrNull(kotlin.Function1)" - - "kotlin.collections.List.firstNotNullOfOrNull(kotlin.Function1)" - "kotlin.collections.List.flatMap(kotlin.Function1)" - "kotlin.collections.List.fold(com.google.gson.JsonArray, kotlin.Function2)" - "kotlin.collections.List.fold(kotlin.Long, kotlin.Function2)" @@ -525,14 +466,12 @@ datadog: - "kotlin.collections.List.windowed(kotlin.Int, kotlin.Int, kotlin.Boolean, kotlin.Function1)" - "kotlin.collections.List.withIndex()" - "kotlin.collections.Map.asSequence()" - - "kotlin.collections.Map.containsKey(kotlin.String)" + - "kotlin.collections.Map.containsKey(*)" - "kotlin.collections.Map.filter(kotlin.Function1)" - "kotlin.collections.Map.filterKeys(kotlin.Function1)" - "kotlin.collections.Map.filterValues(kotlin.Function1)" - "kotlin.collections.Map.forEach(kotlin.Function1)" - - "kotlin.collections.Map.forEach(kotlin.Function1)" # one of our usage is with <*, *> which doesn't get captured - - "kotlin.collections.Map.get(kotlin.String)" - - "kotlin.collections.Map.get(kotlin.String?)" + - "kotlin.collections.Map.get(?)" - "kotlin.collections.Map.ifEmpty(kotlin.Function0)" - "kotlin.collections.Map.isEmpty()" - "kotlin.collections.Map.isNotEmpty()" @@ -546,42 +485,15 @@ datadog: - "kotlin.collections.Map.toJsonObject()" - "kotlin.collections.Map.toMap()" - "kotlin.collections.Map.toMutableMap()" - - "kotlin.collections.MutableCollection.firstOrNull(kotlin.Function1)" - "kotlin.collections.MutableCollection.filterIsInstance()" + - "kotlin.collections.MutableCollection.firstOrNull(kotlin.Function1)" - "kotlin.collections.MutableCollection.flatMap(kotlin.Function1)" - "kotlin.collections.MutableCollection.forEach(kotlin.Function1)" - "kotlin.collections.MutableCollection.toList()" - "kotlin.collections.MutableIterator.forEach(kotlin.Function1)" - "kotlin.collections.MutableIterator.hasNext()" - - "kotlin.collections.MutableList.add(com.datadog.android.api.InternalLogger.Target)" - - "kotlin.collections.MutableList.add(com.datadog.android.core.internal.persistence.Batch)" - - "kotlin.collections.MutableList.add(com.datadog.android.core.internal.persistence.tlvformat.TLVBlock)" - - "kotlin.collections.MutableList.add(com.datadog.android.rum.internal.domain.scope.RumSessionScope)" - - "kotlin.collections.MutableList.add(com.datadog.android.rum.internal.domain.scope.RumScope)" - - "kotlin.collections.MutableList.add(com.datadog.android.rum.internal.domain.scope.RumViewScope)" - - "kotlin.collections.MutableList.add(com.datadog.android.rum.internal.vitals.FrameStateListener)" - - "kotlin.collections.MutableList.add(com.datadog.android.rum.model.ActionEvent.Type)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.compose.internal.data.Parameter)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.compose.internal.utils.BackgroundInfo)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.prerequisite.SystemRequirementChecker)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.processor.MutationResolver.Entry)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.model.MobileSegment.Add)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.model.MobileSegment.MobileRecord)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.model.MobileSegment.Position)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.model.MobileSegment.Remove)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)" - - "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.model.MobileSegment.WireframeUpdateMutation)" - - "kotlin.collections.MutableList.add(java.io.File)" - - "kotlin.collections.MutableList.add(java.lang.ref.WeakReference)" - - "kotlin.collections.MutableList.add(java.net.InetAddress)" - - "kotlin.collections.MutableList.add(kotlin.Any)" - - "kotlin.collections.MutableList.add(kotlin.ByteArray)" - - "kotlin.collections.MutableList.add(kotlin.Int)" - - "kotlin.collections.MutableList.add(kotlin.Int, com.datadog.android.sessionreplay.MapperTypeWrapper)" - - "kotlin.collections.MutableList.add(kotlin.Int, com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper)" - - "kotlin.collections.MutableList.add(kotlin.Int, com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)" - - "kotlin.collections.MutableList.add(kotlin.Int, com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ShapeWireframe)" - - "kotlin.collections.MutableList.add(kotlin.String)" + - "kotlin.collections.MutableList.add(*)" + - "kotlin.collections.MutableList.add(kotlin.Int, *)" - "kotlin.collections.MutableList.addAll(kotlin.collections.Collection)" - "kotlin.collections.MutableList.any(kotlin.Function1)" - "kotlin.collections.MutableList.clear()" @@ -589,10 +501,11 @@ datadog: - "kotlin.collections.MutableList.filter(kotlin.Function1)" - "kotlin.collections.MutableList.filterIsInstance()" - "kotlin.collections.MutableList.find(kotlin.Function1)" + - "kotlin.collections.MutableList.firstOrNull()" - "kotlin.collections.MutableList.firstOrNull(kotlin.Function1)" - "kotlin.collections.MutableList.forEach(kotlin.Function1)" - "kotlin.collections.MutableList.forEachIndexed(kotlin.Function2)" - - "kotlin.collections.MutableList.indexOf(kotlin.Any)" + - "kotlin.collections.MutableList.indexOf(*)" - "kotlin.collections.MutableList.isEmpty()" - "kotlin.collections.MutableList.isNotEmpty()" - "kotlin.collections.MutableList.isNullOrEmpty()" @@ -600,100 +513,61 @@ datadog: - "kotlin.collections.MutableList.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)" - "kotlin.collections.MutableList.map(kotlin.Function1)" - "kotlin.collections.MutableList.partition(kotlin.Function1)" - - "kotlin.collections.MutableList.remove(java.io.File)" - - "kotlin.collections.MutableList.remove(java.lang.ref.WeakReference)" - - "kotlin.collections.MutableList.remove(kotlin.Int)" + - "kotlin.collections.MutableList.remove(*)" - "kotlin.collections.MutableList.removeAll(kotlin.Function1)" - "kotlin.collections.MutableList.removeAt(kotlin.Int)" - "kotlin.collections.MutableList.removeFirstOrNull()" - - "kotlin.collections.MutableList.toMutableList()" - "kotlin.collections.MutableList.toList()" + - "kotlin.collections.MutableList.toMutableList()" - "kotlin.collections.MutableList.toSet()" - "kotlin.collections.MutableList.toTypedArray()" - "kotlin.collections.MutableList.withIndex()" - - "kotlin.collections.MutableList.firstOrNull()" - "kotlin.collections.MutableMap.any(kotlin.Function1)" - "kotlin.collections.MutableMap.asSequence()" - "kotlin.collections.MutableMap.clear()" - - "kotlin.collections.MutableMap.containsKey(android.view.Window)" - - "kotlin.collections.MutableMap.containsKey(com.datadog.android.api.SdkCore)" - - "kotlin.collections.MutableMap.containsKey(com.datadog.android.api.storage.RawBatchEvent)" - - "kotlin.collections.MutableMap.containsKey(kotlin.Long)" - - "kotlin.collections.MutableMap.containsKey(kotlin.String)" + - "kotlin.collections.MutableMap.containsKey(*)" - "kotlin.collections.MutableMap.filter(kotlin.Function1)" - "kotlin.collections.MutableMap.filterKeys(kotlin.Function1)" - "kotlin.collections.MutableMap.filterValues(kotlin.Function1)" - "kotlin.collections.MutableMap.forEach(kotlin.Function1)" - - "kotlin.collections.MutableMap.get(java.lang.Class)" - - "kotlin.collections.MutableMap.get(kotlin.String)" - - "kotlin.collections.MutableMap.get(kotlin.String?)" + - "kotlin.collections.MutableMap.get(?)" - "kotlin.collections.MutableMap.getOrElse(java.lang.Class, kotlin.Function0)" - "kotlin.collections.MutableMap.getOrPut(java.lang.Class, kotlin.Function0)" - "kotlin.collections.MutableMap.getOrPut(kotlin.String, kotlin.Function0)" - "kotlin.collections.MutableMap.isEmpty()" - "kotlin.collections.MutableMap.isNotEmpty()" - "kotlin.collections.MutableMap.iterator()" - - "kotlin.collections.MutableMap.orEmpty()" - "kotlin.collections.MutableMap.map(kotlin.Function1)" - "kotlin.collections.MutableMap.mapValues(kotlin.Function1)" - - "kotlin.collections.MutableMap.put(java.lang.Class, kotlin.String?)" - - "kotlin.collections.MutableMap.put(kotlin.Any?, kotlin.Any?)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.Any)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.Any?)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.String)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.Int)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.Long?)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.Long)" - - "kotlin.collections.MutableMap.put(kotlin.String, kotlin.collections.Map)" - - "kotlin.collections.MutableMap.put(kotlin.String, UNKNOWN)" + - "kotlin.collections.MutableMap.orEmpty()" + - "kotlin.collections.MutableMap.put(?, ?)" - "kotlin.collections.MutableMap.putAll(kotlin.collections.Iterable)" - "kotlin.collections.MutableMap.putAll(kotlin.collections.Map)" - - "kotlin.collections.MutableMap.remove(androidx.compose.foundation.interaction.DragInteraction.Start)" - - "kotlin.collections.MutableMap.remove(com.datadog.android.api.SdkCore)" - - "kotlin.collections.MutableMap.remove(com.datadog.android.rum.internal.vitals.VitalListener)" - - "kotlin.collections.MutableMap.remove(java.lang.Class)" - - "kotlin.collections.MutableMap.remove(kotlin.Int)" - - "kotlin.collections.MutableMap.remove(kotlin.Long)" - - "kotlin.collections.MutableMap.remove(kotlin.String)" + - "kotlin.collections.MutableMap.remove(*)" - "kotlin.collections.MutableMap.toMap()" - "kotlin.collections.MutableMap.toMutableMap()" - "kotlin.collections.MutableMap?.forEach(kotlin.Function1)" - - "kotlin.collections.MutableSet.add(android.app.Activity?)" - - "kotlin.collections.MutableSet.add(com.datadog.android.api.feature.FeatureContextUpdateReceiver)" - - "kotlin.collections.MutableSet.add(com.datadog.android.core.internal.persistence.ConsentAwareStorage.Batch)" - - "kotlin.collections.MutableSet.add(com.datadog.android.profiling.internal.perfetto.PerfettoProfiler.TelemetryData)" - - "kotlin.collections.MutableSet.add(com.datadog.android.sessionreplay.ExtensionSupport)" - - "kotlin.collections.MutableSet.add(com.datadog.android.telemetry.internal.TelemetryEventId)" - - "kotlin.collections.MutableSet.add(java.io.File)" - - "kotlin.collections.MutableSet.add(kotlin.String)" - - "kotlin.collections.MutableSet.add(kotlin.String?)" + - "kotlin.collections.MutableSet.add(?)" - "kotlin.collections.MutableSet.addAll(kotlin.collections.Collection)" - "kotlin.collections.MutableSet.any(kotlin.Function1)" - "kotlin.collections.MutableSet.clear()" - - "kotlin.collections.MutableSet.contains(com.datadog.android.telemetry.internal.TelemetryEventId)" - - "kotlin.collections.MutableSet.contains(kotlin.String)" - - "kotlin.collections.MutableSet.contains(kotlin.String?)" + - "kotlin.collections.MutableSet.contains(?)" + - "kotlin.collections.MutableSet.elementAtOrNull(kotlin.Int)" - "kotlin.collections.MutableSet.filter(kotlin.Function1)" - "kotlin.collections.MutableSet.firstOrNull(kotlin.Function1)" - "kotlin.collections.MutableSet.flatMap(kotlin.Function1)" - "kotlin.collections.MutableSet.forEach(kotlin.Function1)" - - "kotlin.collections.MutableSet.elementAtOrNull(kotlin.Int)" - - "kotlin.collections.MutableSet.indexOf(kotlin.String)" + - "kotlin.collections.MutableSet.indexOf(*)" - "kotlin.collections.MutableSet.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)" + - "kotlin.collections.MutableSet.lastOrNull()" - "kotlin.collections.MutableSet.map(kotlin.Function1)" - - "kotlin.collections.MutableSet.remove(android.app.Activity?)" - - "kotlin.collections.MutableSet.remove(com.datadog.android.api.feature.FeatureContextUpdateReceiver)" - - "kotlin.collections.MutableSet.remove(com.datadog.android.core.internal.persistence.ConsentAwareStorage.Batch)" - - "kotlin.collections.MutableSet.remove(java.io.File)" - - "kotlin.collections.MutableSet.remove(kotlin.collections.MutableMap.Mutab(...)" + - "kotlin.collections.MutableSet.remove(?)" - "kotlin.collections.MutableSet.removeAll(kotlin.collections.Collection)" - - "kotlin.collections.MutableSet.toList()" - - "kotlin.collections.MutableSet.lastOrNull()" - "kotlin.collections.MutableSet.sortedByDescending(kotlin.Function1)" + - "kotlin.collections.MutableSet.toList()" - "kotlin.collections.Set.any(kotlin.Function1)" - "kotlin.collections.Set.associate(kotlin.Function1)" - - "kotlin.collections.Set.contains(com.datadog.android.trace.TracingHeaderType)" - - "kotlin.collections.Set.contains(kotlin.String)" + - "kotlin.collections.Set.contains(*)" - "kotlin.collections.Set.filter(kotlin.Function1)" - "kotlin.collections.Set.firstOrNull()" - "kotlin.collections.Set.firstOrNull(kotlin.Function1)" @@ -707,39 +581,15 @@ datadog: - "kotlin.collections.Set.orEmpty()" - "kotlin.collections.Set.plus(kotlin.collections.Iterable)" - "kotlin.collections.Set.toHashSet()" - - "kotlin.collections.Set.toSet()" - "kotlin.collections.Set.toList()" - "kotlin.collections.Set.toMutableSet()" + - "kotlin.collections.Set.toSet()" - "kotlin.collections.emptyList()" - "kotlin.collections.emptyMap()" - "kotlin.collections.emptySet()" + - "kotlin.collections.linkedMapOf()" - "kotlin.collections.listOf()" - - "kotlin.collections.listOf(android.view.Window)" - - "kotlin.collections.listOf(androidx.compose.ui.graphics.Color)" - - "kotlin.collections.listOf(com.datadog.android.api.InternalLogger.Target)" - - "kotlin.collections.listOf(com.datadog.android.rum.internal.vitals.FPSVitalListener)" - - "kotlin.collections.listOf(com.datadog.android.flags.model.BatchedFlagEvaluations.FlagEvaluation)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.ActionEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.ErrorEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.LongTaskEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.ResourceEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.ViewEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.VitalAppLaunchEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.rum.model.VitalOperationStepEvent.Interface)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.MapperTypeWrapper)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.internal.recorder.DefaultOptionSelectorDetector)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.material.internal.MaterialDrawableToColorMapper)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.material.internal.MaterialOptionSelectorDetector)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.MobileRecord.ViewEndRecord)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.PlaceholderWireframe)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ShapeWireframe)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.TextWireframe)" - - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.WebviewWireframe)" - - "kotlin.collections.listOf(java.io.File)" - - "kotlin.collections.listOf(kotlin.Array)" - - "kotlin.collections.listOf(kotlin.String)" - - "kotlin.collections.listOf(okhttp3.ConnectionSpec)" + - "kotlin.collections.listOf(*)" - "kotlin.collections.listOfNotNull(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe?)" - "kotlin.collections.listOfNotNull(kotlin.Array)" - "kotlin.collections.listOfNotNull(kotlin.String?)" @@ -751,23 +601,19 @@ datadog: - "kotlin.collections.mutableMapOf()" - "kotlin.collections.mutableMapOf(kotlin.Array)" - "kotlin.collections.mutableSetOf()" - - "kotlin.collections.setOf(com.datadog.android.trace.TracingHeaderType)" - - "kotlin.collections.setOf(kotlin.Array)" - - "kotlin.collections.setOf(kotlin.Int)" - - "kotlin.collections.setOf(kotlin.String)" - - "kotlin.collections.linkedMapOf()" + - "kotlin.collections.setOf(*)" - "kotlin.collections.MutableCollection.count(kotlin.Function1)" - - "kotlin.collections.MutableCollection.sumOf(kotlin.Function1)" - "kotlin.collections.MutableCollection.sum()" + - "kotlin.collections.MutableCollection.sumOf(kotlin.Function1)" - "kotlin.collections.List.sortedByDescending(kotlin.Function1)" - "kotlin.emptyArray()" - - "kotlin.sequences.Sequence.groupBy(kotlin.Function1)" - "kotlin.sequences.Sequence.filter(kotlin.Function1)" - "kotlin.sequences.Sequence.firstOrNull()" - "kotlin.sequences.Sequence.firstOrNull(kotlin.Function1)" - "kotlin.sequences.Sequence.flatMap(kotlin.Function1)" - "kotlin.sequences.Sequence.fold(com.google.gson.JsonArray, kotlin.Function2)" - "kotlin.sequences.Sequence.forEach(kotlin.Function1)" + - "kotlin.sequences.Sequence.groupBy(kotlin.Function1)" - "kotlin.sequences.Sequence.map(kotlin.Function1)" - "kotlin.sequences.Sequence.mapIndexedNotNull(kotlin.Function2)" - "kotlin.sequences.Sequence.mapNotNull(kotlin.Function1)" @@ -792,8 +638,8 @@ datadog: - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" - "kotlin.Double.coerceAtMost(kotlin.Double)" - "kotlin.Double.isNaN()" - - "kotlin.Double.pow(kotlin.Int)" - "kotlin.Double.pow(kotlin.Double)" + - "kotlin.Double.pow(kotlin.Int)" - "kotlin.Double.rangeTo(kotlin.Double)" - "kotlin.Double.roundToInt()" - "kotlin.Double.times(kotlin.Long)" @@ -830,8 +676,8 @@ datadog: - "kotlin.Long.shl(kotlin.Int)" - "kotlin.Long.takeIf(kotlin.Function1)" - "kotlin.Long.toDouble()" - - "kotlin.Long.toInt()" - "kotlin.Long.toFloat()" + - "kotlin.Long.toInt()" - "kotlin.Long.toULong()" - "kotlin.LongArray.constructor(kotlin.Int)" - "kotlin.Number.toDouble()" @@ -862,15 +708,15 @@ datadog: - "kotlin.math.sqrt(kotlin.Double)" # endregion # region Kotlin Tuples - - "kotlin.Pair.constructor(kotlin.Float, kotlin.Float)" - - "kotlin.Pair.constructor(kotlin.String, kotlin.Int)" - "kotlin.Pair.constructor(com.datadog.android.sessionreplay.internal.utils.SessionReplayRumContext, com.google.gson.JsonArray)" - "kotlin.Pair.constructor(com.datadog.android.sessionreplay.model.MobileSegment, com.google.gson.JsonObject)" - "kotlin.Pair.constructor(com.google.gson.JsonObject, kotlin.Long)" + - "kotlin.Pair.constructor(kotlin.Float, kotlin.Float)" - "kotlin.Pair.constructor(kotlin.Int, kotlin.Int)" - "kotlin.Pair.constructor(kotlin.Long, kotlin.Long)" - - "kotlin.Triple.constructor(kotlin.String?, kotlin.String?, kotlin.String?)" + - "kotlin.Pair.constructor(kotlin.String, kotlin.Int)" - "kotlin.Triple.constructor(kotlin.Nothing?, kotlin.Nothing?, kotlin.Nothing?)" + - "kotlin.Triple.constructor(kotlin.String?, kotlin.String?, kotlin.String?)" # endregion # region Kotlin String - "kotlin.Any?.hashCode()" @@ -914,7 +760,6 @@ datadog: - "kotlin.String.substringAfterLast(kotlin.Char, kotlin.String)" - "kotlin.String.substringBefore(kotlin.Char, kotlin.String)" - "kotlin.String.takeLeastSignificant64Bits()" - - "kotlin.String.toByteArray(java.nio.charset.Charset) " - "kotlin.String.toByteArray(java.nio.charset.Charset)" - "kotlin.String.toDoubleOrNull()" - "kotlin.String.toHttpUrlOrNull()" @@ -922,16 +767,16 @@ datadog: - "kotlin.String.toIntOrNull(kotlin.Int)" - "kotlin.String.toLongOrNull()" - "kotlin.String.toLongOrNull(kotlin.Int)" - - "kotlin.String.toULongOrNull(kotlin.Int)" - "kotlin.String.toMediaTypeOrNull()" - "kotlin.String.toMethod()" + - "kotlin.String.toULongOrNull(kotlin.Int)" - "kotlin.String.trim()" - "kotlin.String.trimEnd(kotlin.CharArray)" - "kotlin.String.trimStart()" - "kotlin.String.uppercase(java.util.Locale)" - "kotlin.text.String(kotlin.ByteArray)" - - "kotlin.text.String(kotlin.ByteArray, kotlin.Int, kotlin.Int, java.nio.charset.Charset)" - "kotlin.text.String(kotlin.ByteArray, java.nio.charset.Charset)" + - "kotlin.text.String(kotlin.ByteArray, kotlin.Int, kotlin.Int, java.nio.charset.Charset)" - "kotlin.text.String(kotlin.CharArray)" - "kotlin.text.StringBuilder()" - "kotlin.text.buildString(kotlin.Function1)" @@ -1003,8 +848,8 @@ datadog: - "okhttp3.HttpUrl.url()" - "okhttp3.Interceptor.Chain.call()" - "okhttp3.Interceptor.Chain.request()" - - "okhttp3.MultipartBody.Builder.constructor(kotlin.String)" - "okhttp3.MultipartBody.Builder.addFormDataPart(kotlin.String, kotlin.String?, okhttp3.RequestBody)" + - "okhttp3.MultipartBody.Builder.constructor(kotlin.String)" - "okhttp3.MultipartBody.Part.body()" - "okhttp3.MultipartBody.Part.headers()" - "okhttp3.MultipartBody.parts()" @@ -1069,9 +914,9 @@ datadog: - "dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.GeneralError.constructor(kotlin.String)" - "dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.ProviderFatalError.constructor()" - "dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.ProviderFatalError.constructor(kotlin.String)" + - "dev.openfeature.kotlin.sdk.ProviderEvaluation.constructor(dev.openfeature.kotlin.sdk.Value, kotlin.String?, kotlin.String?, dev.openfeature.kotlin.sdk.exceptions.ErrorCode?, kotlin.String?, dev.openfeature.kotlin.sdk.EvaluationMetadata)" - "dev.openfeature.kotlin.sdk.ProviderEvaluation.constructor(kotlin.Any, kotlin.String?, kotlin.String?, dev.openfeature.kotlin.sdk.exceptions.ErrorCode?, kotlin.String?)" - "dev.openfeature.kotlin.sdk.ProviderEvaluation.constructor(kotlin.Any, kotlin.String?, kotlin.String?, dev.openfeature.kotlin.sdk.exceptions.ErrorCode?, kotlin.String?, dev.openfeature.kotlin.sdk.EvaluationMetadata)" - - "dev.openfeature.kotlin.sdk.ProviderEvaluation.constructor(dev.openfeature.kotlin.sdk.Value, kotlin.String?, kotlin.String?, dev.openfeature.kotlin.sdk.exceptions.ErrorCode?, kotlin.String?, dev.openfeature.kotlin.sdk.EvaluationMetadata)" - "dev.openfeature.kotlin.sdk.Value.Boolean.constructor(kotlin.Boolean)" - "dev.openfeature.kotlin.sdk.Value.Double.constructor(kotlin.Double)" - "dev.openfeature.kotlin.sdk.Value.Integer.constructor(kotlin.Int)" diff --git a/detekt_custom_unsafe_calls.yml b/detekt_custom_unsafe_calls.yml index 99a271f74d..92c9191217 100644 --- a/detekt_custom_unsafe_calls.yml +++ b/detekt_custom_unsafe_calls.yml @@ -10,6 +10,7 @@ datadog: - "android.content.pm.PackageManager.getPackageInfo(kotlin.String, android.content.pm.PackageManager.PackageInfoFlags):android.content.pm.PackageManager.NameNotFoundException" - "android.content.pm.PackageManager.getPackageInfo(kotlin.String, kotlin.Int):android.content.pm.PackageManager.NameNotFoundException" - "android.content.res.AssetManager.open(kotlin.String):java.io.IOException" + - "android.content.res.AssetManager.open(kotlin.String, kotlin.Int):java.io.IOException" - "android.content.res.Resources.getResourceEntryName(kotlin.Int):android.content.res.Resources.NotFoundException" - "android.content.res.Resources.getResourceName(kotlin.Int):android.content.res.Resources.NotFoundException" - "android.content.res.Resources.openRawResource(kotlin.Int):android.content.res.Resources.NotFoundException" @@ -47,15 +48,14 @@ datadog: - "android.view.MotionEvent.obtain(android.view.MotionEvent):java.lang.IllegalArgumentException" - "android.view.View.getGlobalVisibleRect(android.graphics.Rect?):java.lang.NullPointerException" - "android.view.ViewPropertyAnimator.setDuration(kotlin.Long):java.lang.IllegalArgumentException" - - "android.view.ViewTreeObserver.removeOnDrawListener(android.view.ViewTreeObserver.OnDrawListener?):java.lang.IllegalStateException" - "android.view.ViewTreeObserver.addOnDrawListener(android.view.ViewTreeObserver.OnDrawListener?):java.lang.IllegalStateException" + - "android.view.ViewTreeObserver.removeOnDrawListener(android.view.ViewTreeObserver.OnDrawListener?):java.lang.IllegalStateException" - "android.view.Window.addOnFrameMetricsAvailableListener(android.view.Window.OnFrameMetricsAvailableListener, android.os.Handler?):java.lang.IllegalStateException,java.lang.NullPointerException" - "android.view.Window.removeOnFrameMetricsAvailableListener(android.view.Window.OnFrameMetricsAvailableListener?):java.lang.IllegalArgumentException" - "android.widget.FrameLayout.addView(android.view.View, android.view.ViewGroup.LayoutParams):java.lang.IllegalArgumentException" - "android.widget.LinearLayout.addView(android.view.View):java.lang.IllegalArgumentException" - "androidx.collection.LruCache.constructor(kotlin.Int):java.lang.IllegalArgumentException" - - "androidx.collection.LruCache.get(java.io.File):java.lang.NullPointerException" - - "androidx.collection.LruCache.get(kotlin.String):java.lang.NullPointerException" + - "androidx.collection.LruCache.get(*):java.lang.NullPointerException" - "androidx.collection.LruCache.put(java.io.File, kotlin.Unit):java.lang.NullPointerException" - "androidx.collection.LruCache.remove(java.io.File):java.lang.NullPointerException" - "androidx.metrics.performance.JankStats.createAndTrack(android.view.Window, androidx.metrics.performance.JankStats.OnFrameListener):java.lang.IllegalStateException" @@ -110,11 +110,9 @@ datadog: # endregion # region Java Concurrency - "java.lang.Thread.constructor(java.lang.Runnable?, kotlin.String?):java.lang.NullPointerException,java.lang.SecurityException,java.lang.IllegalArgumentException" - - "java.lang.Thread.constructor(java.lang.Runnable, kotlin.String):java.lang.NullPointerException,java.lang.SecurityException,java.lang.IllegalArgumentException" - "java.lang.Thread.getAllStackTraces():java.lang.SecurityException" - "java.lang.Thread.interrupt():java.lang.SecurityException" - "java.lang.Thread.sleep(kotlin.Long):java.lang.IllegalArgumentException,java.lang.InterruptedException" - - "java.util.concurrent.BlockingQueue.drainTo(kotlin.collections.MutableCollection):java.lang.UnsupportedOperationException,java.lang.ClassCastException,java.lang.NullPointerException,java.lang.IllegalArgumentException" - "java.util.concurrent.BlockingQueue.drainTo(kotlin.collections.MutableCollection?):java.lang.UnsupportedOperationException,java.lang.ClassCastException,java.lang.NullPointerException,java.lang.IllegalArgumentException" - "java.util.concurrent.Callable.call():java.lang.Exception" - "java.util.concurrent.ConcurrentHashMap.computeIfAbsent(kotlin.String, java.util.function.Function):java.lang.NullPointerException,java.lang.IllegalStateException,java.lang.RuntimeException" @@ -139,7 +137,6 @@ datadog: - "java.util.concurrent.ScheduledThreadPoolExecutor.constructor(kotlin.Int):java.lang.IllegalArgumentException" - "java.util.concurrent.ScheduledThreadPoolExecutor.schedule(java.lang.Runnable, kotlin.Long, java.util.concurrent.TimeUnit):java.util.concurrent.RejectedExecutionException,java.lang.NullPointerException" - "java.util.concurrent.ThreadPoolExecutor.awaitTermination(kotlin.Long, java.util.concurrent.TimeUnit?):java.lang.InterruptedException" - - "java.util.concurrent.ThreadPoolExecutor.constructor(kotlin.Int, kotlin.Int, kotlin.Long, java.util.concurrent.TimeUnit, java.util.concurrent.BlockingQueue):java.lang.NullPointerException,java.lang.IllegalArgumentException" - "java.util.concurrent.ThreadPoolExecutor.constructor(kotlin.Int, kotlin.Int, kotlin.Long, java.util.concurrent.TimeUnit?, java.util.concurrent.BlockingQueue?):java.lang.NullPointerException,java.lang.IllegalArgumentException" - "java.util.concurrent.locks.Lock.lock():java.lang.InterruptedException" - "java.util.concurrent.locks.Lock.tryLock(kotlin.Long, java.util.concurrent.TimeUnit?):java.lang.InterruptedException,java.lang.NullPointerException" @@ -158,8 +155,7 @@ datadog: - "java.lang.StringBuilder.constructor(kotlin.Int):java.lang.NegativeArraySizeException" - "java.lang.System.arraycopy(kotlin.Any, kotlin.Int, kotlin.Any, kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException,java.lang.ArrayStoreException,java.lang.NullPointerException" - "java.lang.System.loadLibrary(kotlin.String?):java.lang.SecurityException,java.lang.UnsatisfiedLinkError,java.lang.NullPointerException" - - "java.lang.reflect.Field.get(kotlin.Any):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" - - "java.lang.reflect.Field.get(kotlin.Any?):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" + - "java.lang.reflect.Field.get(?):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" - "java.math.BigInteger.and(java.math.BigInteger?):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.constructor(kotlin.String?, kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.shiftRight(kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" @@ -174,8 +170,7 @@ datadog: # endregion # region Java Collections - "java.util.Collections.newSetFromMap(kotlin.collections.MutableMap?):java.lang.IllegalArgumentException" - - "java.util.LinkedList.add(kotlin.Int, android.view.View):java.lang.IndexOutOfBoundsException" - - "java.util.LinkedList.add(kotlin.Int, com.datadog.android.webview.internal.rum.domain.WebViewNativeRumViewsCache.ViewEntry):java.lang.IndexOutOfBoundsException" + - "java.util.LinkedList.add(kotlin.Int, *):java.lang.IndexOutOfBoundsException" - "java.util.LinkedList.offer(com.datadog.android.core.internal.data.upload.UploadWorker.UploadNextBatchTask):java.lang.ClassCastException,java.lang.NullPointerException,java.lang.IllegalArgumentException" - "java.util.LinkedList.removeFirst():java.util.NoSuchElementException" - "java.util.LinkedList.removeLast():java.util.NoSuchElementException" @@ -197,14 +192,7 @@ datadog: - "kotlin.String.substring(kotlin.Int):java.lang.IndexOutOfBoundsException" - "kotlin.String.substring(kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException" - "kotlin.String.takeLast(kotlin.Int):java.lang.IllegalArgumentException" - - "kotlin.String.takeLast(kotlin.Int):kotlin.IllegalArgumentException" - "kotlin.String.toLong():java.lang.NumberFormatException" - - "kotlin.collections.MutableSet.first():java.util.NoSuchElementException" - - "kotlin.String.format(kotlin.Array):java.util.IllegalFormatException" - - "kotlin.ByteArray.copyOf(kotlin.Int):java.lang.NegativeArraySizeException" - - "kotlin.ByteArray.copyOfRange(kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException,java.lang.IllegalArgumentException" - - "kotlin.ByteArray.get(kotlin.Int):java.lang.IndexOutOfBoundsException" - - "kotlin.Double.roundToLong():java.lang.IllegalArgumentException" # endregion # region Kotlin Collections - "kotlin.Array.first(kotlin.Function1):java.util.NoSuchElementException" @@ -221,8 +209,8 @@ datadog: # endregion # region Kotlin Coroutines - "kotlinx.coroutines.Deferred.await():java.util.concurrent.CancellationException" - - "kotlinx.coroutines.flow.Flow.collect(kotlinx.coroutines.flow.FlowCollector):java.lang.Exception" - "kotlinx.coroutines.flow.Flow.collect(kotlin.coroutines.SuspendFunction1):java.lang.Exception" + - "kotlinx.coroutines.flow.Flow.collect(kotlinx.coroutines.flow.FlowCollector):java.lang.Exception" - "kotlinx.coroutines.runBlocking(kotlin.coroutines.CoroutineContext, kotlin.coroutines.SuspendFunction1):java.lang.InterruptedException" - "kotlinx.coroutines.withContext(kotlin.coroutines.CoroutineContext, kotlin.coroutines.SuspendFunction1):kotlinx.coroutines.CancellationException" # endregion @@ -234,14 +222,12 @@ datadog: - "okhttp3.MultipartBody.Builder.build():java.lang.IllegalStateException" - "okhttp3.MultipartBody.Builder.setType(okhttp3.MediaType):java.lang.IllegalArgumentException" - "okhttp3.Request.Builder.build():java.lang.IllegalStateException" - - "okhttp3.Request.Builder.post(okhttp3.RequestBody):java.lang.NullPointerException,java.lang.IllegalArgumentException" - "okhttp3.Request.Builder.method(kotlin.String, okhttp3.RequestBody?):java.lang.NullPointerException,java.lang.IllegalArgumentException" - - "okhttp3.Request.Builder.tag(java.lang.Class, com.datadog.android.okhttp.internal.graphql.GraphQLAttributes?):java.lang.ClassCastException" - - "okhttp3.Request.Builder.tag(java.lang.Class, com.datadog.android.okhttp.TraceContext?):java.lang.ClassCastException" + - "okhttp3.Request.Builder.post(okhttp3.RequestBody):java.lang.NullPointerException,java.lang.IllegalArgumentException" + - "okhttp3.Request.Builder.tag(java.lang.Class, ?):java.lang.ClassCastException" - "okhttp3.Request.Builder.url(kotlin.String):java.lang.NullPointerException,java.lang.IllegalArgumentException" - "okhttp3.RequestBody.writeTo(okio.BufferedSink):java.io.IOException" - "okhttp3.Interceptor.Chain.proceed(okhttp3.Request):java.io.IOException" - - "okhttp3.RequestBody.writeTo(okio.BufferedSink):java.io.IOException" - "okhttp3.Response.close():java.lang.IllegalStateException" - "okhttp3.Response.peekBody(kotlin.Long):java.io.IOException,java.lang.IllegalArgumentException,java.lang.IllegalStateException" - "okhttp3.ResponseBody.string():java.io.IOException" @@ -251,8 +237,8 @@ datadog: - "okhttp3.OkHttpClient.Builder.eventListenerFactory(okhttp3.EventListener.Factory):java.lang.NullPointerException" - "okio.BufferedSink.close():java.io.IOException" - "okio.Okio.buffer(okio.Sink):java.lang.NullPointerException" - - "okio.Buffer.readString(java.nio.charset.Charset):java.lang.IllegalArgumentException,java.io.IOException" - "okio.Buffer.readByteArray():java.io.EOFException" + - "okio.Buffer.readString(java.nio.charset.Charset):java.lang.IllegalArgumentException,java.io.IOException" # endregion # region org.json - "org.json.JSONArray.get(kotlin.Int):org.json.JSONException" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/ContextExt.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/ContextExt.kt index b6dc042972..10d80a650c 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/ContextExt.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/ContextExt.kt @@ -27,6 +27,8 @@ import java.io.InputStream * * @return [InputStream] access to the asset data * + * @throws java.io.IOException if the asset cannot be opened (e.g. it does not exist). + * * @see [AssetManager.ACCESS_UNKNOWN] * @see [AssetManager.ACCESS_STREAMING] * @see [AssetManager.ACCESS_RANDOM] @@ -37,6 +39,8 @@ fun Context.getAssetAsRumResource( accessMode: Int = AssetManager.ACCESS_STREAMING, sdkCore: SdkCore = Datadog.getInstance() ): InputStream { + // Suppress this warning here to keep the public API backward compatible with the previous version. + @Suppress("UnsafeThirdPartyFunctionCall") return RumResourceInputStream( assets.open(fileName, accessMode), "assets://$fileName", diff --git a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCall.kt b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCall.kt index b2bb16e807..cd521b6427 100644 --- a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCall.kt +++ b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCall.kt @@ -6,8 +6,12 @@ package com.datadog.tools.detekt.rules.sdk -import com.datadog.tools.detekt.ext.fqTypeName import com.datadog.tools.detekt.rules.AbstractCallExpressionRule +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.CodeParser +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.CodeParser.KtMethodParameter +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.DetektConfigParser +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.DetektConfigValidator +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.SignatureRule import io.gitlab.arturbosch.detekt.api.CodeSmell import io.gitlab.arturbosch.detekt.api.Config import io.gitlab.arturbosch.detekt.api.Debt @@ -18,46 +22,40 @@ import io.gitlab.arturbosch.detekt.api.config import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtTryExpression -import org.jetbrains.kotlin.resolve.BindingContext import java.util.Stack /** - * This rule will report any call to a "third party" function that is considered unsafe, that is, - * which could throw an exception. + * Reports any call to a "third party" function that is considered unsafe (i.e. could throw an + * exception). Third party functions are detected based on an internal package prefix: any method + * with that prefix is treated as first party. * - * Third party functions are detected based on an internal package prefix: any method which has a - * package name with this prefix is considered first party, anything else is third party. + * The decision logic lives in this rule. Config-string parsing is delegated to [DetektConfigParser] + * and PSI-level extraction to [CodeParser]; + * both feed into [com.datadog.tools.detekt.rules.sdk.rule.thirdparty.SignatureRule] which is the + * single abstraction matching/validating either direct or wildcarded YAML records. */ @RequiresTypeResolution class UnsafeThirdPartyFunctionCall( config: Config ) : AbstractCallExpressionRule(config, includeTypeArguments = false) { + private val codeParser = CodeParser() + private val ruleParser = DetektConfigParser() + private val configValidator = DetektConfigValidator() private val internalPackagePrefix: String by config(defaultValue = "") private val treatUnknownFunctionAsThrowing: Boolean by config(defaultValue = true) - private val knownThrowingCalls: List by config(defaultValue = emptyList()) - private val knownSafeAndroidCalls: List by config(defaultValue = emptyList()) - private val knownSafeThirdPartyCalls: List by config(defaultValue = emptyList()) - - private val knownSafeCalls: Set by lazy { - (knownSafeAndroidCalls + knownSafeThirdPartyCalls).toSet() - } - - private val knownThrowingCallsMap: Map> by lazy { - knownThrowingCalls.map { - val splitColon = it.split(':') - val key = splitColon.first() - if (splitColon.size == 1) { - println("✘ ERROR WITH KNOWN THROWING CALL: $it") - } - val exceptions = splitColon[1].split(',').toList() - key to exceptions - }.toMap() - } - - private val caughtExceptions = Stack>() - - // region Rule + private val knownSafeAndroidCalls: List by config(emptyList(), ruleParser::parseSafeCalls) + private val knownSafeThirdPartyCalls: List by config(emptyList(), ruleParser::parseSafeCalls) + private val knownThrowingCalls: List by config(emptyList(), ruleParser::parseThrowingCalls) + private val knownSafeCalls: List by lazy { knownSafeAndroidCalls + knownSafeThirdPartyCalls } + private val caughtExceptions = Stack>() + + // Exact entries are looked up via O(1) map; wildcards fall through to list scan. + // Throwing category is always checked before safe category (preserves original semantics). + private val exactThrowingCalls: Map + private val wildcardThrowingCalls: List + private val exactSafeCalls: Map + private val wildcardSafeCalls: List override val issue: Issue = Issue( javaClass.simpleName, @@ -67,86 +65,88 @@ class UnsafeThirdPartyFunctionCall( Debt.TWENTY_MINS ) + init { + configValidator.validate(knownSafeCalls, knownThrowingCalls) + exactThrowingCalls = knownThrowingCalls.filter { it.canUseExactLookup }.associateBy { it.source } + wildcardThrowingCalls = knownThrowingCalls.filterNot { it.canUseExactLookup } + exactSafeCalls = knownSafeCalls.filter { it.canUseExactLookup }.associateBy { it.source } + wildcardSafeCalls = knownSafeCalls.filterNot { it.canUseExactLookup } + } + override fun visitTryExpression(expression: KtTryExpression) { - val caughtTypes = expression.catchClauses - .mapNotNull { - val typeReference = it.catchParameter?.typeReference - bindingContext.get(BindingContext.TYPE, typeReference)?.fqTypeName() - } - caughtExceptions.push(caughtTypes) + caughtExceptions.push(codeParser.parseCaughtTypes(expression, bindingContext)) super.visitTryExpression(expression) caughtExceptions.pop() } - // endregion - - // region AbstractCallExpressionRule - @Suppress("ReturnCount") override fun visitResolvedFunctionCall( expression: KtCallExpression, resolvedCall: ResolvedFunCall ) { - if (internalPackagePrefix.isNotEmpty()) { - val belongsToInternalContainer = resolvedCall.containerFqName.startsWith(internalPackagePrefix) || - resolvedCall.containingPackage.startsWith(internalPackagePrefix) - if (belongsToInternalContainer) return - } - if (resolvedCall.functionName in kotlinHelperMethods) { + if (resolvedCall.functionName in kotlinHelperMethods || + resolvedCall.isBelongsToInternalPrefix(internalPackagePrefix) + ) { return } - if (knownThrowingCallsMap.containsKey(resolvedCall.call)) { - val knownThrowables = knownThrowingCallsMap[resolvedCall.call] ?: emptyList() - checkCallThrowingExceptions(expression, resolvedCall.call, knownThrowables) - } else if (treatUnknownFunctionAsThrowing && !knownSafeCalls.contains(resolvedCall.call)) { - val message = "Calling ${resolvedCall.call} could throw exceptions, but this method is unknown" - reportUnsafeCall(expression, message) - } + val params = codeParser.parseFormalParams(expression, bindingContext) + classifyAndReport(expression, signature = resolvedCall.call, params = params) } - // endregion + @Suppress("ReturnCount") + private fun classifyAndReport(expression: KtCallExpression, signature: String, params: List) { + val throwingMatch = exactThrowingCalls[signature] ?: wildcardThrowingCalls.firstOrNull { it.matches(signature) } + if (throwingMatch != null) { + throwingMatch.validate(params) + checkCallThrowingExceptions(expression, signature, throwingMatch.exceptions) + return + } - // region Internal + val safeMatch = exactSafeCalls[signature] ?: wildcardSafeCalls.firstOrNull { it.matches(signature) } + if (safeMatch != null) { + safeMatch.validate(params) + return + } + + if (treatUnknownFunctionAsThrowing) { + reportUnsafeCall( + expression, + "Calling $signature could throw exceptions, but this method is unknown" + ) + } + } private fun checkCallThrowingExceptions( expression: KtCallExpression, call: String, exceptions: List ) { - val catchesAnyException = caughtExceptions.any { list -> - list.any { e -> e in topLevelExceptions } + val catchesAnyException = caughtExceptions.any { list -> list.any { e -> e in topLevelExceptions } } + val catchesAnyError = caughtExceptions.any { list -> list.any { e -> e in topLevelErrors } } + val uncaught = exceptions.filter { exception -> caughtExceptions.none { it.contains(exception) } }.filter { + val isUncaughtException = !catchesAnyException && it.endsWith("Exception") + val isUncaughtError = !catchesAnyError && it.endsWith("Error") + isUncaughtException || isUncaughtError } - val catchesAnyError = caughtExceptions.any { list -> - list.any { e -> e in topLevelErrors } - } - val uncaught = exceptions.filter { exception -> - caughtExceptions.none { it.contains(exception) } - } - .filter { - val isUncaughtException = it.endsWith("Exception") && !catchesAnyException - val isUncaughtError = it.endsWith("Error") && !catchesAnyError - isUncaughtException || isUncaughtError - } - if (uncaught.isEmpty()) { - return - } + if (uncaught.isEmpty()) return - val msg = "Calling $call can throw the following exceptions: ${exceptions.joinToString()}." - reportUnsafeCall(expression, msg) + reportUnsafeCall( + expression = expression, + message = "Calling $call can throw the following exceptions: ${uncaught.joinToString()}." + ) } - private fun reportUnsafeCall( - expression: KtCallExpression, - message: String - ) { - report(CodeSmell(issue, Entity.from(expression), message = message)) + private fun reportUnsafeCall(expression: KtCallExpression, message: String) { + report(CodeSmell(issue, Entity.from(expression), message = message + WILDCARD_CONFIG_RULES)) } - // endregion - companion object { + private const val WILDCARD_CONFIG_RULES = + " Config wildcard rules: '*' matches non-nullable generic, Any, or java.lang.Object parameters only; " + + "'?' matches both nullable and non-nullable generic, Any, or java.lang.Object parameters. " + + "'?' covers '*', but '*' does not cover nullable types." private const val JAVA_EXCEPTION_CLASS = "java.lang.Exception" private const val JAVA_ERROR_CLASS = "java.lang.Error" private const val JAVA_THROWABLE_CLASS = "java.lang.Throwable" @@ -172,5 +172,14 @@ class UnsafeThirdPartyFunctionCall( "let", "run", "with", "apply", "also", "print", "println", "toString", "invoke" ) + + private fun ResolvedFunCall.isBelongsToInternalPrefix( + internalPackagePrefix: String + ): Boolean { + val isInternal = + containerFqName.startsWith(internalPackagePrefix) || containingPackage.startsWith(internalPackagePrefix) + + return internalPackagePrefix.isNotEmpty() && isInternal + } } } diff --git a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/CodeParser.kt b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/CodeParser.kt new file mode 100644 index 0000000000..438d51418e --- /dev/null +++ b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/CodeParser.kt @@ -0,0 +1,179 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import com.datadog.tools.detekt.ext.fqTypeName +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtTryExpression +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall +import org.jetbrains.kotlin.types.KotlinType +import java.lang.reflect.Executable + +/** + * Extracts structured Kotlin PSI data needed by the rule: formal parameters of a call expression + * and the caught-exception types declared by a `try`/`catch` block. It also reflectively inspects + * the declared exceptions to a resolved JVM member. The rule owns all decision logic - this object + * only translates PSI nodes and JVM members into plain Kotlin values. + */ +internal class CodeParser { + + // Resolved-class memoization: many call signatures share the same declaring class, so we avoid + // re-running (and re-failing) Class.forName for every one of them. + private val resolvedClassesCache: MutableMap?> = mutableMapOf?>() + + /** + * Returns the formal parameters of the resolved function call as plain [KtMethodParameter] values, + * sorted by their declaration order. Returns an empty list if the call cannot be resolved. + * + * e.g. `fun MutableList.add(index: Int, element: T)` -> + * ``` + * [ + * KtMethodParameter(name="index", type="kotlin.Int", isGeneric=false), + * KtMethodParameter(name="element", type="T", isGeneric=true) + * ] + * ``` + * + * Used by [SignatureRule.validate] to enforce that wildcard slots only target generic + * type parameters, not concrete types. + */ + internal fun parseFormalParams( + expression: KtCallExpression, + bindingContext: BindingContext + ): List = expression.getResolvedCall(bindingContext) + ?.valueArguments?.keys + ?.sortedBy { it.index } + ?.map(::toKtMethodParameter) + .orEmpty() + + /** + * Returns the fully-qualified exception type names declared in all `catch` clauses of [expression]. + * e.g. `try { } catch (e: IOException) { }` -> `{"java.io.IOException"}`. + * + * Used by the rule to determine whether a known-throwing call's exceptions are already handled. + */ + internal fun parseCaughtTypes( + expression: KtTryExpression, + bindingContext: BindingContext + ): Set = expression.catchClauses + .mapNotNull { bindingContext.get(BindingContext.TYPE, it.catchParameter?.typeReference)?.fqTypeName() } + .toSet() + + /** + * Returns checked exceptions declared by the JVM method or constructor. + * + * Examples: + * - `java.io.BufferedWriter.write(1 param)` -> `{"java.io.IOException"}` + * - `java.io.RandomAccessFile.constructor(2 params)` -> `{"java.io.FileNotFoundException"}` + * - unknown classes, extension functions, or members without checked exceptions -> `emptySet()` + * + * A name plus parameter count can match several overloads. This returns exceptions only when + * every matching overload declares checked exceptions. For example, an overload set containing + * both a throwing `put(double)` and a non-throwing `put(Object)` returns `emptySet()`. + */ + internal fun parseDeclaredCheckedExceptions( + className: String, + memberName: String, + parameterCount: Int + ): Set { + if (className.isEmpty() || memberName.isEmpty()) return emptySet() + + return resolvedClassesCache.loadClassOrNull(className) + ?.getMethodsMatchingSignature(memberName, parameterCount) + ?.extractCheckedExceptionsSet() + .orEmpty() + } + + internal data class KtMethodParameter( + val name: String, + val type: String, + val isGeneric: Boolean + ) + + private companion object { + private const val CONSTRUCTOR_MEMBER = "constructor" + + // Upper bound on dotted segments in a call's class name. Real fully-qualified names stay + // well under this; exceeding it signals a malformed signature rather than a deep nesting. + private const val MAX_NESTED_CLASS_DEPTH = 15 + + private fun MutableMap?>.loadClassOrNull(className: String): Class<*>? = getOrPut(className) { + className + .computeClassNameCandidates() + .firstNotNullOfOrNull { + runCatching { Class.forName(it, false, javaClass.classLoader) }.getOrNull() + } + } + + private fun Class<*>.getMethodsMatchingSignature( + memberName: String, + parameterCount: Int + ): List { + val members = if (memberName == CONSTRUCTOR_MEMBER) { + declaredConstructors.filter { it.parameterCount == parameterCount } + } else { + methods.filter { it.name == memberName && it.parameterCount == parameterCount } + } + + // Skip compiler-synthesized bridge methods: a class overriding an interface method + // (e.g. StringBuilder over Appendable) gets a bridge that carries the *interface's* + // throws clause, which would otherwise yield false positives. + return members.filterNot(Executable::isSynthetic) + } + + private fun List.extractCheckedExceptionsSet(): Set { + val checkedExceptions = map { it.extractCheckedExceptionNames() } + + // A name plus arity can match multiple overloads. If any candidate has no checked + // exceptions, the configured member is not guaranteed to throw. + if (checkedExceptions.any(Set::isEmpty)) return emptySet() + + return checkedExceptions.flatten().toSet() + } + + private fun Executable.extractCheckedExceptionNames(): Set = exceptionTypes + .filter(::isCheckedException) + .mapTo(mutableSetOf(), Class<*>::getName) + + /** + * The candidate JVM class names for this dotted class name, from the name as-is to progressively + * reinterpreting trailing dotted segments as nested classes, so JVM nested types resolve too: + * `a.b.Outer.Inner` -> `[a.b.Outer.Inner, a.b.Outer$Inner, a.b$Outer$Inner]`. + */ + private fun String.computeClassNameCandidates(): List { + check(this.count { it == '.' } <= MAX_NESTED_CLASS_DEPTH) { + "Class name '$this' is nested too deeply (limit $MAX_NESTED_CLASS_DEPTH) to resolve reflectively" + } + val candidates = mutableListOf(this) + var current = this + while (current.contains('.')) { + current = current.substringBeforeLast('.') + '$' + current.substringAfterLast('.') + candidates += current + } + return candidates + } + + private fun toKtMethodParameter(valueParameter: ValueParameterDescriptor) = KtMethodParameter( + name = valueParameter.name.toString(), + type = valueParameter.type.toString(), + isGeneric = containsGenericParameter(valueParameter.original.type) + ) + + // Detects both direct generic parameters (`T`) and nested ones (`List`, `Map`). + private fun containsGenericParameter(type: KotlinType): Boolean { + if (type.constructor.declarationDescriptor is TypeParameterDescriptor) return true + return type.arguments.any { !it.isStarProjection && containsGenericParameter(it.type) } + } + + private fun isCheckedException(exceptionClass: Class<*>): Boolean = + Throwable::class.java.isAssignableFrom(exceptionClass) && + !RuntimeException::class.java.isAssignableFrom(exceptionClass) && + !Error::class.java.isAssignableFrom(exceptionClass) + } +} diff --git a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigParser.kt b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigParser.kt new file mode 100644 index 0000000000..50065231e0 --- /dev/null +++ b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigParser.kt @@ -0,0 +1,144 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.DetektConfigParser.Companion.NON_NULL_WILDCARD +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.DetektConfigParser.Companion.parseEntry + +/** + * Parses raw YAML config strings into [SignatureRule] instances. + * + * Two entry formats are supported: + * - Safe calls: `"java.io.File.readText(kotlin.String)"` - no exception list + * - Throwing calls: `"java.io.File.readText(kotlin.String):java.io.IOException,kotlin.Exception"` - + * method signature followed by `:` and a comma-separated list of thrown exception types + * + * In safe-call config, concrete nullable slots, e.g. `kotlin.String?`, match both nullable and + * non-nullable resolved calls for the same type. Throwing-call config keeps nullable concrete + * slots exact. Bare wildcard slots (`*` for non-nullable, `?` for any type) are resolved by + * [parseEntry] into regex patterns and recorded as [SignatureRule.WildcardSlot] entries. + */ +internal class DetektConfigParser { + + fun parseSafeCalls(entries: List): List = + entries.map { parseEntry(it, nullableConcreteMatchesNonNullable = true) } + + fun parseThrowingCalls(entries: List): List = entries.map { + val parts = it.split(COLON_SEPARATOR) + val exceptions = parts.getOrNull(1) + ?.split(COMMA_SEPARATOR) + ?.map { exception -> exception.trim() } + ?: error("ERROR WITH KNOWN THROWING CALL: $it") + + parseEntry( + source = parts.first(), + exceptions = exceptions, + nullableConcreteMatchesNonNullable = false + ) + } + + internal companion object { + private const val COLON_SEPARATOR = ':' + private const val COMMA_SEPARATOR = "," + private const val NON_NULL_WILDCARD = "*" + private const val NULLABLE_WILDCARD = "?" + + /** + * Matches any non-nullable type at a wildcard position. + * e.g. `kotlin.String` matches, `kotlin.String?` does not. + */ + private const val NON_NULL_WILDCARD_PATTERN = "[^,)?]+" + + /** + * Matches any type at a wildcard position. + * e.g. both `kotlin.String` and `kotlin.String?` match. + */ + private const val NULLABLE_WILDCARD_PATTERN = "[^,)]+" + + /** + * Parses a config entry string into a [SignatureRule]. + * + * The [source] string is split into a method part and comma-separated param slots. Each slot + * becomes either a literal (regex-escaped), a nullable literal pattern, or a wildcard pattern. + * Wildcard positions are recorded in `wildcardSlots` so wildcard rules can be separated from + * exact signatures. + * + * Example - exact signature, no wildcards: + * ``` + * source : "java.io.File.readText(kotlin.String)" + * paramSlots : ["kotlin.String"] + * wildcardSlots: [] + * regex : ^java\.io\.File\.readText\(kotlin\.String\)$ + * ``` + * + * Example - [NON_NULL_WILDCARD] (`*`) matches any non-nullable type at that position: + * ``` + * source : "java.io.File.listFiles(*)" + * paramSlots : ["*"] + * wildcardSlots: [WildcardSlot(index=0, symbol="*")] + * regex : ^java\.io\.File\.listFiles\([^,)?]+\)$ + * ``` + * + * Example - mixed wildcards: + * ``` + * source : "kotlin.collections.MutableMap.put(*, ?)" + * paramSlots : ["*", "?"] + * wildcardSlots: [WildcardSlot(index=0, symbol="*"), WildcardSlot(index=1, symbol="?")] + * regex : ^kotlin\.collections\.MutableMap\.put\([^,)?]+, [^,)]+\)$ + * ``` + */ + internal fun parseEntry( + source: String, + exceptions: List = emptyList(), + nullableConcreteMatchesNonNullable: Boolean = true + ): SignatureRule { + val paramSlots = source.substringAfter('(') + .dropLast(1) + .takeIf { it.isNotEmpty() }?.split(",")?.map { it.trim() } + .orEmpty() + + val paramsPattern = buildParamsPattern(paramSlots, nullableConcreteMatchesNonNullable) + val wildcardSlots = parseWildcardSlots(paramSlots) + return SignatureRule( + source, + Regex("^${Regex.escape(source.substringBefore('(') + '(')}$paramsPattern\\)$"), + paramSlots, + wildcardSlots, + nullableConcreteMatchesNonNullable, + exceptions + ) + } + + private fun buildParamsPattern( + paramSlots: List, + nullableConcreteMatchesNonNullable: Boolean + ): String = + paramSlots.joinToString(", ") { param -> + when (param) { + NON_NULL_WILDCARD -> NON_NULL_WILDCARD_PATTERN + NULLABLE_WILDCARD -> NULLABLE_WILDCARD_PATTERN + else -> param.toConcreteParamPattern(nullableConcreteMatchesNonNullable) + } + } + + private fun String.toConcreteParamPattern(nullableConcreteMatchesNonNullable: Boolean): String = + if (nullableConcreteMatchesNonNullable && endsWith(NULLABLE_WILDCARD)) { + Regex.escape(removeSuffix(NULLABLE_WILDCARD)) + "\\??" + } else { + Regex.escape(this) + } + + private fun parseWildcardSlots(paramSlots: List): List = + paramSlots.mapIndexedNotNull { index, param -> + when (param) { + NON_NULL_WILDCARD -> SignatureRule.WildcardSlot(index, NON_NULL_WILDCARD) + NULLABLE_WILDCARD -> SignatureRule.WildcardSlot(index, NULLABLE_WILDCARD) + else -> null + } + } + } +} diff --git a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigValidator.kt b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigValidator.kt new file mode 100644 index 0000000000..fafe8f6d8d --- /dev/null +++ b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigValidator.kt @@ -0,0 +1,122 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +/** + * Validates parsed UnsafeThirdPartyFunctionCall config entries. + */ +internal class DetektConfigValidator( + private val codeParser: CodeParser = CodeParser() +) { + + fun validate( + knownSafeCalls: List, + knownThrowingCalls: List + ) { + val conflicts = buildList { + addAll(validateNoDuplicationWithinSameFile("knownSafeCalls", knownSafeCalls)) + addAll(validateNoDuplicationWithinSameFile("knownThrowingCalls", knownThrowingCalls)) + addAll(validateSortedWithinClasses("knownSafeCalls", knownSafeCalls)) + addAll(validateSortedWithinClasses("knownThrowingCalls", knownThrowingCalls)) + addAll(validateThrowingCallIsNotInSafeCallsConfig(knownSafeCalls)) + addAll(validateNoConflictsBetweenSafeAndThrowingRules(knownSafeCalls, knownThrowingCalls)) + } + if (conflicts.isNotEmpty()) { + error( + "Found ${conflicts.size} issue(s) in detekt config:\n" + + conflicts.joinToString("\n") + ) + } + } + + private fun validateSortedWithinClasses( + configName: String, + patterns: List + ) = buildList { + for (i in 1 until patterns.size) { + val prev = patterns[i - 1] + val curr = patterns[i] + val prevClass = prev.source.substringBefore('(').substringBeforeLast('.') + val currClass = curr.source.substringBefore('(').substringBeforeLast('.') + if (prevClass == currClass && prev.source > curr.source) { + add( + " - $configName entries for '$currClass' are not sorted: " + + "'${curr.source}' should come before '${prev.source}'" + ) + } + } + } + + private fun validateNoConflictsBetweenSafeAndThrowingRules( + safePatterns: List, + throwingPatterns: List + ) = buildList { + val throwingByKey = throwingPatterns.groupBy { it.overlapKey } + safePatterns.forEach { safe -> + throwingByKey[safe.overlapKey].orEmpty().forEach { throwing -> + if (safe.intersects(throwing)) { + add( + " - '${safe.source}' (knownSafeCalls) overlaps with " + + "'${throwing.source}' (knownThrowingCalls)" + ) + } + } + } + } + + private fun validateNoDuplicationWithinSameFile( + configName: String, + patterns: List + ) = buildList { + patterns.groupBy { it.overlapKey }.values.forEach { matchingPatterns -> + for (i in matchingPatterns.indices) { + for (j in i + 1 until matchingPatterns.size) { + if (matchingPatterns[i].intersects(matchingPatterns[j])) { + add( + " - '${matchingPatterns[i].source}' duplicates " + + "'${matchingPatterns[j].source}' in $configName" + ) + } + } + } + } + } + + /** + * Protects `knownSafeCalls` from methods that declare a checked exception. + * + * A method that can throw a checked exception is, by definition, not safe. Listing it under + * `knownSafeCalls` would silence a genuine unsafe-call warning. This check is independent of + * `knownThrowingCalls`: it resolves the *actual* JVM method/constructor behind each safe entry + * via reflection and inspects its declared exception types, so it catches a dangerous method + * even when it was never added to `knownThrowingCalls` (and regardless of whether the offending + * entry spelled out an `:Exception` suffix). + * + * It is best-effort: entries whose declaring class is not on the rule's classpath (most + * `android.*`, `androidx.*` and third-party types) or that resolve to Kotlin extension functions + * cannot be inspected and are silently skipped. Only resolvable JVM members are validated. + */ + private fun validateThrowingCallIsNotInSafeCallsConfig( + safePatterns: List + ) = buildList { + safePatterns.forEach { safe -> + val memberReference = safe.memberReference + val checkedExceptions = codeParser.parseDeclaredCheckedExceptions( + className = memberReference.className, + memberName = memberReference.memberName, + parameterCount = memberReference.parameterCount + ) + if (checkedExceptions.isNotEmpty()) { + add( + " - '${safe.source}' (knownSafeCalls) resolves to a member declaring checked " + + "exception(s) ${checkedExceptions.sorted().joinToString()}; methods that can throw " + + "checked exceptions are not safe and must not be listed in knownSafeCalls" + ) + } + } + } +} diff --git a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/SignatureRule.kt b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/SignatureRule.kt new file mode 100644 index 0000000000..b47dcee885 --- /dev/null +++ b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/SignatureRule.kt @@ -0,0 +1,133 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.CodeParser.KtMethodParameter + +/** + * A single matchable rule for a method signature from a detekt config entry. + * + * Handles exact entries, entries with nullable concrete slots, and entries with `*` / `?` + * wildcards. A concrete nullable slot, e.g. `kotlin.String?`, matches both nullable and + * non-nullable resolved calls for the same concrete type. Bare wildcard slots are recorded in + * [wildcardSlots] and are only valid against generic formal parameters. + */ +internal class SignatureRule( + val source: String, + val regex: Regex, + private val paramSlots: List, + private val wildcardSlots: List, + private val nullableConcreteMatchesNonNullable: Boolean, + val exceptions: List = emptyList() +) { + + private val methodPart: String = source.substringBefore('(') + internal val overlapKey: OverlapKey = OverlapKey(methodPart, paramSlots.size) + internal val memberReference: MemberReference = MemberReference( + className = methodPart.substringBeforeLast('.', missingDelimiterValue = ""), + memberName = methodPart.substringAfterLast('.'), + parameterCount = paramSlots.size + ) + + /** True when this rule can be matched with a plain map lookup. */ + internal val canUseExactLookup: Boolean = + wildcardSlots.isEmpty() && (!nullableConcreteMatchesNonNullable || paramSlots.none { it.isConcreteNullable() }) + + /** Returns true if this rule's pattern matches the given fully-qualified [signature] string. */ + fun matches(signature: String): Boolean = + if (canUseExactLookup) source == signature else regex.matches(signature) + + /** + * Validates that every bare wildcard slot in this rule targets a generic type parameter at the + * call site described by [ktMethodParameters]. + */ + fun validate(ktMethodParameters: List) { + wildcardSlots.forEach { slot -> + val formalParam = ktMethodParameters.getOrNull(slot.index) ?: return@forEach + if (!formalParam.isGeneric && !formalParam.isAnyType()) { + error( + "Wildcard '${slot.symbol}' in pattern '$source' targets non-generic " + + "parameter '${formalParam.name}: ${formalParam.type}'. " + + "Wildcards are only valid for generic, Any, or java.lang.Object parameters." + ) + } + } + } + + /** + * Returns true if there exists at least one resolved call signature that BOTH this pattern + * and [other] would match. Wildcards are intersected per-slot: `*` matches non-nullable + * literals only, `?` matches any literal (nullable or non-nullable), so `?` subsumes `*`. + */ + fun intersects(other: SignatureRule): Boolean = + methodPart == other.methodPart && + paramSlots.size == other.paramSlots.size && + paramSlots.zip(other.paramSlots).all { (a, b) -> + slotsMatches( + a, + b, + aNullableMatchesNonNullable = nullableConcreteMatchesNonNullable, + bNullableMatchesNonNullable = other.nullableConcreteMatchesNonNullable + ) + } + + internal data class WildcardSlot(val index: Int, val symbol: String) + + internal data class OverlapKey(val methodPart: String, val paramCount: Int) + + internal data class MemberReference(val className: String, val memberName: String, val parameterCount: Int) + + private companion object { + + private fun slotsMatches( + a: String, + b: String, + aNullableMatchesNonNullable: Boolean, + bNullableMatchesNonNullable: Boolean + ): Boolean { + val aWild = a == NON_NULL_WILDCARD || a == NULLABLE_WILDCARD + val bWild = b == NON_NULL_WILDCARD || b == NULLABLE_WILDCARD + return when { + aWild && bWild -> true + aWild -> b.matchesWildcard(wildcard = a) + bWild -> a.matchesWildcard(wildcard = b) + a == b -> true + a.baseType() != b.baseType() -> false + a.isConcreteNullable() -> aNullableMatchesNonNullable + b.isConcreteNullable() -> bNullableMatchesNonNullable + else -> false + } + } + + private fun String.matchesWildcard( + wildcard: String + ) = wildcard != NON_NULL_WILDCARD || !endsWith("?") + + private fun String.baseType(): String = removeSuffix("?") + + private fun String.isConcreteNullable(): Boolean = + this != NULLABLE_WILDCARD && endsWith("?") + + private fun KtMethodParameter.isAnyType(): Boolean { + // Kotlin can render Java Object/Any parameters in several forms: + // - `?` marks a nullable type, e.g. `kotlin.Any?`. + // - `!` marks a Java platform type, e.g. `java.lang.Object!`. + // - `..` separates flexible bounds, e.g. `(Any..Any?)`. + val normalizedType = type + .substringAfterLast("(") + .substringBefore("..") + .removeSuffix("?") + .removeSuffix("!") + + return normalizedType in ANY_PARAMETER_TYPES + } + + private const val NON_NULL_WILDCARD = "*" + private const val NULLABLE_WILDCARD = "?" + private val ANY_PARAMETER_TYPES = setOf("Any", "kotlin.Any", "Object", "java.lang.Object") + } +} diff --git a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCallTest.kt b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCallTest.kt index b2ea325a08..f4ea5e4449 100644 --- a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCallTest.kt +++ b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/UnsafeThirdPartyFunctionCallTest.kt @@ -16,9 +16,12 @@ import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import org.assertj.core.api.Assertions.assertThat as assertjThat @ExtendWith(ForgeExtension::class) +@Suppress("LargeClass") internal class UnsafeThirdPartyFunctionCallTest { lateinit var kotlinEnv: KotlinCoreEnvironmentWrapper @@ -78,6 +81,19 @@ internal class UnsafeThirdPartyFunctionCallTest { assertThat(findings).hasSize(1) } + @Test + fun `M throw exception W init {safe call declares checked exception}`() { + // Given + val config = TestConfig( + "knownSafeThirdPartyCalls" to listOf("java.io.BufferedWriter.write(kotlin.String)") + ) + + // When / Then + assertThrows { + UnsafeThirdPartyFunctionCall(config) + } + } + @Test fun `ignore call on internal package extension function`() { // Given @@ -172,7 +188,6 @@ internal class UnsafeThirdPartyFunctionCallTest { @Test fun `detekt unsafe call on unknown third party function { explicit it receiver }`() { // Given - @Suppress("SimpleRedundantLet") val code = """ import java.io.File @@ -249,6 +264,38 @@ internal class UnsafeThirdPartyFunctionCallTest { assertThat(findings).hasSize(1) } + @Test + fun `M report only uncaught exceptions W known throwing call catches one configured exception`() { + // Given + val knownThrowingCalls = listOf( + "java.io.File.inputStream():java.io.FileNotFoundException,java.io.IOException" + ) + val config = TestConfig("knownThrowingCalls" to knownThrowingCalls) + val code = + """ + import java.io.File + import java.io.FileNotFoundException + + fun test(f: File): Any? { + try { + return f.inputStream() + } catch (e: FileNotFoundException) { + return null + } + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + assertjThat(findings.first().message) + .contains("java.io.IOException") + .doesNotContain("java.io.FileNotFoundException") + } + @Test fun `detekt call with catch on unknown third party function`() { // Given @@ -641,4 +688,278 @@ internal class UnsafeThirdPartyFunctionCallTest { // Then assertThat(findings).hasSize(0) } + + @Test + fun `M report finding W knownThrowingCalls wildcard matches call {first concrete type for wildcard param}`() { + // Given + val knownThrowingCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.Int, *):java.lang.UnsupportedOperationException" + ) + val config = TestConfig("knownThrowingCalls" to knownThrowingCalls) + val code = + """ + fun test(list: MutableList) { + list.add(0, "hello") + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + } + + @Test + fun `M report finding W knownThrowingCalls wildcard matches call {different concrete type for wildcard param}`() { + // Given + val knownThrowingCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.Int, *):java.lang.UnsupportedOperationException" + ) + val config = TestConfig("knownThrowingCalls" to knownThrowingCalls) + val code = + """ + import java.io.File + fun test(list: MutableList, file: File) { + list.add(0, file) + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + } + + @Test + fun `M not report finding W knownThrowingCalls wildcard pattern arity differs from call`() { + // Given + val knownThrowingCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.Int, *):java.lang.UnsupportedOperationException" + ) + val config = TestConfig( + "knownThrowingCalls" to knownThrowingCalls, + "treatUnknownFunctionAsThrowing" to false + ) + val code = + """ + fun test(list: MutableList) { + list.add("hello") + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(0) + } + + @Test + fun `M report finding W knownThrowingCalls has multiple wildcard params matching call`() { + // Given + val knownThrowingCalls = listOf( + "kotlin.collections.MutableMap.put(*, ?):java.lang.UnsupportedOperationException" + ) + val config = TestConfig("knownThrowingCalls" to knownThrowingCalls) + val code = + """ + fun test(map: MutableMap, value: Int?) { + map.put("key", value) + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + } + + @Test + fun `M report finding W knownThrowingCalls wildcard targets Any parameter`() { + // Given + val knownThrowingCalls = listOf( + "ThirdParty.call(?):java.lang.RuntimeException" + ) + val config = TestConfig("knownThrowingCalls" to knownThrowingCalls) + val code = + """ + class ThirdParty { + fun call(value: Any?) { + } + } + + fun test(thirdParty: ThirdParty, value: Any?) { + thirdParty.call(value) + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + } + + @Test + fun `M not report finding W knownSafeCalls wildcard matches call`() { + // Given + val knownSafeCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.Int, *)" + ) + val config = TestConfig("knownSafeThirdPartyCalls" to knownSafeCalls) + val code = + """ + fun test(list: MutableList) { + list.add(0, "hello") + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(0) + } + + @Test + fun `M not report finding W knownSafeCalls nullable concrete pattern matches non-nullable call`() { + // Given + val knownSafeCalls = listOf( + "java.io.File.listFiles(java.io.FileFilter?)" + ) + val config = TestConfig("knownSafeThirdPartyCalls" to knownSafeCalls) + val code = + """ + import java.io.File + import java.io.FileFilter + + fun test(file: File, filter: FileFilter) { + file.listFiles(filter) + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(0) + } + + @Test + fun `M report finding W knownSafeCalls non-nullable concrete pattern misses nullable call`() { + // Given + val knownSafeCalls = listOf( + "java.io.File.listFiles(java.io.FileFilter)" + ) + val config = TestConfig( + "knownSafeThirdPartyCalls" to knownSafeCalls + ) + val code = + """ + import java.io.File + import java.io.FileFilter + + fun test(file: File, filter: FileFilter?) { + file.listFiles(filter) + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + assertjThat(findings.first().message) + .contains("Config wildcard rules") + .contains("'?' matches both nullable and non-nullable") + .contains("'?' covers '*', but '*' does not cover nullable types.") + } + + @Test + fun `M not report finding W throwing nullable concrete pattern exists for safe non-nullable call`() { + // Given + val config = TestConfig( + "knownSafeThirdPartyCalls" to listOf( + "java.util.concurrent.ConcurrentHashMap.remove(kotlin.String)" + ), + "knownThrowingCalls" to listOf( + "java.util.concurrent.ConcurrentHashMap.remove(kotlin.String?):java.lang.NullPointerException" + ), + "treatUnknownFunctionAsThrowing" to false + ) + val code = + """ + import java.util.concurrent.ConcurrentHashMap + + fun test(map: ConcurrentHashMap, key: String) { + map.remove(key) + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(0) + } + + @Test + fun `M report finding W knownSafeCalls wildcard pattern arity differs from call`() { + // Given + val knownSafeCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.Int, *)" + ) + val config = TestConfig("knownSafeThirdPartyCalls" to knownSafeCalls) + val code = + """ + fun test(list: MutableList) { + list.add("hello") + } + """.trimIndent() + + // When + val findings = UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + } + + @Test + fun `M throw exception W wildcard used for non-generic parameter`() { + // Given + val knownThrowingCalls = listOf( + "kotlin.collections.MutableList.add(*, *):java.lang.UnsupportedOperationException" + ) + val config = TestConfig("knownThrowingCalls" to knownThrowingCalls) + val code = + """ + fun test(list: MutableList) { + list.add(0, "hello") + } + """.trimIndent() + + // When + val error = assertThrows { + UnsafeThirdPartyFunctionCall(config) + .compileAndLintWithContext(kotlinEnv.env, code) + } + + // Then + assertjThat(error).hasMessageContaining( + "Wildcards are only valid for generic, Any, or java.lang.Object parameters." + ) + } } diff --git a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/CodeParserTest.kt b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/CodeParserTest.kt new file mode 100644 index 0000000000..a15f9d4862 --- /dev/null +++ b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/CodeParserTest.kt @@ -0,0 +1,112 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +internal class CodeParserTest { + + private val testedParser = CodeParser() + + @Test + fun `M return checked exception W parseDeclaredCheckedExceptions() {method declares checked}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "java.io.BufferedWriter", + memberName = "write", + parameterCount = 1 + ) + + assertThat(result).contains("java.io.IOException") + } + + @Test + fun `M return checked exception W parseDeclaredCheckedExceptions() {no-arg method declares checked}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "java.io.InputStream", + memberName = "read", + parameterCount = 0 + ) + + assertThat(result).contains("java.io.IOException") + } + + @Test + fun `M return checked exception W parseDeclaredCheckedExceptions() {constructor declares checked}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "java.io.RandomAccessFile", + memberName = "constructor", + parameterCount = 2 + ) + + assertThat(result).contains("java.io.FileNotFoundException") + } + + @Test + fun `M return empty W parseDeclaredCheckedExceptions() {only unchecked exceptions}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "java.lang.StringBuilder", + memberName = "append", + parameterCount = 1 + ) + + assertThat(result).isEmpty() + } + + @Test + fun `M return empty W parseDeclaredCheckedExceptions() {overload set has a non-throwing member}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "java.io.FileInputStream", + memberName = "constructor", + parameterCount = 1 + ) + + assertThat(result).isEmpty() + } + + @Test + fun `M return empty W parseDeclaredCheckedExceptions() {class not on classpath}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "com.example.unknown.Mystery", + memberName = "doStuff", + parameterCount = 1 + ) + + assertThat(result).isEmpty() + } + + @Test + fun `M return empty W parseDeclaredCheckedExceptions() {no member matches arity}`() { + val result = testedParser.parseDeclaredCheckedExceptions( + className = "java.io.InputStream", + memberName = "read", + parameterCount = 9 + ) + + assertThat(result).isEmpty() + } + + @Test + fun `M return empty W parseDeclaredCheckedExceptions() {blank class or member}`() { + assertThat(testedParser.parseDeclaredCheckedExceptions("", "read", 0)).isEmpty() + assertThat(testedParser.parseDeclaredCheckedExceptions("java.io.InputStream", "", 0)).isEmpty() + } + + @Test + fun `M throw exception W parseDeclaredCheckedExceptions() {class name nested too deeply}`() { + val tooDeep = (0..16).joinToString(".") { "a" } + + assertThrows { + testedParser.parseDeclaredCheckedExceptions( + className = tooDeep, + memberName = "foo", + parameterCount = 0 + ) + } + } +} diff --git a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigParserTest.kt b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigParserTest.kt new file mode 100644 index 0000000000..b0a5c4a3f8 --- /dev/null +++ b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigParserTest.kt @@ -0,0 +1,288 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +internal class DetektConfigParserTest { + + // region No wildcard — exact match + + @Test + fun `M match exact call W signaturePatternOf() {no wildcard}`() { + val pattern = "java.io.File.inputStream()".toRegex() + + assertThat(pattern.matches("java.io.File.inputStream()")).isTrue() + } + + @Test + fun `M not match different call W signaturePatternOf() {no wildcard}`() { + val pattern = "java.io.File.inputStream()".toRegex() + + assertThat(pattern.matches("java.io.File.outputStream()")).isFalse() + } + + @Test + fun `M not match partial call W signaturePatternOf() {dots not treated as regex wildcards}`() { + // dots in package names must be escaped so they don't act as regex "any char" + val pattern = "java.io.File.inputStream()".toRegex() + + assertThat(pattern.matches("javaXioXFileXinputStream()")).isFalse() + } + + @Test + fun `M match call with params W signaturePatternOf() {no wildcard, exact params}`() { + val pattern = "java.io.File.listFiles(java.io.FileFilter?)".toRegex() + + assertThat(pattern.matches("java.io.File.listFiles(java.io.FileFilter?)")).isTrue() + assertThat(pattern.matches("java.io.File.listFiles(java.io.FileFilter)")).isTrue() + assertThat(pattern.matches("java.io.File.listFiles(java.io.FilenameFilter?)")).isFalse() + } + + @Test + fun `M not match nullable call W signaturePatternOf() {non-nullable concrete param}`() { + val pattern = "java.io.File.listFiles(java.io.FileFilter)".toRegex() + + assertThat(pattern.matches("java.io.File.listFiles(java.io.FileFilter?)")).isFalse() + } + + @Test + fun `M not match non-nullable call W signaturePatternOf() {throwing nullable concrete param}`() { + val pattern = DetektConfigParser() + .parseThrowingCalls( + listOf("java.util.concurrent.ConcurrentHashMap.remove(kotlin.String?):java.lang.NullPointerException") + ) + .first() + .regex + + assertThat(pattern.matches("java.util.concurrent.ConcurrentHashMap.remove(kotlin.String)")).isFalse() + } + + // endregion + + // region NON_NULL_WILDCARD — non-nullable types only + + @ParameterizedTest + @MethodSource("provideNonNullableWildcardMatches") + fun `M match call W signaturePatternOf() {NON_NULL_WILDCARD matches non-nullable param type}`( + pattern: String, + call: String + ) { + assertThat(pattern.toRegex().matches(call)).isTrue() + } + + @ParameterizedTest + @MethodSource("provideNonNullableWildcardNonMatches") + fun `M not match call W signaturePatternOf() {NON_NULL_WILDCARD does not match nullable or wrong arity}`( + pattern: String, + call: String + ) { + assertThat(pattern.toRegex().matches(call)).isFalse() + } + + // endregion + + // region NULLABLE_WILDCARD — any types (nullable and non-nullable) + + @ParameterizedTest + @MethodSource("provideNullableWildcardMatches") + fun `M match call W signaturePatternOf() {NULLABLE_WILDCARD matches any param type}`( + pattern: String, + call: String + ) { + assertThat(pattern.toRegex().matches(call)).isTrue() + } + + @ParameterizedTest + @MethodSource("provideNullableWildcardNonMatches") + fun `M not match call W signaturePatternOf() {NULLABLE_WILDCARD does not match wrong arity}`( + pattern: String, + call: String + ) { + assertThat(pattern.toRegex().matches(call)).isFalse() + } + + // endregion + + // region Multiple wildcards + + @Test + fun `M match call W signaturePatternOf() {two NON_NULL_WILDCARDs match two non-nullable types}`() { + val pattern = "kotlin.collections.MutableList.add(*, *)".toRegex() + + assertThat(pattern.matches("kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)")).isTrue() + assertThat(pattern.matches("kotlin.collections.MutableList.add(kotlin.Int, java.io.File)")).isTrue() + } + + @Test + fun `M match call W signaturePatternOf() {NON_NULL_WILDCARD and NULLABLE_WILDCARD}`() { + val pattern = "kotlin.collections.MutableMap.put(*, ?)".toRegex() + + assertThat(pattern.matches("kotlin.collections.MutableMap.put(kotlin.String, kotlin.Any?)")).isTrue() + assertThat(pattern.matches("kotlin.collections.MutableMap.put(kotlin.String, kotlin.String?)")).isTrue() + } + + @Test + fun `M not match call W signaturePatternOf() {NON_NULL_WILDCARD rejects nullable first param}`() { + val pattern = "kotlin.collections.MutableMap.put(*, ?)".toRegex() + + // first param is nullable, rejected by `*` + assertThat(pattern.matches("kotlin.collections.MutableMap.put(kotlin.String?, kotlin.Any)")).isFalse() + } + + @Test + fun `M not match call W signaturePatternOf() {two-wildcard pattern does not match single-param call}`() { + val pattern = "kotlin.collections.MutableList.add(*, *)".toRegex() + + assertThat(pattern.matches("kotlin.collections.MutableList.add(kotlin.String)")).isFalse() + } + + @Test + fun `M not match call W signaturePatternOf() {two-wildcard pattern does not match three-param call}`() { + val pattern = "kotlin.collections.MutableList.add(*, *)".toRegex() + + assertThat( + pattern.matches("kotlin.collections.MutableList.add(kotlin.Int, kotlin.String, kotlin.Boolean)") + ).isFalse() + } + + // endregion + + // region Space-tolerant separator + + @Test + fun `M match call W signaturePatternOf() {no space after comma in config entry}`() { + val pattern = "kotlin.collections.MutableList.add(kotlin.Int,kotlin.String)".toRegex() + + assertThat(pattern.matches("kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)")).isTrue() + } + + @Test + fun `M produce same regex W signaturePatternOf() {space after comma vs no space}`() { + val withSpace = "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)".toRegex() + val noSpace = "kotlin.collections.MutableList.add(kotlin.Int,kotlin.String)".toRegex() + + assertThat(noSpace.pattern).isEqualTo(withSpace.pattern) + } + + @Test + fun `M match wildcard call W signaturePatternOf() {no space after comma with wildcard}`() { + val pattern = "kotlin.collections.MutableMap.put(*,?)".toRegex() + + assertThat(pattern.matches("kotlin.collections.MutableMap.put(kotlin.String, kotlin.Any?)")).isTrue() + assertThat(pattern.matches("kotlin.collections.MutableMap.put(kotlin.String?, kotlin.Any)")).isFalse() + } + + @Test + fun `M trim exception names W parseThrowingCalls() {space after comma}`() { + val exceptions = DetektConfigParser() + .parseThrowingCalls( + listOf("java.io.File.inputStream():java.io.FileNotFoundException, java.lang.SecurityException") + ) + .first() + .exceptions + + assertThat(exceptions).containsExactly( + "java.io.FileNotFoundException", + "java.lang.SecurityException" + ) + } + + // endregion + + companion object { + private fun String.toRegex(): Regex = DetektConfigParser.parseEntry(this).regex + + @JvmStatic + fun provideNonNullableWildcardMatches(): List = listOf( + Arguments.of( + "kotlin.collections.MutableList.add(*)", + "kotlin.collections.MutableList.add(kotlin.String)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(*)", + "kotlin.collections.MutableList.add(java.io.File)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, *)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, *)", + "kotlin.collections.MutableList.add(kotlin.Int, com.example.MyClass)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(*, kotlin.String)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)" + ) + ) + + @JvmStatic + fun provideNonNullableWildcardNonMatches(): List = listOf( + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, *)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String?)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(*)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, *)", + "kotlin.collections.MutableList.add(kotlin.String)" + ) + ) + + @JvmStatic + fun provideNullableWildcardMatches(): List = listOf( + Arguments.of( + "kotlin.collections.MutableList.add(?)", + "kotlin.collections.MutableList.add(kotlin.String?)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(?)", + "kotlin.collections.MutableList.add(kotlin.String)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, ?)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String?)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, ?)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, ?)", + "kotlin.collections.MutableList.add(kotlin.Int, java.io.File?)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(?, kotlin.String)", + "kotlin.collections.MutableList.add(kotlin.Int?, kotlin.String)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(?, kotlin.String)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)" + ) + ) + + @JvmStatic + fun provideNullableWildcardNonMatches(): List = listOf( + Arguments.of( + "kotlin.collections.MutableList.add(?)", + "kotlin.collections.MutableList.add(kotlin.Int?, kotlin.String?)" + ), + Arguments.of( + "kotlin.collections.MutableList.add(kotlin.Int, ?)", + "kotlin.collections.MutableList.add(kotlin.String?)" + ) + ) + } +} diff --git a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigValidatorTest.kt b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigValidatorTest.kt new file mode 100644 index 0000000000..e847451407 --- /dev/null +++ b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/DetektConfigValidatorTest.kt @@ -0,0 +1,278 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +internal class DetektConfigValidatorTest { + + private val parser = DetektConfigParser() + private val testedValidator = DetektConfigValidator() + + @Test + fun `M throw exception W validate() {known throwing literal is listed as safe}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.Array.first(kotlin.Function1)"), + throwingCalls = listOf("kotlin.Array.first(kotlin.Function1):java.util.NoSuchElementException") + ) + } + } + + @Test + fun `M throw exception W validate() {known throwing call is covered by safe wildcard}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.Array.first(?)"), + throwingCalls = listOf("kotlin.Array.first(kotlin.Function1):java.util.NoSuchElementException") + ) + } + } + + @Test + fun `M throw exception W validate() {safe call resolves to method declaring checked exception}`() { + assertThrows { + validate( + safeCalls = listOf("java.io.BufferedWriter.write(kotlin.String)") + ) + } + } + + @Test + fun `M throw exception W validate() {safe call resolves to no-arg method declaring checked exception}`() { + assertThrows { + validate( + safeCalls = listOf("java.io.InputStream.read()") + ) + } + } + + @Test + fun `M throw exception W validate() {safe call resolves to constructor declaring checked exception}`() { + assertThrows { + validate( + safeCalls = listOf("java.io.RandomAccessFile.constructor(kotlin.String, kotlin.String)") + ) + } + } + + @Test + fun `M not throw W validate() {safe call resolves to overload set with a non-throwing member}`() { + validate( + safeCalls = listOf("java.io.FileInputStream.constructor(kotlin.String)") + ) + } + + @Test + fun `M not throw W validate() {safe call resolves to method with only unchecked exceptions}`() { + validate( + safeCalls = listOf("java.lang.StringBuilder.append(kotlin.String?)") + ) + } + + @Test + fun `M not throw W validate() {safe call declaring class is not on the classpath}`() { + validate( + safeCalls = listOf("com.example.unknown.Mystery.doStuff(kotlin.String)") + ) + } + + @Test + fun `M throw exception W validate() {safe wildcard overlaps with throwing literal}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.collections.MutableList.add(*)"), + throwingCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.String):java.lang.UnsupportedOperationException" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {safe literal overlaps with throwing wildcard}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.collections.MutableList.add(kotlin.String)"), + throwingCalls = listOf( + "kotlin.collections.MutableList.add(*):java.lang.UnsupportedOperationException" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {safe and throwing wildcards have same shape}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.collections.MutableMap.put(*, ?)"), + throwingCalls = listOf( + "kotlin.collections.MutableMap.put(*, ?):java.lang.UnsupportedOperationException" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {safe non-nullable wildcard overlaps with throwing any-type wildcard}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.collections.MutableList.add(*)"), + throwingCalls = listOf( + "kotlin.collections.MutableList.add(?):java.lang.UnsupportedOperationException" + ) + ) + } + } + + @Test + fun `M not throw W validate() {safe and throwing patterns target different methods}`() { + validate( + safeCalls = listOf("kotlin.collections.MutableList.add(*)"), + throwingCalls = listOf( + "kotlin.collections.MutableList.remove(*):java.lang.UnsupportedOperationException" + ) + ) + } + + @Test + fun `M throw exception W validate() {safe any-type wildcard overlaps with throwing non-nullable literal}`() { + assertThrows { + validate( + safeCalls = listOf("kotlin.collections.MutableList.add(?)"), + throwingCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.String):java.lang.UnsupportedOperationException" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {same literal entry listed twice in safe list}`() { + assertThrows { + validate( + safeCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.String)", + "kotlin.collections.MutableList.add(kotlin.String)" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {duplicate entry with trailing whitespace in param}`() { + assertThrows { + validate( + safeCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.String )", + "kotlin.collections.MutableList.add(kotlin.String)" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {duplicate entry with no space after comma}`() { + assertThrows { + validate( + safeCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.Int,kotlin.String)", + "kotlin.collections.MutableList.add(kotlin.Int, kotlin.String)" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {wildcard subsumes literal in same safe list}`() { + assertThrows { + validate( + safeCalls = listOf( + "kotlin.collections.MutableList.add(*)", + "kotlin.collections.MutableList.add(kotlin.String)" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {safe nullable concrete entry subsumes non-nullable entry}`() { + assertThrows { + validate( + safeCalls = listOf( + "foo.Bar.baz(kotlin.String)", + "foo.Bar.baz(kotlin.String?)" + ) + ) + } + } + + @Test + fun `M not throw W validate() {throwing nullable concrete entry does not subsume non-nullable entry}`() { + validate( + throwingCalls = listOf( + "foo.Bar.baz(kotlin.String):java.lang.IllegalArgumentException", + "foo.Bar.baz(kotlin.String?):java.lang.IllegalArgumentException" + ) + ) + } + + @Test + fun `M throw exception W validate() {wildcard subsumes literal in same throwing list}`() { + assertThrows { + validate( + throwingCalls = listOf( + "kotlin.collections.MutableList.add(*):java.lang.UnsupportedOperationException", + "kotlin.collections.MutableList.add(kotlin.String):java.lang.UnsupportedOperationException" + ) + ) + } + } + + @Test + fun `M throw exception W validate() {safe calls for same class are not sorted}`() { + assertThrows { + validate( + safeCalls = listOf( + "kotlin.collections.MutableList.remove(kotlin.Int)", + "kotlin.collections.MutableList.add(kotlin.String)" + ) + ) + } + } + + @Test + fun `M not throw W validate() {safe calls for same class are sorted}`() { + validate( + safeCalls = listOf( + "kotlin.collections.MutableList.add(kotlin.String)", + "kotlin.collections.MutableList.remove(kotlin.Int)" + ) + ) + } + + @Test + fun `M not throw W validate() {safe calls from different classes are in any order}`() { + validate( + safeCalls = listOf( + "kotlin.collections.MutableSet.add(kotlin.String)", + "kotlin.collections.MutableList.add(kotlin.String)" + ) + ) + } + + private fun validate( + safeCalls: List = emptyList(), + throwingCalls: List = emptyList() + ) { + testedValidator.validate( + knownSafeCalls = parser.parseSafeCalls(safeCalls), + knownThrowingCalls = parser.parseThrowingCalls(throwingCalls) + ) + } +} diff --git a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/SignatureRuleTest.kt b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/SignatureRuleTest.kt new file mode 100644 index 0000000000..759825968f --- /dev/null +++ b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/rule/thirdparty/SignatureRuleTest.kt @@ -0,0 +1,231 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.detekt.rules.sdk.rule.thirdparty + +import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.CodeParser.KtMethodParameter +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +internal class SignatureRuleTest { + + @Test + fun `M match W matches() {exact signature}`() { + val rule = "foo.Bar.baz(kotlin.String)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String)")).isTrue() + } + + @Test + fun `M not match W matches() {different method name}`() { + val rule = "foo.Bar.baz(kotlin.String)".rule() + assertThat(rule.matches("foo.Bar.qux(kotlin.String)")).isFalse() + } + + @Test + fun `M not match W matches() {different param type}`() { + val rule = "foo.Bar.baz(kotlin.String)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.Int)")).isFalse() + } + + @Test + fun `M match W matches() {nullable concrete param covers non-nullable call}`() { + val rule = "foo.Bar.baz(kotlin.String?)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String)")).isTrue() + } + + @Test + fun `M not match W matches() {non-nullable concrete param does not cover nullable call}`() { + val rule = "foo.Bar.baz(kotlin.String)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String?)")).isFalse() + } + + @Test + fun `M not match W matches() {different arity}`() { + val rule = "foo.Bar.baz(kotlin.String)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String, kotlin.Int)")).isFalse() + } + + @Test + fun `M match W matches() {NON_NULL_WILDCARD matches non-nullable type}`() { + val rule = "foo.Bar.baz(*)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String)")).isTrue() + } + + @Test + fun `M not match W matches() {NON_NULL_WILDCARD rejects nullable type}`() { + val rule = "foo.Bar.baz(*)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String?)")).isFalse() + } + + @Test + fun `M match W matches() {NULLABLE_WILDCARD matches non-nullable type}`() { + val rule = "foo.Bar.baz(?)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String)")).isTrue() + } + + @Test + fun `M match W matches() {NULLABLE_WILDCARD matches nullable type}`() { + val rule = "foo.Bar.baz(?)".rule() + assertThat(rule.matches("foo.Bar.baz(kotlin.String?)")).isTrue() + } + + @Test + fun `M match W matches() {no params}`() { + val rule = "foo.Bar.baz()".rule() + assertThat(rule.matches("foo.Bar.baz()")).isTrue() + } + + @Test + fun `M match W intersects() {identical exact signatures overlap}`() { + val a = "foo.Bar.baz(kotlin.String)".rule() + val b = "foo.Bar.baz(kotlin.String)".rule() + assertThat(a.intersects(b)).isTrue() + } + + @Test + fun `M match W intersects() {nullable concrete param overlaps with non-nullable concrete param}`() { + val a = "foo.Bar.baz(kotlin.String?)".rule() + val b = "foo.Bar.baz(kotlin.String)".rule() + assertThat(a.intersects(b)).isTrue() + } + + @Test + fun `M not match W intersects() {throwing nullable concrete param and non-nullable concrete param}`() { + val a = "foo.Bar.baz(kotlin.String?)".throwingRule() + val b = "foo.Bar.baz(kotlin.String)".rule() + assertThat(a.intersects(b)).isFalse() + } + + @Test + fun `M not match W intersects() {different method names do not overlap}`() { + val a = "foo.Bar.baz(kotlin.String)".rule() + val b = "foo.Bar.qux(kotlin.String)".rule() + assertThat(a.intersects(b)).isFalse() + } + + @Test + fun `M not match W intersects() {different arity does not overlap}`() { + val a = "foo.Bar.baz(kotlin.String)".rule() + val b = "foo.Bar.baz(kotlin.String, kotlin.Int)".rule() + assertThat(a.intersects(b)).isFalse() + } + + @Test + fun `M match W intersects() {NON_NULL_WILDCARD overlaps with concrete non-nullable type}`() { + val a = "foo.Bar.baz(*)".rule() + val b = "foo.Bar.baz(kotlin.String)".rule() + assertThat(a.intersects(b)).isTrue() + } + + @Test + fun `M not match W intersects() {NON_NULL_WILDCARD does not overlap with nullable concrete type}`() { + val a = "foo.Bar.baz(*)".rule() + val b = "foo.Bar.baz(kotlin.String?)".rule() + assertThat(a.intersects(b)).isFalse() + } + + @Test + fun `M match W intersects() {NULLABLE_WILDCARD overlaps with nullable concrete type}`() { + val a = "foo.Bar.baz(?)".rule() + val b = "foo.Bar.baz(kotlin.String?)".rule() + assertThat(a.intersects(b)).isTrue() + } + + @Test + fun `M match W intersects() {NULLABLE_WILDCARD overlaps with NON_NULL_WILDCARD}`() { + val a = "foo.Bar.baz(?)".rule() + val b = "foo.Bar.baz(*)".rule() + assertThat(a.intersects(b)).isTrue() + } + + @Test + fun `M match W intersects() {NON_NULL_WILDCARD and NULLABLE_WILDCARD overlap with each other}`() { + val a = "foo.Bar.baz(*, ?)".rule() + val b = "foo.Bar.baz(*, *)".rule() + assertThat(a.intersects(b)).isTrue() + } + + @Test + fun `M not throw W validate() {NON_NULL_WILDCARD targets generic param}`() { + val rule = "foo.Bar.baz(*)".rule() + val params = listOf(KtMethodParameter("t", "T", isGeneric = true)) + assertDoesNotThrow { rule.validate(params) } + } + + @Test + fun `M throw W validate() {NON_NULL_WILDCARD targets non-generic param}`() { + val rule = "foo.Bar.baz(*)".rule() + val params = listOf(KtMethodParameter("s", "kotlin.String", isGeneric = false)) + assertThrows { rule.validate(params) } + } + + @Test + fun `M not throw W validate() {NULLABLE_WILDCARD targets Any param}`() { + val rule = "foo.Bar.baz(?)".rule() + val params = listOf(KtMethodParameter("value", "kotlin.Any?", isGeneric = false)) + assertDoesNotThrow { rule.validate(params) } + } + + // Kotlin can add `EnhancedForWarnings` to Java types whose enhanced nullability is warning-only. + // Detekt exposed this shape while analyzing `java.lang.reflect.Field.get(java.lang.Object)`. + @Test + fun `M not throw W validate() {NULLABLE_WILDCARD targets enhanced Any param}`() { + val rule = "foo.Bar.baz(?)".rule() + val params = listOf(KtMethodParameter("value", "[@EnhancedForWarnings(Any?)] (Any..Any?)", isGeneric = false)) + assertDoesNotThrow { rule.validate(params) } + } + + @Test + fun `M not throw W validate() {NON_NULL_WILDCARD targets java Object param}`() { + val rule = "foo.Bar.baz(*)".rule() + val params = listOf(KtMethodParameter("value", "java.lang.Object", isGeneric = false)) + assertDoesNotThrow { rule.validate(params) } + } + + @Test + fun `M return parsed reference W memberReference {qualified member}`() { + // Given + val rule = "foo.Bar.baz(kotlin.String, kotlin.Int)".rule() + + // Then + assertThat(rule.memberReference).isEqualTo( + SignatureRule.MemberReference( + className = "foo.Bar", + memberName = "baz", + parameterCount = 2 + ) + ) + } + + @Test + fun `M return parsed reference W memberReference {unqualified member}`() { + // Given + val rule = "baz()".rule() + + // Then + assertThat(rule.memberReference).isEqualTo( + SignatureRule.MemberReference( + className = "", + memberName = "baz", + parameterCount = 0 + ) + ) + } + + companion object { + private fun String.rule(exceptions: List = emptyList()) = + DetektConfigParser.parseEntry(this, exceptions) + + private fun String.throwingRule(exceptions: List = emptyList()) = + DetektConfigParser.parseEntry( + source = this, + exceptions = exceptions, + nullableConcreteMatchesNonNullable = false + ) + } +}