1 /*
<lambda>null2 * Copyright 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.lint
18
19 import androidx.compose.lint.Name
20 import androidx.compose.lint.Package
21 import androidx.compose.lint.isInPackageName
22 import androidx.compose.lint.isInvokedWithinComposable
23 import com.android.SdkConstants
24 import com.android.tools.lint.client.api.UElementHandler
25 import com.android.tools.lint.detector.api.Category
26 import com.android.tools.lint.detector.api.Context
27 import com.android.tools.lint.detector.api.Detector
28 import com.android.tools.lint.detector.api.Implementation
29 import com.android.tools.lint.detector.api.Incident
30 import com.android.tools.lint.detector.api.Issue
31 import com.android.tools.lint.detector.api.JavaContext
32 import com.android.tools.lint.detector.api.LintMap
33 import com.android.tools.lint.detector.api.Scope
34 import com.android.tools.lint.detector.api.Severity
35 import com.android.tools.lint.detector.api.SourceCodeScanner
36 import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
37 import com.android.utils.iterator
38 import com.android.xml.AndroidManifest
39 import com.intellij.psi.PsiMember
40 import java.util.EnumSet
41 import org.jetbrains.uast.UElement
42 import org.jetbrains.uast.UQualifiedReferenceExpression
43 import org.jetbrains.uast.UVariable
44 import org.jetbrains.uast.getContainingUFile
45 import org.jetbrains.uast.matchesQualified
46 import org.jetbrains.uast.skipParenthesizedExprDown
47 import org.w3c.dom.Element
48
49 /**
50 * Detector that warns for calls to Configuration.screenWidthDp/screenHeightDp inside composition,
51 * or calls to Configuration.screenWidthDp/screenHeightDp made on a Configuration object that was
52 * retrieved using LocalConfiguration.current (since that also comes from composition). Instead
53 * LocalWindowInfo.current.containerSize should be used.
54 */
55 class ConfigurationScreenWidthHeightDetector : Detector(), SourceCodeScanner {
56 override fun getApplicableUastTypes() = listOf(UQualifiedReferenceExpression::class.java)
57
58 override fun createUastHandler(context: JavaContext): UElementHandler =
59 object : UElementHandler() {
60 override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
61 val resolved = node.resolve() as? PsiMember ?: return
62 val name = resolved.name
63 if (name != ScreenWidthDp && name != ScreenHeightDp) return
64
65 val containingClass = resolved.containingClass ?: return
66 if (
67 containingClass.name != Configuration.shortName ||
68 !containingClass.isInPackageName(Configuration.packageName)
69 )
70 return
71
72 // If we are invoking this inside a composable function, report
73 if (node.isInvokedWithinComposable()) {
74 report(name, context, node)
75 return
76 }
77
78 // Otherwise, check to see if the configuration object was retrieved via
79 // LocalConfiguration.current. In which case, this could be replaced with
80 // LocalWindowInfo, so we should still warn. For other cases the configuration might
81 // come from outside Compose, so it can't be replaced.
82 // Simple check to see if the `configuration` receiver is a variable defined as val
83 // someVariable = LocalConfiguration.current
84 val configurationSource =
85 (node.receiver.skipParenthesizedExprDown().tryResolveUDeclaration()
86 as? UVariable)
87 ?.uastInitializer ?: return
88 if (configurationSource.matchesQualified("LocalConfiguration.current")) {
89 report(name, context, node)
90 }
91 }
92 }
93
94 /** b/333784604 Ignore wear since this is the recommended API on wear */
95 override fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean {
96 return !isWearProject(context)
97 }
98
99 private fun report(referencedFieldName: String, context: JavaContext, node: UElement) {
100 // b/333784604 Ignore wear since this is the recommended API on wear. We check in
101 // filterIncident to see if the main project is wear, but this won't work for isolated
102 // libraries that have wear dependencies, yet aren't part of an app project that targets
103 // wear. As a best effort for this case, we just see if there are any wear imports in
104 // the file with the error, and avoid reporting in that case.
105 val hasWearImport =
106 node.getContainingUFile()?.imports?.any { import ->
107 val importString = import.importReference?.asSourceString()
108 importString?.contains(WearPackage) == true
109 } == true
110 if (hasWearImport) return
111 val incident =
112 Incident(
113 issue = ConfigurationScreenWidthHeight,
114 scope = node,
115 location = context.getNameLocation(node),
116 message =
117 "Using Configuration.$referencedFieldName instead of LocalWindowInfo.current.containerSize"
118 )
119 context.report(incident, map())
120 }
121
122 companion object {
123 private val ResPackage = Package("android.content.res")
124 private val Configuration = Name(ResPackage, "Configuration")
125 private const val ScreenWidthDp = "screenWidthDp"
126 private const val ScreenHeightDp = "screenHeightDp"
127
128 val ConfigurationScreenWidthHeight =
129 Issue.create(
130 "ConfigurationScreenWidthHeight",
131 "Using Configuration.screenWidthDp/screenHeightDp instead of LocalWindowInfo.current.containerSize",
132 "Configuration.screenWidthDp and Configuration.screenHeightDp have different insets behaviour depending on target SDK version, and are rounded to the nearest Dp. This means that using these values in composition to size a layout can result in issues, as these values do not accurately represent the actual available window size. Instead it is recommended to use WindowInfo.containerSize which accurately represents the window size.",
133 Category.CORRECTNESS,
134 3,
135 Severity.WARNING,
136 Implementation(
137 ConfigurationScreenWidthHeightDetector::class.java,
138 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
139 )
140 )
141 }
142 }
143
144 private const val WearPackage = "androidx.wear"
145
146 // TODO: b/386335480 use lint API when available
147 // This is copied from Lint's WearDetector until it is made public / an equivalent API is provided
148
isWearProjectnull149 private fun isWearProject(context: Context) =
150 containsWearFeature(context.mainProject.mergedManifest?.documentElement)
151
152 private fun containsWearFeature(manifest: Element?): Boolean {
153 if (manifest == null) {
154 return false
155 }
156 for (element in manifest) {
157 if (isWearFeature(element)) return true
158 }
159 return false
160 }
161
isWearFeaturenull162 private fun isWearFeature(element: Element) =
163 element.tagName == SdkConstants.TAG_USES_FEATURE &&
164 element
165 .getAttributeNS(SdkConstants.ANDROID_URI, AndroidManifest.ATTRIBUTE_NAME)
166 .equals(FEATURE_WATCH)
167
168 private const val FEATURE_WATCH = "android.hardware.type.watch"
169