diff --git a/.mapping.json b/.mapping.json index 9b73c0328..7bcfddd6b 100644 --- a/.mapping.json +++ b/.mapping.json @@ -624,6 +624,9 @@ "client/android/div-core/src/main/java/com/yandex/div/core/images/DivImagePriority.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/images/DivImagePriority.kt", "client/android/div-core/src/main/java/com/yandex/div/core/images/LoadReference.java":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/images/LoadReference.java", "client/android/div-core/src/main/java/com/yandex/div/core/uri/UriHandler.java":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/uri/UriHandler.java", + "client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt", + "client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt", + "client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt", "client/android/div-data/build.gradle":"divkit/public/client/android/div-data/build.gradle", "client/android/div-data/div2-generator-config.json":"divkit/public/client/android/div-data/div2-generator-config.json", "client/android/div-data/div2-shared-data-generator-config.json":"divkit/public/client/android/div-data/div2-shared-data-generator-config.json", @@ -1174,9 +1177,9 @@ "client/android/div/src/main/java/com/yandex/div/core/util/ImageRepresentation.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ImageRepresentation.kt", "client/android/div/src/main/java/com/yandex/div/core/util/ImageUtils.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ImageUtils.kt", "client/android/div/src/main/java/com/yandex/div/core/util/Releasables.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/Releasables.kt", + "client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt", "client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialog.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialog.kt", "client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialogBuilder.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialogBuilder.kt", - "client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt", "client/android/div/src/main/java/com/yandex/div/core/util/SafePopupWindow.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafePopupWindow.kt", "client/android/div/src/main/java/com/yandex/div/core/util/SearchUtil.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SearchUtil.kt", "client/android/div/src/main/java/com/yandex/div/core/util/SparseArrays.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SparseArrays.kt", @@ -1401,13 +1404,11 @@ "client/android/div/src/main/java/com/yandex/div/core/widget/DivExtendableView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DivExtendableView.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/DivViewDelegate.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DivViewDelegate.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/DivViewWrapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DivViewWrapper.kt", - "client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightHelper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightHelper.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightView.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/GridContainer.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/GridContainer.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/LinearContainerLayout.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/LinearContainerLayout.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/LoadableImageView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/LoadableImageView.kt", - "client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/ShowSeparatorsMode.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/ShowSeparatorsMode.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/ViewPager2Wrapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/ViewPager2Wrapper.kt", "client/android/div/src/main/java/com/yandex/div/core/widget/Views.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/Views.kt", @@ -1557,7 +1558,7 @@ "client/android/div/src/test/java/com/yandex/div/core/util/DivPatchApplyItemsTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/DivPatchApplyItemsTest.kt", "client/android/div/src/test/java/com/yandex/div/core/util/DivWalkTreeTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/DivWalkTreeTest.kt", "client/android/div/src/test/java/com/yandex/div/core/util/EnableAssertsRule.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/EnableAssertsRule.kt", - "client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt", + "client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt", "client/android/div/src/test/java/com/yandex/div/core/util/TypeConverterTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/TypeConverterTest.kt", "client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt", "client/android/div/src/test/java/com/yandex/div/core/util/mask/FixedLengthMaskTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/mask/FixedLengthMaskTest.kt", @@ -12155,6 +12156,12 @@ "client/android/gradle/wrapper/gradle-wrapper.properties":"divkit/public/client/android/gradle/wrapper/gradle-wrapper.properties", "client/android/gradlew":"divkit/public/client/android/gradlew", "client/android/gradlew.bat":"divkit/public/client/android/gradlew.bat", + "client/android/lint-rules/build.gradle":"divkit/public/client/android/lint-rules/build.gradle", + "client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt":"divkit/public/client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt", + "client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt":"divkit/public/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt", + "client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt":"divkit/public/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt", + "client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry":"divkit/public/client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry", + "client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt":"divkit/public/client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt", "client/android/logging/build.gradle":"divkit/public/client/android/logging/build.gradle", "client/android/logging/jacoco.excludes":"divkit/public/client/android/logging/jacoco.excludes", "client/android/logging/proguard-rules.pro":"divkit/public/client/android/logging/proguard-rules.pro", diff --git a/client/android/build.gradle b/client/android/build.gradle index 2e0541a17..1d02c620e 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -126,6 +126,7 @@ apiValidation { "divkit-demo-app", "divkit-perftests", "divkit-regression-testing", + "lint-rules", "sample", "screenshot-test-runtime", "ui-test-common", diff --git a/client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt b/client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt new file mode 100644 index 000000000..9e8080a8d --- /dev/null +++ b/client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt @@ -0,0 +1,18 @@ +package com.yandex.div.core.view + +import android.view.ViewTreeObserver.OnPreDrawListener +import com.yandex.div.core.annotations.InternalApi + +@InternalApi +public fun interface DrawingPassOverrideStrategy { + + public fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean + + @InternalApi + public object NoOp : DrawingPassOverrideStrategy { + override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean = proceed + } + + @InternalApi + public object Safe : SafeDrawingPassOverrideStrategy() +} diff --git a/client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt b/client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt new file mode 100644 index 000000000..bc0384f38 --- /dev/null +++ b/client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt @@ -0,0 +1,42 @@ +@file:JvmName("OnPreDrawListeners") + +package com.yandex.div.core.view + +import android.annotation.SuppressLint +import android.view.ViewTreeObserver +import com.yandex.div.core.annotations.InternalApi + +@InternalApi +public fun onPreDrawListener( + overrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe, + action: () -> Boolean +): ViewTreeObserver.OnPreDrawListener { + return OverridableOnPreDrawListener( + delegate = action, + overrideStrategy + ) +} + +@InternalApi +public fun onPreDrawListener( + overrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe, + delegate: ViewTreeObserver.OnPreDrawListener +): ViewTreeObserver.OnPreDrawListener { + return OverridableOnPreDrawListener( + delegate = delegate, + overrideStrategy + ) +} + +@InternalApi +@SuppressLint("OnPreDrawListenerIssue") +public class OverridableOnPreDrawListener @JvmOverloads constructor( + private val delegate: ViewTreeObserver.OnPreDrawListener, + private val overrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe +) : ViewTreeObserver.OnPreDrawListener { + + override fun onPreDraw(): Boolean { + val proceed = delegate.onPreDraw() + return overrideStrategy.overrideDrawingPass(delegate, proceed) + } +} diff --git a/client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt b/client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt new file mode 100644 index 000000000..e6bd1a3fc --- /dev/null +++ b/client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt @@ -0,0 +1,43 @@ +package com.yandex.div.core.view + +import android.view.ViewTreeObserver.OnPreDrawListener +import com.yandex.div.core.annotations.InternalApi + +@InternalApi +public open class SafeDrawingPassOverrideStrategy : DrawingPassOverrideStrategy { + + public var frameCancelLimit: Int = DEFAULT_FRAME_CANCEL_LIMIT + set(value) { + if (field != value) { + field = value + frameCancelCount = 0 + } + } + + private var frameCancelCount = 0 + + override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean { + if (proceed) { + frameCancelCount = 0 + return true + } else if (frameCancelCount < frameCancelLimit) { + frameCancelCount++ + onFrameCancelled(listener, frameCancelCount) + return false + } else if (frameCancelCount == frameCancelLimit) { + frameCancelCount++ + onFrameCancelLimitExceeded(listener, frameCancelCount) + return true + } else { + return true + } + } + + protected open fun onFrameCancelled(listener: OnPreDrawListener, frameCancelCount: Int): Unit = Unit + + protected open fun onFrameCancelLimitExceeded(listener: OnPreDrawListener, frameCancelCount: Int): Unit = Unit + + private companion object { + const val DEFAULT_FRAME_CANCEL_LIMIT: Int = 3 + } +} diff --git a/client/android/div-evaluable/build.gradle b/client/android/div-evaluable/build.gradle index 22d9109b9..cab47e820 100644 --- a/client/android/div-evaluable/build.gradle +++ b/client/android/div-evaluable/build.gradle @@ -1,6 +1,9 @@ -apply plugin: 'java-library' -apply plugin: 'org.jetbrains.kotlin.jvm' -apply plugin: "kotlin-allopen" +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' + id 'org.jetbrains.kotlin.plugin.allopen' +} + apply from: "${project.projectDir}/../publish-java.gradle" apply from: "${project.projectDir}/../div-tests-coverage.gradle" diff --git a/client/android/div-library.gradle b/client/android/div-library.gradle index cec833253..532f1815d 100644 --- a/client/android/div-library.gradle +++ b/client/android/div-library.gradle @@ -1,3 +1,7 @@ apply plugin: 'com.android.library' apply from: "${buildscript.sourceFile.parent}/div-common.gradle" + +dependencies { + lintChecks project(path: ':lint-rules') +} diff --git a/client/android/div-size-provider/src/main/java/com/yandex/div/sizeprovider/DivSizeProviderExtensionHandler.kt b/client/android/div-size-provider/src/main/java/com/yandex/div/sizeprovider/DivSizeProviderExtensionHandler.kt index 5698eead6..c889d2491 100644 --- a/client/android/div-size-provider/src/main/java/com/yandex/div/sizeprovider/DivSizeProviderExtensionHandler.kt +++ b/client/android/div-size-provider/src/main/java/com/yandex/div/sizeprovider/DivSizeProviderExtensionHandler.kt @@ -2,8 +2,8 @@ package com.yandex.div.sizeprovider import android.util.DisplayMetrics import android.view.View -import android.view.ViewTreeObserver import com.yandex.div.core.extension.DivExtensionHandler +import com.yandex.div.core.view.onPreDrawListener import com.yandex.div.core.view2.Div2View import com.yandex.div.core.view2.divs.pxToDp import com.yandex.div.json.expressions.ExpressionResolver @@ -59,7 +59,7 @@ class DivSizeProviderExtensionHandler( if (divView.getTag(R.id.div_size_provider_clear_variables_listener) != null) return - val clearVariablesListener = ViewTreeObserver.OnPreDrawListener { + val clearVariablesListener = onPreDrawListener { variablesHolder.clear() sizes.forEach { divView.setVariable(it.key, it.value.toString()) } sizes.clear() diff --git a/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewComponent.kt b/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewComponent.kt index 03e8a2e9d..e8774e64a 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewComponent.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewComponent.kt @@ -2,7 +2,6 @@ package com.yandex.div.core.dagger import com.yandex.div.core.expression.local.DivRuntimeVisitor import com.yandex.div.core.tooltip.DivTooltipController -import com.yandex.div.core.util.SafeDrawingPassOverrideStrategy import com.yandex.div.core.view2.Div2View import com.yandex.div.core.view2.DivTransitionBuilder import com.yandex.div.core.view2.DivViewIdProvider @@ -15,6 +14,7 @@ import com.yandex.div.core.view2.errors.ErrorVisualMonitor import com.yandex.div.core.view2.reuse.InputFocusTracker import com.yandex.div.core.view2.state.DivStateSwitcher import com.yandex.div.core.view2.state.DivStateTransitionHolder +import com.yandex.div.core.view.DrawingPassOverrideStrategy import com.yandex.yatagan.BindsInstance import com.yandex.yatagan.Component @@ -40,7 +40,7 @@ internal interface Div2ViewComponent { val animatorController: DivAnimatorController val divTooltipController: DivTooltipController val runtimeVisitor: DivRuntimeVisitor - val drawingPassOverrideStrategy: SafeDrawingPassOverrideStrategy + val drawingPassOverrideStrategy: DrawingPassOverrideStrategy @Component.Builder interface Builder { diff --git a/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewModule.kt b/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewModule.kt index e13a4ae33..9b69ab604 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewModule.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/dagger/Div2ViewModule.kt @@ -1,24 +1,33 @@ package com.yandex.div.core.dagger import com.yandex.div.core.experiments.Experiment.MULTIPLE_STATE_CHANGE_ENABLED +import com.yandex.div.core.util.ReportingSafeDrawingPassOverrideStrategy import com.yandex.div.core.view2.state.DivJoinedStateSwitcher import com.yandex.div.core.view2.state.DivMultipleStateSwitcher import com.yandex.div.core.view2.state.DivStateSwitcher +import com.yandex.div.core.view.DrawingPassOverrideStrategy +import com.yandex.yatagan.Binds import com.yandex.yatagan.Module import com.yandex.yatagan.Provides import javax.inject.Provider @Module -internal object Div2ViewModule { +internal interface Div2ViewModule { - @JvmStatic - @Provides - @DivViewScope - fun provideStateSwitcher( - @ExperimentFlag(MULTIPLE_STATE_CHANGE_ENABLED) multipleStateChangeEnabled: Boolean, - joinedStateSwitcher: Provider, - multipleStateSwitcher: Provider - ): DivStateSwitcher { - return if (multipleStateChangeEnabled) multipleStateSwitcher.get() else joinedStateSwitcher.get() + @Binds + fun bindsDrawingPassOverrideStrategy(i: ReportingSafeDrawingPassOverrideStrategy): DrawingPassOverrideStrategy + + companion object { + + @JvmStatic + @Provides + @DivViewScope + fun provideStateSwitcher( + @ExperimentFlag(MULTIPLE_STATE_CHANGE_ENABLED) multipleStateChangeEnabled: Boolean, + joinedStateSwitcher: Provider, + multipleStateSwitcher: Provider + ): DivStateSwitcher { + return if (multipleStateChangeEnabled) multipleStateSwitcher.get() else joinedStateSwitcher.get() + } } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt b/client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt new file mode 100644 index 000000000..e7a782445 --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt @@ -0,0 +1,26 @@ +package com.yandex.div.core.util + +import android.view.ViewTreeObserver.OnPreDrawListener +import com.yandex.div.core.Div2Logger +import com.yandex.div.core.dagger.DivViewScope +import com.yandex.div.core.view.SafeDrawingPassOverrideStrategy +import com.yandex.div.core.view2.Div2View +import javax.inject.Inject + +@DivViewScope +internal class ReportingSafeDrawingPassOverrideStrategy @Inject constructor( + private val divView: Div2View, + private val logger: Div2Logger, +) : SafeDrawingPassOverrideStrategy() { + + override fun onFrameCancelled(listener: OnPreDrawListener, frameCancelCount: Int) { + logger.logFrameCancelled(divView, "Frame cancelled by $listener") + } + + override fun onFrameCancelLimitExceeded(listener: OnPreDrawListener, frameCancelCount: Int) { + logger.logFrameCancelLimitExceeded( + divView, + "Frame cancellation limit exceeded by $listener. Forcing frame drawing." + ) + } +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt b/client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt deleted file mode 100644 index 26a61db27..000000000 --- a/client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.yandex.div.core.util - -import android.view.ViewTreeObserver.OnPreDrawListener -import com.yandex.div.core.Div2Logger -import com.yandex.div.core.dagger.DivViewScope -import com.yandex.div.core.view2.Div2View -import com.yandex.div.core.widget.DrawingPassOverrideStrategy -import javax.inject.Inject - -@DivViewScope -internal class SafeDrawingPassOverrideStrategy @Inject constructor( - private val divView: Div2View, - private val logger: Div2Logger, -) : DrawingPassOverrideStrategy { - - var frameCancelLimit: Int = DEFAULT_FRAME_CANCEL_LIMIT - set(value) { - if (field != value) { - field = value - frameCancelCount = 0 - } - } - - private var frameCancelCount = 0 - - override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean { - if (proceed) { - frameCancelCount = 0 - return true - } else if (frameCancelCount < frameCancelLimit) { - frameCancelCount++ - logger.logFrameCancelled(divView, "Frame cancelled by $listener") - return false - } else if (frameCancelCount == frameCancelLimit) { - frameCancelCount++ - logger.logFrameCancelLimitExceeded( - divView, - "Frame cancellation limit exceeded by $listener. Forcing frame drawing." - ) - return true - } else { - return true - } - } - - companion object { - const val DEFAULT_FRAME_CANCEL_LIMIT = 3 - } -} diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivBaseBinder.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivBaseBinder.kt index d6d27758a..035fc26a6 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivBaseBinder.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivBaseBinder.kt @@ -6,7 +6,6 @@ import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.MarginLayoutParams -import android.view.ViewTreeObserver import androidx.transition.Transition import androidx.transition.TransitionManager import androidx.transition.Visibility @@ -20,6 +19,7 @@ import com.yandex.div.core.util.isConstant import com.yandex.div.core.util.observeEdgeInsets import com.yandex.div.core.util.observeSize import com.yandex.div.core.util.observeTransform +import com.yandex.div.core.view.onPreDrawListener import com.yandex.div.core.view2.BindingContext import com.yandex.div.core.view2.Div2View import com.yandex.div.core.view2.DivAccessibilityBinder @@ -301,7 +301,7 @@ internal class DivBaseBinder @Inject constructor( setTag(R.id.div_layout_provider_listener_id, listener) if (divView.clearVariablesListener != null) return - val clearVariablesListener = ViewTreeObserver.OnPreDrawListener { + val clearVariablesListener = onPreDrawListener { variablesHolder.clear() divView.layoutSizes.forEach { (resolver, variableMap) -> variableMap.forEach { diff --git a/client/android/div/src/main/java/com/yandex/div/core/widget/AdaptiveMaxLines.kt b/client/android/div/src/main/java/com/yandex/div/core/widget/AdaptiveMaxLines.kt index a36c7c03c..fe4785edd 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/widget/AdaptiveMaxLines.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/widget/AdaptiveMaxLines.kt @@ -5,6 +5,8 @@ import android.view.View import android.view.ViewTreeObserver import android.widget.TextView import androidx.core.view.ViewCompat +import com.yandex.div.core.view.DrawingPassOverrideStrategy +import com.yandex.div.core.view.onPreDrawListener import com.yandex.div.core.widget.AdaptiveMaxLines.Params /** diff --git a/client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt b/client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt deleted file mode 100644 index 542a626ca..000000000 --- a/client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.yandex.div.core.widget - -import android.view.ViewTreeObserver.OnPreDrawListener - -internal fun interface DrawingPassOverrideStrategy { - - fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean - - object Default : DrawingPassOverrideStrategy { - override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean) = proceed - } -} diff --git a/client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt b/client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt deleted file mode 100644 index 5f7c1fee4..000000000 --- a/client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.yandex.div.core.widget - -import android.view.ViewTreeObserver - -internal fun onPreDrawListener( - overrideStrategy: DrawingPassOverrideStrategy, - action: () -> Boolean -): ViewTreeObserver.OnPreDrawListener { - return OverridableOnPreDrawListener( - delegate = { action() }, - overrideStrategy - ) -} - -internal fun onPreDrawListener( - overrideStrategy: DrawingPassOverrideStrategy, - delegate: ViewTreeObserver.OnPreDrawListener -): ViewTreeObserver.OnPreDrawListener { - return OverridableOnPreDrawListener( - delegate = delegate, - overrideStrategy - ) -} - -internal class OverridableOnPreDrawListener( - private val delegate: ViewTreeObserver.OnPreDrawListener, - private val overrideStrategy: DrawingPassOverrideStrategy -): ViewTreeObserver.OnPreDrawListener { - - override fun onPreDraw(): Boolean { - val proceed = delegate.onPreDraw() - return overrideStrategy.overrideDrawingPass(delegate, proceed) - } -} diff --git a/client/android/div/src/main/java/com/yandex/div/internal/widget/AutoEllipsizeHelper.kt b/client/android/div/src/main/java/com/yandex/div/internal/widget/AutoEllipsizeHelper.kt index 6923166ee..f14458aa4 100644 --- a/client/android/div/src/main/java/com/yandex/div/internal/widget/AutoEllipsizeHelper.kt +++ b/client/android/div/src/main/java/com/yandex/div/internal/widget/AutoEllipsizeHelper.kt @@ -1,8 +1,8 @@ package com.yandex.div.internal.widget import android.view.ViewTreeObserver -import com.yandex.div.core.widget.DrawingPassOverrideStrategy -import com.yandex.div.core.widget.onPreDrawListener +import com.yandex.div.core.view.DrawingPassOverrideStrategy +import com.yandex.div.core.view.onPreDrawListener import com.yandex.div.internal.KLog /** @@ -15,7 +15,7 @@ internal class AutoEllipsizeHelper(private val textView: EllipsizedTextView) { */ var isEnabled = false - var drawingPassOverrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Default + var drawingPassOverrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe private var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null diff --git a/client/android/div/src/main/java/com/yandex/div/internal/widget/EllipsizedTextView.kt b/client/android/div/src/main/java/com/yandex/div/internal/widget/EllipsizedTextView.kt index 6b82fba5c..b614d1623 100644 --- a/client/android/div/src/main/java/com/yandex/div/internal/widget/EllipsizedTextView.kt +++ b/client/android/div/src/main/java/com/yandex/div/internal/widget/EllipsizedTextView.kt @@ -10,7 +10,7 @@ import android.util.AttributeSet import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import com.yandex.div.R -import com.yandex.div.core.widget.DrawingPassOverrideStrategy +import com.yandex.div.core.view.DrawingPassOverrideStrategy open class EllipsizedTextView @JvmOverloads constructor( context: Context, diff --git a/client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt b/client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt similarity index 96% rename from client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt rename to client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt index a9b606613..967371f40 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt @@ -14,12 +14,12 @@ import org.mockito.kotlin.verify import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class SafeDrawingPassOverrideStrategyTest { +class ReportingSafeDrawingPassOverrideStrategyTest { private val divView = mock() private val logger = mock() - private val overrideStrategy = SafeDrawingPassOverrideStrategy(divView, logger) + private val overrideStrategy = ReportingSafeDrawingPassOverrideStrategy(divView, logger) @Test fun `no frame cancellation`() { diff --git a/client/android/div/src/test/java/com/yandex/div/core/widget/AdaptiveMaxLinesTest.kt b/client/android/div/src/test/java/com/yandex/div/core/widget/AdaptiveMaxLinesTest.kt index 38e12c657..0a7ee0917 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/widget/AdaptiveMaxLinesTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/widget/AdaptiveMaxLinesTest.kt @@ -3,6 +3,7 @@ package com.yandex.div.core.widget import android.view.View import android.view.ViewTreeObserver import android.widget.TextView +import com.yandex.div.core.view.DrawingPassOverrideStrategy import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -23,7 +24,7 @@ class AdaptiveMaxLinesTest { on { viewTreeObserver } doReturn viewTreeObserver } - private val underTest = AdaptiveMaxLines(textView, DrawingPassOverrideStrategy.Default) + private val underTest = AdaptiveMaxLines(textView, DrawingPassOverrideStrategy.NoOp) @Test fun `add pre draw listener when view already attached to window`() { diff --git a/client/android/gradle/libs.versions.toml b/client/android/gradle/libs.versions.toml index 5acefe992..ca8fc9039 100644 --- a/client/android/gradle/libs.versions.toml +++ b/client/android/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.8.2" +android-lint = "31.8.2" # agp + "23.0.0" androidsvg = "1.4" buildTimeTracker = "5.0.1" coil = "3.0.4" @@ -66,6 +67,10 @@ androidx-work = "2.9.1" agp-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" } agp-gradleApi = { module = "com.android.tools.build:gradle-api", version.ref = "agp" } +android-lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "android-lint" } +android-lint = { module = "com.android.tools.lint:lint", version.ref = "android-lint" } +android-lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "android-lint" } + androidsvg-aar = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvg" } androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } diff --git a/client/android/lint-rules/build.gradle b/client/android/lint-rules/build.gradle new file mode 100644 index 000000000..4b0a2fae9 --- /dev/null +++ b/client/android/lint-rules/build.gradle @@ -0,0 +1,26 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + compileOnly(libs.android.lint.api) + + testImplementation(libs.junit) + testImplementation(libs.android.lint) + testImplementation(libs.android.lint.tests) +} + +tasks.withType(KotlinCompile).configureEach { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } +} diff --git a/client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt b/client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt new file mode 100644 index 000000000..fc0fd2e2a --- /dev/null +++ b/client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt @@ -0,0 +1,20 @@ +package com.yandex.div.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API + +class DivKitIssueRegistry : IssueRegistry() { + + override val api = CURRENT_API + + override val minApi = 8 + + override val issues = listOf(OnPreDrawListenerIssue.get()) + + override val vendor = Vendor( + feedbackUrl = "https://divkit.tech/", + identifier = "com.yandex.div", + vendorName = "DivKit Open Source Project" + ) +} diff --git a/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt b/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt new file mode 100644 index 000000000..2f5a350c3 --- /dev/null +++ b/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt @@ -0,0 +1,110 @@ +package com.yandex.div.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UCallableReferenceExpression +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UElement +import org.jetbrains.uast.ULambdaExpression +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.getParameterForArgument +import org.jetbrains.uast.getParentOfType +import org.jetbrains.uast.getQualifiedName +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.util.isConstructorCall + +class OnPreDrawListenerDetector : Detector(), SourceCodeScanner { + + override fun getApplicableUastTypes(): List> = listOf( + UClass::class.java, + ULambdaExpression::class.java, + UCallableReferenceExpression::class.java + ) + + override fun createUastHandler(context: JavaContext): UElementHandler = OnPreDrawListenerVisitor(context) +} + +class OnPreDrawListenerVisitor( + private val context: JavaContext +) : UElementHandler() { + + override fun visitClass(node: UClass) { + val usage = node.uastSuperTypes.find { typeReference -> + typeReference.getQualifiedName() == TARGET_INTERFACE_FQN + } + if (usage != null) { + reportIssue(scopeClass = node, usage = usage) + } + } + + override fun visitLambdaExpression(node: ULambdaExpression) { + val callExpression = node.uastParent as? UCallExpression ?: return + val constructorExpression = if (callExpression.isConstructorCall()) { + callExpression.classReference + } else { + null + } + val lambdaType = node.functionalInterfaceType?.canonicalText + + if (lambdaType == TARGET_INTERFACE_FQN) { + val nodeClass = node.getParentOfType() + + val usageNode = if (constructorExpression.getQualifiedName() == TARGET_INTERFACE_FQN) { + constructorExpression!! + } else { + node + } + + val location = if (constructorExpression.getQualifiedName() == TARGET_INTERFACE_FQN) { + context.getCallLocation( + callExpression, + includeReceiver = true, + includeArguments = false + ) + } else { + context.getLocation(node) + } + + reportIssue( + scopeClass = nodeClass, + usage = usageNode, + location = location + ) + } + } + + override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) { + val callExpression = node.uastParent as? UCallExpression ?: return + val referenceParameter = callExpression.getParameterForArgument(node).toUElementOfType() ?: return + val referenceType = referenceParameter.typeReference?.getQualifiedName() + + if (referenceType == TARGET_INTERFACE_FQN) { + val nodeClass = node.getParentOfType() + reportIssue( + scopeClass = nodeClass, + usage = node + ) + } + } + + private fun reportIssue( + scopeClass: UClass?, + usage: UElement, + location: Location = context.getLocation(usage) + ) { + context.report( + issue = OnPreDrawListenerIssue.get(), + scopeClass = scopeClass, + location = location, + message = OnPreDrawListenerIssue.fullDescription() + ) + } + + private companion object { + const val TARGET_INTERFACE_FQN = "android.view.ViewTreeObserver.OnPreDrawListener" + } +} diff --git a/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt b/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt new file mode 100644 index 000000000..a4b02b0fa --- /dev/null +++ b/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt @@ -0,0 +1,33 @@ +package com.yandex.div.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity + +object OnPreDrawListenerIssue { + + const val ID = "OnPreDrawListenerIssue" + + private const val DESCRIPTION = "ViewTreeObserver.OnPreDrawListener is potentially unsafe" + private const val EXPLANATION = """Return of `false` from `ViewTreeObserver.OnPreDrawListener.onPreDraw()` may break the drawing of the entire screen. +Replace it with `com.yandex.div.core.view.OverridableOnPreDrawListener` or `com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...)` function.""" + + private val issue = Issue.create( + id = ID, + briefDescription = DESCRIPTION, + explanation = EXPLANATION, + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.ERROR, + implementation = Implementation( + OnPreDrawListenerDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + fun get(): Issue = issue + + fun fullDescription(): String = "$DESCRIPTION\n$EXPLANATION" +} diff --git a/client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 000000000..447723194 --- /dev/null +++ b/client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -0,0 +1 @@ +com.yandex.div.lint.DivKitIssueRegistry diff --git a/client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt b/client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt new file mode 100644 index 000000000..add1629b2 --- /dev/null +++ b/client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt @@ -0,0 +1,273 @@ +package com.yandex.div.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class OnPreDrawListenerDetectorTest : LintDetectorTest() { + + private val declarationFile: TestFile = kotlin( + """ + package android.view + + class ViewTreeObserver { + fun interface OnPreDrawListener { + override fun onPreDraw(): Boolean + } + } + """ + ).indented() + + override fun getDetector(): Detector = OnPreDrawListenerDetector() + + override fun getIssues(): List = listOf(OnPreDrawListenerIssue.get()) + + @Test + fun `issue detected in implementation`() { + val testFile = kotlin( + """ + package test + + import android.view.ViewTreeObserver + + class OnPreDrawListenerImpl : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean = true + } + + val onPreDrawListener = OnPreDrawListenerImpl() + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .run() + .expect( + """ + src/test/OnPreDrawListenerImpl.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + class OnPreDrawListenerImpl : ViewTreeObserver.OnPreDrawListener { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun `issue detected in anonymous implementation`() { + val testFile = kotlin( + """ + package test + + import android.view.ViewTreeObserver + + val onPreDrawListener = object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean = true + } + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .run() + .expect( + """ + src/test/test.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + val onPreDrawListener = object : ViewTreeObserver.OnPreDrawListener { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun `issue does not detected in implicit implementation`() { + val testFile = kotlin(""" + package test + + import android.view.ViewTreeObserver + + @Suppress("OnPreDrawListenerIssue") + class OnPreDrawListenerImpl : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean = true + } + + fun onPreDrawListener(): ViewTreeObserver.OnPreDrawListener { + return OnPreDrawListenerImpl() + } + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .skipTestModes(TestMode.SUPPRESSIBLE) + .run() + .expectClean() + } + + @Test + fun `issue detected in SAM constructor`() { + val testFile = kotlin( + """ + package test + + import android.view.ViewTreeObserver.OnPreDrawListener + + val onPreDrawListener = OnPreDrawListener { true } + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .skipTestModes(TestMode.SUPPRESSIBLE) + .run() + .expect( + """ + src/test/test.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + val onPreDrawListener = OnPreDrawListener { true } + ~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun `issue detected in qualified SAM constructor`() { + val testFile = kotlin( + """ + package test + + import android.view.ViewTreeObserver + + val onPreDrawListener = ViewTreeObserver.OnPreDrawListener { true } + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .skipTestModes(TestMode.SUPPRESSIBLE) + .run() + .expect( + """ + src/test/test.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + val onPreDrawListener = ViewTreeObserver.OnPreDrawListener { true } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun `issue detected in lambda SAM conversion`() { + val testFile = kotlin( + """ + package test + + import android.view.ViewTreeObserver + + class Wrapper( + private val count: Int, + private val listener: ViewTreeObserver.OnPreDrawListener + ) + + fun wrap(listener: ViewTreeObserver.OnPreDrawListener): Wrapper { + return Wrapper(0, listener) + } + + fun doWrapConstructorCall() { + Wrapper(0, listener = { true }) + } + + fun doWrapMethodCall() { + wrap(listener = { true }) + } + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .skipTestModes(TestMode.SUPPRESSIBLE, TestMode.SOURCE_TRANSFORMATION_GROUP) + .run() + .expect( + """ + src/test/Wrapper.kt:15: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + Wrapper(0, listener = { true }) + ~~~~~~~~ + src/test/Wrapper.kt:19: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + wrap(listener = { true }) + ~~~~~~~~ + 2 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun `issue detected in method reference SAM conversion`() { + val testFile = kotlin( + """ + package test + + import android.view.ViewTreeObserver + + class Wrapper(private val listener: ViewTreeObserver.OnPreDrawListener) + + fun wrap(listener: ViewTreeObserver.OnPreDrawListener): Wrapper { + return Wrapper(listener) + } + + fun onPreDraw(): Boolean = true + + fun doWrapConstructorCall() { + Wrapper(listener = ::onPreDraw) + } + + fun doWrapMethodCall() { + wrap(listener = ::onPreDraw) + } + """ + ).indented() + + lint() + .files(declarationFile, testFile) + .allowMissingSdk() + .skipTestModes(TestMode.SUPPRESSIBLE, TestMode.SOURCE_TRANSFORMATION_GROUP) + .run() + .expect( + """ + src/test/Wrapper.kt:14: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + Wrapper(listener = ::onPreDraw) + ~~~~~~~~~~~ + src/test/Wrapper.kt:18: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe + Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen. + Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue] + wrap(listener = ::onPreDraw) + ~~~~~~~~~~~ + 2 errors, 0 warnings + """.trimIndent() + ) + } +} diff --git a/client/android/settings.gradle b/client/android/settings.gradle index 406fedc5f..cb8d15b2b 100644 --- a/client/android/settings.gradle +++ b/client/android/settings.gradle @@ -50,6 +50,7 @@ include ':divkit-regression-testing' include ':expression-test-common' include ':fonts' include ':glide' +include ':lint-rules' include ':logging' include ':picasso' include ':sample'