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