Add a lint check for using BoxWithConstraintsScope
It is pure overhead to use a BoxWithConstraints composable
without using it's associated BoxWithConstraintsScope properties.
In these cases it could be replaced simply with Box or possibly
just removed.
Test: BoxWithConstraintsDetectorTest
Change-Id: I6ce719e98e9a39d93862844465e1329ab241f33a
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt
index 148eb8b..22289de 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.unit.dp
+@Suppress("UnusedBoxWithConstraintsScope")
@Composable
fun LookaheadWithBoxWithConstraints() {
Box(Modifier.fillMaxSize()) {
diff --git a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/BoxWithConstraintsDetector.kt b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/BoxWithConstraintsDetector.kt
new file mode 100644
index 0000000..9d45624
--- /dev/null
+++ b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/BoxWithConstraintsDetector.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lint
+
+import androidx.compose.lint.inheritsFrom
+import androidx.compose.lint.isInPackageName
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.computeKotlinArgumentMapping
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiWildcardType
+import com.intellij.psi.impl.source.PsiClassReferenceType
+import java.util.EnumSet
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.ULambdaExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.UThisExpression
+import org.jetbrains.uast.tryResolve
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+class BoxWithConstraintsDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> = listOf(
+ FoundationNames.Layout.BoxWithConstraints.shortName
+ )
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (method.isInPackageName(FoundationNames.Layout.PackageName)) {
+ val contentArgument = computeKotlinArgumentMapping(node, method)
+ .orEmpty()
+ .filter { (_, parameter) ->
+ parameter.name == "content"
+ }
+ .keys
+ .filterIsInstance<ULambdaExpression>()
+ .firstOrNull() ?: return
+
+ var foundValidReference = false
+ contentArgument.accept(object : AbstractUastVisitor() {
+ // Check for references to any property of BoxWithConstraintsScope
+ override fun visitSimpleNameReferenceExpression(
+ node: USimpleNameReferenceExpression
+ ): Boolean {
+ val reference = (node.tryResolve() as? PsiMethod)
+ ?: return foundValidReference // No need to continue if already found
+ if (reference.isInPackageName(FoundationNames.Layout.PackageName) &&
+ reference.containingClass?.name == FoundationNames
+ .Layout.BoxWithConstraintsScope.shortName
+ ) {
+ foundValidReference = true
+ }
+
+ // Check if reference is an extension property on BoxWithConstraintsScope
+ if (reference.hierarchicalMethodSignature
+ .parameterTypes.firstOrNull()?.inheritsFrom(
+ FoundationNames
+ .Layout.BoxWithConstraintsScope
+ ) == true
+ ) {
+ foundValidReference = true
+ }
+ return foundValidReference
+ }
+
+ // If this is referenced in the content lambda then consider
+ // the constraints used.
+ override fun visitThisExpression(node: UThisExpression): Boolean {
+ foundValidReference = true
+ return foundValidReference
+ }
+
+ // Check function calls inside the content lambda to see if they
+ // are using BoxWithConstraintsScope
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ val receiverType = node.receiverType ?: return foundValidReference
+
+ // Check for function calls with a BoxWithConstraintsScope receiver type
+ if (receiverType.inheritsFrom(FoundationNames.Layout.BoxWithConstraintsScope)) {
+ foundValidReference = true
+ return foundValidReference
+ }
+
+ // Check for calls to a lambda with a BoxWithConstraintsScope receiver type
+ // e.g. BoxWithConstraintsScope.() -> Unit
+ val firstChildReceiverType = (receiverType as? PsiClassReferenceType)?.reference
+ ?.typeParameters
+ ?.firstOrNull() ?: return foundValidReference
+
+ val resolvedWildcardType = (firstChildReceiverType as? PsiWildcardType)?.bound
+ if (
+ resolvedWildcardType?.inheritsFrom(
+ FoundationNames.Layout.BoxWithConstraintsScope
+ ) == true
+ ) {
+ foundValidReference = true
+ }
+
+ return foundValidReference
+ }
+ })
+ if (!foundValidReference) {
+ context.report(
+ UnusedConstraintsParameter,
+ node,
+ context.getLocation(contentArgument),
+ "BoxWithConstraints scope is not used"
+ )
+ }
+ }
+ }
+
+ companion object {
+ val UnusedConstraintsParameter = Issue.create(
+ "UnusedBoxWithConstraintsScope",
+ "BoxWithConstraints content should use the constraints provided " +
+ "via BoxWithConstraintsScope",
+ "The `content` lambda in BoxWithConstraints has a scope " +
+ "which will include the incoming constraints. If this " +
+ "scope is ignored, then the cost of subcomposition is being wasted and " +
+ "this BoxWithConstraints should be replaced with a Box.",
+ Category.CORRECTNESS, 3, Severity.ERROR,
+ Implementation(
+ BoxWithConstraintsDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
diff --git a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationIssueRegistry.kt b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationIssueRegistry.kt
index 2a936c1..d3de50b 100644
--- a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationIssueRegistry.kt
+++ b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationIssueRegistry.kt
@@ -32,7 +32,8 @@
override val issues get() = listOf(
LazyLayoutStateReadInCompositionDetector.FrequentlyChangedStateReadInComposition,
UnrememberedMutableInteractionSourceDetector.UnrememberedMutableInteractionSource,
- NonLambdaOffsetModifierDetector.UseOfNonLambdaOverload
+ NonLambdaOffsetModifierDetector.UseOfNonLambdaOverload,
+ BoxWithConstraintsDetector.UnusedConstraintsParameter
)
override val vendor = Vendor(
vendorName = "Jetpack Compose",
diff --git a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationNames.kt b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationNames.kt
index b6a07ab..c87171b 100644
--- a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationNames.kt
+++ b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/FoundationNames.kt
@@ -41,5 +41,7 @@
val PackageName = Package(FoundationNames.PackageName, "layout")
val Offset = Name(PackageName, "offset")
val AbsoluteOffset = Name(PackageName, "absoluteOffset")
+ val BoxWithConstraints = Name(PackageName, "BoxWithConstraints")
+ val BoxWithConstraintsScope = Name(PackageName, "BoxWithConstraintsScope")
}
}
diff --git a/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/BoxWithConstraintsDetectorTest.kt b/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/BoxWithConstraintsDetectorTest.kt
new file mode 100644
index 0000000..8052414
--- /dev/null
+++ b/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/BoxWithConstraintsDetectorTest.kt
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lint
+
+import androidx.compose.lint.test.Stubs
+import androidx.compose.lint.test.bytecodeStub
+import androidx.compose.lint.test.kotlinAndBytecodeStub
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+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
+
+private val ExternalModuleFunctionStub = kotlinAndBytecodeStub(
+ filename = "Other.kt",
+ filepath = "bar/compose",
+ checksum = 0xad5be2a5,
+ source = """
+ package bar.compose
+
+ import androidx.compose.foundation.layout.BoxWithConstraints
+ import androidx.compose.foundation.layout.BoxWithConstraintsScope
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun BoxWithConstraintsScope.Other() {}
+
+ @Composable
+ fun UseThis(scope: BoxWithConstraintsScope) {}
+
+ @Composable
+ fun Test() {
+ BoxWithConstraints {
+ UseThis(scope = this)
+ }
+ }
+ """.trimIndent(),
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGJ2KM3AZcWllJiXUpSfmVKhl5yfW5BfnKqX
+ ll+al5JYkpmfp5eTWJlfWiIk4pRfEZ5ZkuGcn1dcUpSYmVdS7F3CJcbFnZRY
+ BNMmxO5fkpFaBBTn5WJOy88XYgtJLS7xLlFi0GIAALeBd4B7AAAA
+ """,
+ """
+ bar/compose/OtherKt$Test$1.class:
+ H4sIAAAAAAAA/6VU33MTVRT+7ibNJtvFlCLQFgSVCGmrbIv4i8RiCa2sxOCY
+ Usfp083m0myzubezezdT3vrm/+FfIDojjM44HR/9oxzP3cYQhuKDPOTck3PP
+ +c53zv2Sv/7+7Q8AN3GfYaHDYy9Qg32VCO+B7on4vq5siURXVm0whh+afaWj
+ UHp7w4EXSi1iySOvyQedLq9N3j1KZaBDJRNvc+St1ptcdmMVdg/GHR6pVHa5
+ ufUi/lil2rujDr4Lda9BlTrm1CFpB2pfjLEfylDX1mrE9NVEbOQZLv03GRsF
+ hkI9JLg1hlx1cZshX/UXt10U4TiYwjQFdC9MGC42X70VYlII5VD1BcPt6utM
+ aBhcaap419sTumNuEo9LqTQ/pt5SupVGETWcqxhelZeBiph9cfDxYnypYwIM
+ g8TGmwxng54I+iPEb3jMB4ISGa5Vm3t8yImq3PUedPZEoGsTkbYB2a2ZJZ3D
+ eQdnMcdw5oTl2FhgsB8mYouIuriIGQcX8BbDa2mA4eoJ/BZfDjHc+v9tbLzj
+ omwYW7jCMD0hPBvvMRT9VntrvdXYYDj1gipdXEO1hKtYZLD2VxlmT2JWrAdR
+ pjojtJJpct0UvlEib4Xh9L+QXwvNiS+nEmswzNEPlBljM7C+cXIUPwiNR1VW
+ l9pVjg5d5+jQsWYsx5qzyJ05OlywVtiStWLdc/78sWAVTVX3Bk1V51LJxwOV
+ JiR/kNSNnF3cRokoZq94va8p3FBdYSZRAY+2eRzyTiS2jGEoN0MpWumgI+JR
+ pPJtKnU4EL4chklIobG01p8LmcH1pRRxI+JJIuhreUMGkUpIWTRzT3UZSu1w
+ V3KdxoTptFUaB2IzNA3mRw22j+EnULFC+5uiQehvCvNmobSZPH1oyRS5Q16F
+ MmhWFJbyT+E+MRtFg6x7HMWprOa0eXvKNBUNOi06p5dnzzzD/PIzXDJlFu6S
+ NU9XoFSHSgzMuePUEYzxZnGZoDdGr0ZpmFkn9LdHfL4YobtLy0d491dUfsbS
+ T2P4QsaqPAHtjqFdLON9ui/ig/F057Mcavs7rO+fwvsFq0+ywBQ2M7ZslDCH
+ L7PVXCAC97J2OfjZuY6v6PyEMm9Q1Yc7yPm46eMjsvjYp4tPfXyGWztgCWqo
+ 7yCf4PMEawkuJyj/AyfuA+BHBgAA
+ """,
+ """
+ bar/compose/OtherKt.class:
+ H4sIAAAAAAAA/6VUWVMTQRD+ZhNICEGWcMglHgQJqCzggRo8MCXlljFYglgl
+ T5PNAAubWWpnQuEbf0mfKB8snv1Rlj1LRDnUKk1Vpo/p/rr7m06+fvv8BcAd
+ FBm6qzxyvLC+EyrhLOlNEb3UKTAGe4vvcifgcsNZqm4Jj7wJhpY4hOFJocxl
+ LQr92t5x9nrYkDWu/VBS2oewoZ1n4d47X2+WQql0xH2p1bIX7ojixCrD2FmA
+ qCG1XxdOKbZ5NRDU4Gg5jDacLaGrBkE5XMpQx1WUUwl1pREEFNU6rzd99TiN
+ NoaR7VAHvnS2dusO1RSR5IHjSh1Ruu+pFNoZer1N4W0381/ziNeFNnONF8qn
+ 5y7+4lk2IBvUfxYduJBBFp0M7XlTO99kZv5/iGFIvVViheCIamVcafQwJFeE
+ 0gyJgiFu8JwXy5uA/EwK/Qxpt7K8slApPWcYLv8+tpjFIIbaMIDhk5StN6R3
+ RO9iUyPcEYa5f5rLbNMVavrsTb4m1nkjoLnmCu/Lf26g6J59FvMI1zCawVXk
+ Gbp+ILwSmlNTnLi06rsJ2nNmjhQD2zaKRf4932jTpNVmGJ4e7tuZw/2MZVsZ
+ K03ffovMwbRNhzXNXqQGbdsy2myrnSCZJE/WbjGeydgyOLOMCiEdczy1TUMl
+ S2FNMHSWfSkqjXpVRCtmoxly5dDjwSqPfGM3nUNvjnbflbu+8sm18HPNGfKn
+ b4839kRY1pVSRKWAKyXIzCyHjcgTi74pMNCEWD0DjxlYSMJ8ErQNLWgleZss
+ 42eGuslc5gD2R0Me/WmAAugHhzTukp49CkEXciTvNW9TJOearFMgCLf7PNze
+ c3DbT+D2/A23Dxcp3eBONKfoSHzCpUNcTrIDjBl0FqNnKAzoJOTcCbwE7se3
+ xFGc3o8HcUezeEjyEfmvEynja0i4KLiYoBOTLm7gpotbmFoDU3AwvYZWhT5i
+ U6FboUchp9DyHfyqvvRqBQAA
+ """
+)
+
+@RunWith(JUnit4::class)
+class BoxWithConstraintsDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = BoxWithConstraintsDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(BoxWithConstraintsDetector.UnusedConstraintsParameter)
+
+ private val BoxWithConstraintsStub = bytecodeStub(
+ filename = "BoxWithConstraints.kt",
+ filepath = "androidx/compose/foundation/layout",
+ checksum = 0xddc1f733,
+ source =
+ """
+ package androidx.compose.foundation.layout
+
+ import androidx.compose.runtime.Composable
+
+ interface Constraints {
+ val minWidth: Int
+ }
+ interface Dp {}
+ interface BoxWithConstraintsScope {
+ val constraints: Constraints
+ val minWidth: Dp
+ val maxWidth: Dp
+ val minHeight: Dp
+ val maxHeight: Dp
+ }
+
+ @Composable
+ fun BoxWithConstraints(
+ propagateMinConstraints: Boolean = false,
+ content: @Composable BoxWithConstraintsScope.() -> Unit
+ ) {}
+ """.trimIndent(),
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGJ2KM3AZcWllJiXUpSfmVKhl5yfW5BfnKqX
+ ll+al5JYkpmfp5eTWJlfWiIk4pRfEZ5ZkuGcn1dcUpSYmVdS7F3CxcvFnJaf
+ L8QWklpc4l2ixKDFAAD5174zYwAAAA==
+ """,
+ """
+ androidx/compose/foundation/layout/BoxWithConstraintsKt.class:
+ H4sIAAAAAAAA/6VTXU8TQRQ9sy394qsU+SqIKCAgwhZiwkOJiRKJjQWNRYzw
+ NGyHsrSdaXZnCbwY/oav/gPfiA+G+OiPMt7ZtlrsA4lusnfunTl77p17z/74
+ +fUbgCdYZ9jgsuwpt3xuO6reUL6wj1Ugy1y7Sto1fqECbT9X5+9dfbKlpK89
+ 7krtv9JxMIb0KT/jhJIV+/XRqXBoN8KQ6cYzzC0eFKtK11xpn57V7eNAOiaF
+ b2+3vLX80j5D41bY5krxn0ouOaoh8m3yd9LV+adhyvluPi+Q2q0LeyuM+VFN
+ 5Blmi8qr2KdCHxlC3+ZSKs2b1e0qvRvUaoSKO0pqIXUCKYbpjqtQDcKTvGYX
+ pPboe9fx4+hjGHFOhFNtEbzhHq8LAjIsLBb/7m6+Y6dkSCp0gT4MYDCFfqQZ
+ xhqeavAK12LHlTfazw4YZm4bAEO2u29zZXHMg5omqdw+wkJ3zabCHsRSsDDG
+ MNRm2BGa08g4JbXqZxGSIzMmTqVWjWPR/rlrvBx55TWGD9eX06nry5SVtppL
+ b7iMW51vNpe+vsxaObaeSBCQvMj6VDqaHc9EM1YuFlqW6/n+OWYl4qFNvIyb
+ BPQrgKTbLq+zKZv/IzgSQZvzxTkJw6dv2uR7FyFgpPvb1Sr1O7qlyoJhsOhK
+ sRvUj4S3Z6RoqlQOr+1zzzVxazNZciuS68Ajf/JtU8AFeeb6Lh0/+6NV+hX/
+ Pv2tuhuw/pLmTnWHN1oJUiUVeI7Ydk0w0eLY7+LHGk06CvNYmDCjp2iJojzF
+ lhnxcqb3CkNfQsAjsjFqfAwjWCZ/tAlBBsMhRRwp3KHzxyE6jpUWPkHrKr1J
+ A6fLA+kkUYySb3JttGoYmIp+/ISeSD67fIXxZkqbbAQsEeYegJFYhjiHiTND
+ x7kQtEjXALaJzlwhe4hIAZMFTJHF3QKmca+AGdw/BPPxALOHSPro8THnIxPa
+ lI95Hw99JHws/AJecgBzcAUAAA==
+ """,
+ """
+ androidx/compose/foundation/layout/BoxWithConstraintsScope.class:
+ H4sIAAAAAAAA/5WSzW7aQBDH/2vA2IYQJ21aQvqdSm0uNUU9tb30Q1WRSCol
+ UhOJkzEOLNi7iF0QvfEUfYAe+hA9VCjHPlTVMSUBQSpRazWz89N/dtYz++v3
+ j58AXuAxw0tfNPuSN0deIOOeVKF3Lgei6WsuhRf5X+RAe2/l6JTr9jsplO77
+ XGh1EshemAVjcDv+0CehaHmfGp0w0FmkGAqtUC/IGSpPD2prVFrIecWwX5P9
+ ltcJdSNByvOFkHqqV96R1EeDKCJVjmodcnHKm7rNcLBeofe9y0x/NMvM/z3n
+ Y8hbbT0L/dFluFXrSh1x4R2G2qezfMo34mGK+sgSk2VgXUIjnkRl2jWfM3yd
+ jEuOUTQcw52MHVrTvZWaecs6L07GFaPMjrddo2SUU2cX39MX30yzlLbSboao
+ STS7QC3XJuos0dyU5pfoxpQWluimaye3qzC8XqdT/xg+/T6og8HikMv/P2I7
+ nrf4ydqDs+KrqdnxfGRWfPUMdlav/axLkr3jgdA8DqtiyBVvROGb+ZticE7k
+ oB+EH3gUMuzOpJ9XhCb1D2kkXybNkIFJrXhIUeKzADEL9gpzrmG5a1h+mVG1
+ R1P7APvk60Q3qGqhjlQVm1W4ZLGVmO0qbuAmCRR2cKsOV+G2QlFhV6GkkFEw
+ FfYU7ijkFWyFuwqOwj2FnMJ9BesPb1xrKxsEAAA=
+ """,
+ """
+ androidx/compose/foundation/layout/Constraints.class:
+ H4sIAAAAAAAA/5WPz07CQBDGv9mWUot/CooiT6AXWonxYjyoiQkJxAQTMeFU
+ aIEVumvYheCNZ/HgQ3gwhKMPZdxyMF7dbH47szO73zdf3x+fAM5xRKhFIp5K
+ Hi+CvkxfpEqCgZyJONJcimASvcqZDm6lUHoacaFVHkTwn6N5ZIpiGNz3npO+
+ zsMiFIaJbnHR4bEeEayT0wah2BxLPeEiaCU6Mp9GlwSWzi2jThnyBBqbqwXP
+ stBE8RnhYrUseazCPOavlp7ZzHc95jJ3UFkt6yykdslnVRZaT+t3e/3mOFXb
+ tf1c9rpOCJv/G8lYAsFNf62Xb+Siw/XoT09trAneg5xN+8kdnySE4/ZMaJ4m
+ j1zx3iS5FkLqjYJyjA/YyBbZhBwcEzGUNzzAoTmvjGDeVNwurAa2GvAMUciw
+ 3cAOdrsghT34XTgKRYWSwv6GOQXnB5/tt/C/AQAA
+ """,
+ """
+ androidx/compose/foundation/layout/Dp.class:
+ H4sIAAAAAAAA/41Oy07DQAwcb6Ep4ZXykMoHIG6krXrjxENIlYqQQIJDT9tm
+ C9sku1V2U4Vbv4sD6pmPQjjlB7Cl8diWZ/z98/kFYIATwrk0SWF1UsVTmy+s
+ U/HMliaRXlsTZ/LDlj6+WwQgQjSXS8kz8xY/TuZq6gM0CO1Ran2mTfygvOQ7
+ eUUQ+bLBBlRDQKCUR5Wuuy6zpEc4Xa9aoeiIUETMZp31qi+6VC/7hIvRv55i
+ I7DSja1etX+/tcb5Qmrj3WXqCeGzLYuputeZIpw9lcbrXL1opyeZujbG+o2a
+ a7IntvAXAkcbbOOYa4/VtzmbYzSGCIZoMWKnhnCIXeyNQQ77OBhDOBw6RL/m
+ nxtjWQEAAA==
+ """
+ )
+
+ @Test
+ fun unreferencedConstraints() {
+ lint().files(
+ kotlin(
+ """
+ package foo
+
+ import androidx.compose.foundation.layout.BoxWithConstraints
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Test() {
+ val foo = 123
+ BoxWithConstraints { /**/ }
+ BoxWithConstraints { foo }
+ BoxWithConstraints(content = { /**/ })
+ BoxWithConstraints(propagateMinConstraints = false, content = { /**/ })
+ }
+ """.trimIndent()
+ ),
+ BoxWithConstraintsStub,
+ Stubs.Composable,
+ )
+ .run()
+ .expect(
+ """
+src/foo/test.kt:9: Error: BoxWithConstraints scope is not used [UnusedBoxWithConstraintsScope]
+ BoxWithConstraints { /**/ }
+ ~~~~~~~~
+src/foo/test.kt:10: Error: BoxWithConstraints scope is not used [UnusedBoxWithConstraintsScope]
+ BoxWithConstraints { foo }
+ ~~~~~~~
+src/foo/test.kt:11: Error: BoxWithConstraints scope is not used [UnusedBoxWithConstraintsScope]
+ BoxWithConstraints(content = { /**/ })
+ ~~~~~~~~
+src/foo/test.kt:12: Error: BoxWithConstraints scope is not used [UnusedBoxWithConstraintsScope]
+ BoxWithConstraints(propagateMinConstraints = false, content = { /**/ })
+ ~~~~~~~~
+4 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun referencedConstraints() {
+ lint().files(
+ kotlin(
+ """
+ package foo
+
+ import androidx.compose.foundation.layout.BoxWithConstraints
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Foo(content: @Composable ()->Unit) {}
+ @Composable
+ fun Bar() {}
+
+ @Composable
+ fun Test() {
+ BoxWithConstraints { constraints }
+ BoxWithConstraints { constraints.minWidth }
+ BoxWithConstraints { minWidth }
+ BoxWithConstraints { maxWidth }
+ BoxWithConstraints { minHeight }
+ BoxWithConstraints { maxHeight }
+ BoxWithConstraints(content = { maxWidth })
+ BoxWithConstraints(propagateMinConstraints = false, content = { minWidth })
+ BoxWithConstraints {
+ if (constraints.minWidth > 100) {
+ Foo {}
+ } else {
+ Bar()
+ }
+ }
+ BoxWithConstraints {
+ Foo {
+ constraints
+ }
+ }
+ }
+ """.trimIndent()
+ ),
+ BoxWithConstraintsStub,
+ Stubs.Composable,
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun referencedConstraintsViaThis() {
+ lint().files(
+ kotlin(
+ """
+ package foo
+
+ import androidx.compose.foundation.layout.BoxWithConstraints
+ import androidx.compose.foundation.layout.BoxWithConstraintsScope
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun PassOnScope(scope: BoxWithConstraintsScope) {}
+
+ @Composable
+ fun Test() {
+ BoxWithConstraints {
+ PassOnScope(scope = this)
+ }
+ }
+ """.trimIndent()
+ ),
+ BoxWithConstraintsStub,
+ Stubs.Composable,
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun referencedConstraintsViaReceiver() {
+ lint().files(
+ kotlin(
+ """
+ package foo
+
+ import androidx.compose.foundation.layout.BoxWithConstraints
+ import androidx.compose.foundation.layout.BoxWithConstraintsScope
+ import androidx.compose.runtime.Composable
+ val lambda: BoxWithConstraintsScope.() -> Unit = {}
+ fun BoxWithConstraintsScope.Func() { constraints.minWidth }
+ @Composable
+ fun BoxWithConstraintsScope.ComposableFunc() { constraints.minWidth }
+ @Composable
+ fun Foo(content: @Composable ()->Unit) {}
+ val BoxWithConstraintsScope.prop: Int
+ get() = 0
+
+ @Composable
+ fun Test() {
+ BoxWithConstraints {
+ lambda()
+ }
+ BoxWithConstraints {
+ Func()
+ }
+ BoxWithConstraints {
+ ComposableFunc()
+ }
+ BoxWithConstraints {
+ prop
+ }
+ BoxWithConstraints {
+ Foo { [email protected]() }
+ }
+ }
+ """.trimIndent()
+ ),
+ BoxWithConstraintsStub,
+ Stubs.Composable,
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun referencedConstraintsInExternalModule() {
+ lint().files(
+ kotlin(
+ """
+ package foo
+
+ import androidx.compose.foundation.layout.BoxWithConstraints
+ import androidx.compose.foundation.layout.BoxWithConstraintsScope
+ import androidx.compose.runtime.Composable
+ import bar.compose.Other
+
+ @Composable
+ fun Test() {
+ BoxWithConstraints {
+ Other()
+ }
+ }
+ """.trimIndent()
+ ),
+ BoxWithConstraintsStub,
+ ExternalModuleFunctionStub.bytecode,
+ Stubs.Composable
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun referencedThisInExternalModule() {
+ lint().files(
+ ExternalModuleFunctionStub.kotlin,
+ BoxWithConstraintsStub,
+ Stubs.Composable
+ )
+ .run()
+ .expectClean()
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt
index d4714d9..244dd6b 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt
@@ -75,6 +75,8 @@
@Composable
private fun NonLazyRow(entry: NestedListEntry) {
+ // Need the nested subcompose layout for testing
+ @Suppress("UnusedBoxWithConstraintsScope")
BoxWithConstraints {
Row(
Modifier