1 /*
2 * Copyright 2020 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 @file:Suppress("UnstableApiUsage")
18
19 package androidx.compose.ui.lint
20
21 import androidx.compose.lint.Names
22 import androidx.compose.lint.inheritsFrom
23 import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ModifierFactoryReturnType
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.Detector
27 import com.android.tools.lint.detector.api.Implementation
28 import com.android.tools.lint.detector.api.Issue
29 import com.android.tools.lint.detector.api.JavaContext
30 import com.android.tools.lint.detector.api.LintFix
31 import com.android.tools.lint.detector.api.Scope
32 import com.android.tools.lint.detector.api.Severity
33 import com.android.tools.lint.detector.api.SourceCodeScanner
34 import com.intellij.psi.PsiClass
35 import com.intellij.psi.PsiType
36 import java.util.EnumSet
37 import org.jetbrains.kotlin.analysis.api.analyze
38 import org.jetbrains.kotlin.analysis.api.calls.KtCall
39 import org.jetbrains.kotlin.analysis.api.calls.KtCallableMemberCall
40 import org.jetbrains.kotlin.analysis.api.calls.KtImplicitReceiverValue
41 import org.jetbrains.kotlin.analysis.api.calls.singleCallOrNull
42 import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol
43 import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol
44 import org.jetbrains.kotlin.analysis.api.symbols.KtReceiverParameterSymbol
45 import org.jetbrains.kotlin.idea.references.mainReference
46 import org.jetbrains.kotlin.psi.KtCallExpression
47 import org.jetbrains.kotlin.psi.KtCallableDeclaration
48 import org.jetbrains.kotlin.psi.KtDeclaration
49 import org.jetbrains.kotlin.psi.KtDeclarationWithBody
50 import org.jetbrains.kotlin.psi.KtFunction
51 import org.jetbrains.kotlin.psi.KtNullableType
52 import org.jetbrains.kotlin.psi.KtParameter
53 import org.jetbrains.kotlin.psi.KtProperty
54 import org.jetbrains.kotlin.psi.KtPropertyAccessor
55 import org.jetbrains.kotlin.psi.KtThisExpression
56 import org.jetbrains.kotlin.psi.KtUserType
57 import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
58 import org.jetbrains.uast.UCallExpression
59 import org.jetbrains.uast.UMethod
60 import org.jetbrains.uast.UThisExpression
61 import org.jetbrains.uast.toUElement
62 import org.jetbrains.uast.tryResolve
63 import org.jetbrains.uast.visitor.AbstractUastVisitor
64
65 /**
66 * [Detector] that checks functions returning Modifiers for consistency with guidelines.
67 * - Modifier factory functions should return Modifier as their type, and not a subclass of Modifier
68 * - Modifier factory functions should be defined as an extension on Modifier to allow fluent
69 * chaining
70 * - Modifier factory functions should not be marked as @Composable, and should use `composed`
71 * instead
72 * - Modifier factory functions should reference the receiver parameter inside their body to make
73 * sure they don't drop old Modifiers in the chain
74 */
75 class ModifierDeclarationDetector : Detector(), SourceCodeScanner {
getApplicableUastTypesnull76 override fun getApplicableUastTypes() = listOf(UMethod::class.java)
77
78 override fun createUastHandler(context: JavaContext) =
79 object : UElementHandler() {
80 override fun visitMethod(node: UMethod) {
81 // Ignore functions that do not return
82 val returnType = node.returnType ?: return
83
84 // Ignore functions that do not return Modifier or something implementing Modifier
85 if (!returnType.inheritsFrom(Names.Ui.Modifier)) return
86
87 // Ignore ParentDataModifiers - this is a special type of Modifier where the type is
88 // used to provide data for use in layout, so we don't want to warn here.
89 if (returnType.inheritsFrom(Names.Ui.Layout.ParentDataModifier)) return
90
91 val source = node.sourcePsi
92
93 // If this node is a property that is a constructor parameter, ignore it.
94 if (source is KtParameter) return
95
96 // Ignore properties in some cases
97 if (source is KtProperty) {
98 // If this node is inside a class or object, ignore it.
99 if (source.containingClassOrObject != null) return
100 // If this node is a var, ignore it.
101 if (source.isVar) return
102 // If this node is a val with no getter, ignore it.
103 if (source.getter == null) return
104 }
105 if (source is KtPropertyAccessor) {
106 // If this node is inside a class or object, ignore it.
107 if (source.property.containingClassOrObject != null) return
108 // If this node is a getter on a var, ignore it.
109 if (source.property.isVar) return
110 }
111
112 node.checkReturnType(context, returnType)
113 node.checkReceiver(context)
114 }
115 }
116
117 companion object {
118 val ModifierFactoryReturnType =
119 Issue.create(
120 "ModifierFactoryReturnType",
121 "Modifier factory functions should return Modifier",
122 "Modifier factory functions should return Modifier as their type, and not a " +
123 "subtype of Modifier (such as Modifier.Element).",
124 Category.CORRECTNESS,
125 3,
126 Severity.WARNING,
127 Implementation(
128 ModifierDeclarationDetector::class.java,
129 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
130 )
131 )
132
133 val ModifierFactoryExtensionFunction =
134 Issue.create(
135 "ModifierFactoryExtensionFunction",
136 "Modifier factory functions should be extensions on Modifier",
137 "Modifier factory functions should be defined as extension functions on" +
138 " Modifier to allow modifiers to be fluently chained.",
139 Category.CORRECTNESS,
140 3,
141 Severity.WARNING,
142 Implementation(
143 ModifierDeclarationDetector::class.java,
144 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
145 )
146 )
147
148 val ModifierFactoryUnreferencedReceiver =
149 Issue.create(
150 "ModifierFactoryUnreferencedReceiver",
151 "Modifier factory functions must use the receiver Modifier instance",
152 "Modifier factory functions are fluently chained to construct a chain of " +
153 "Modifier objects that will be applied to a layout. As a result, each factory " +
154 "function *must* use the receiver `Modifier` parameter, to ensure that the " +
155 "function is returning a chain that includes previous items in the chain. Make " +
156 "sure the returned chain either explicitly includes `this`, such as " +
157 "`return this.then(MyModifier)` or implicitly by returning a chain that starts " +
158 "with an implicit call to another factory function, such as " +
159 "`return myModifier()`, where `myModifier` is defined as " +
160 "`fun Modifier.myModifier(): Modifier`.",
161 Category.CORRECTNESS,
162 3,
163 Severity.ERROR,
164 Implementation(
165 ModifierDeclarationDetector::class.java,
166 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
167 )
168 )
169 }
170 }
171
172 /** @see [ModifierDeclarationDetector.ModifierFactoryExtensionFunction] */
UMethodnull173 private fun UMethod.checkReceiver(context: JavaContext) {
174 fun report(lintFix: LintFix? = null) {
175 context.report(
176 ModifierDeclarationDetector.ModifierFactoryExtensionFunction,
177 this,
178 context.getNameLocation(this),
179 "Modifier factory functions should be extensions on Modifier",
180 lintFix
181 )
182 }
183
184 val source =
185 when (val source = sourcePsi) {
186 is KtFunction -> source
187 is KtPropertyAccessor -> source.property
188 else -> return
189 }
190
191 val receiverTypeReference = source.receiverTypeReference
192
193 // No receiver
194 if (receiverTypeReference == null) {
195 val name = source.nameIdentifier!!.text
196 report(
197 LintFix.create()
198 .replace()
199 .name("Add Modifier receiver")
200 .range(context.getLocation(source))
201 .text(name)
202 .with("${Names.Ui.Modifier.shortName}.$name")
203 .autoFix()
204 .build()
205 )
206 } else {
207 val receiverType =
208 when (val receiverType = receiverTypeReference.typeElement) {
209 is KtUserType -> receiverType
210 // We could have a nullable receiver - try to unwrap it.
211 is KtNullableType -> receiverType.innerType as? KtUserType ?: return
212 // Safe return - shouldn't happen
213 else -> return
214 }
215 val receiverShortName = receiverType.referencedName
216 // Try to resolve the class definition of the receiver
217 val receiverFqn =
218 (receiverType.referenceExpression.toUElement()?.tryResolve().toUElement() as? PsiClass)
219 ?.qualifiedName
220 val hasModifierReceiver =
221 if (receiverFqn != null) {
222 // If we could resolve the class, match fqn
223 receiverFqn == Names.Ui.Modifier.javaFqn
224 } else {
225 // Otherwise just try and match the short names
226 receiverShortName == Names.Ui.Modifier.shortName
227 }
228 if (!hasModifierReceiver) {
229 report(
230 LintFix.create()
231 .replace()
232 .name("Change receiver to Modifier")
233 .range(context.getLocation(source))
234 .text(receiverShortName)
235 .with(Names.Ui.Modifier.shortName)
236 .autoFix()
237 .build()
238 )
239 } else {
240 // Ignore interface / abstract methods with no body
241 if (uastBody != null) {
242 ensureReceiverIsReferenced(context)
243 }
244 }
245 }
246 }
247
248 /** See [ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver] */
ensureReceiverIsReferencednull249 private fun UMethod.ensureReceiverIsReferenced(context: JavaContext) {
250 val factoryMethod = this
251 var isReceiverReferenced = false
252 accept(
253 object : AbstractUastVisitor() {
254 /**
255 * Checks for calls to functions with an implicit receiver (member / extension function)
256 * that use the Modifier receiver from the outer factory function.
257 */
258 override fun visitCallExpression(node: UCallExpression): Boolean {
259 val ktCallExpression =
260 node.sourcePsi as? KtCallExpression ?: return isReceiverReferenced
261 analyze(ktCallExpression) {
262 val ktCall = ktCallExpression.resolveCall()?.singleCallOrNull<KtCall>()
263 val callee = (ktCall as? KtCallableMemberCall<*, *>)?.partiallyAppliedSymbol
264 val receiver =
265 (callee?.extensionReceiver ?: callee?.dispatchReceiver)
266 // Explicit receivers of `this` are handled separately in
267 // visitThisExpression -
268 // that lets us be more defensive and avoid warning for cases like passing
269 // `this` as a parameter to a function / class where we may run into false
270 // positives if we only account for explicit receivers in a call expression.
271 as? KtImplicitReceiverValue ?: return isReceiverReferenced
272 val symbol = receiver.symbol as? KtReceiverParameterSymbol
273 // The symbol of the enclosing factory method
274 val enclosingMethodSymbol =
275 (factoryMethod.sourcePsi as? KtDeclaration)?.getSymbol()
276 as? KtFunctionSymbol
277 // If the receiver parameter symbol matches the outer modifier factory's
278 // symbol, then that means that the receiver for this call is the
279 // factory method, and not some other declaration that provides a modifier
280 // receiver.
281 if (symbol == enclosingMethodSymbol?.receiverParameter) {
282 isReceiverReferenced = true
283 // no further tree traversal, since we found receiver usage.
284 return true
285 }
286 }
287 return isReceiverReferenced
288 }
289
290 /**
291 * If `this` is explicitly referenced, and points to the receiver of the outer factory
292 * function, no error.
293 */
294 override fun visitThisExpression(node: UThisExpression): Boolean {
295 val ktThisExpression =
296 node.sourcePsi as? KtThisExpression ?: return isReceiverReferenced
297 analyze(ktThisExpression) {
298 val symbol = ktThisExpression.instanceReference.mainReference.resolveToSymbol()
299 val referredMethodSymbol =
300 when (symbol) {
301 is KtReceiverParameterSymbol -> symbol.owningCallableSymbol
302 is KtCallableSymbol -> symbol
303 else -> null
304 }
305 // The symbol of the enclosing factory method
306 val enclosingMethodSymbol =
307 (factoryMethod.sourcePsi as? KtDeclaration)?.getSymbol()
308 as? KtFunctionSymbol
309 // If the symbol `this` points to matches the enclosing factory method, then we
310 // consider the modifier receiver referenced. If the symbols do not match,
311 // `this` might point to an inner scope
312 if (referredMethodSymbol == enclosingMethodSymbol) {
313 isReceiverReferenced = true
314 // no further tree traversal, since we found receiver usage.
315 return true
316 }
317 }
318 return isReceiverReferenced
319 }
320 }
321 )
322 if (!isReceiverReferenced) {
323 context.report(
324 ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver,
325 this,
326 context.getNameLocation(this),
327 "Modifier factory functions must use the receiver Modifier instance"
328 )
329 }
330 }
331
332 /** @see [ModifierDeclarationDetector.ModifierFactoryReturnType] */
checkReturnTypenull333 private fun UMethod.checkReturnType(context: JavaContext, returnType: PsiType) {
334 fun report(lintFix: LintFix? = null) {
335 context.report(
336 ModifierFactoryReturnType,
337 this,
338 context.getNameLocation(this),
339 "Modifier factory functions should have a return type of Modifier",
340 lintFix
341 )
342 }
343
344 if (returnType.canonicalText == Names.Ui.Modifier.javaFqn) return
345
346 val source = sourcePsi
347 if (source is KtCallableDeclaration && source.returnTypeString != null) {
348 // Function declaration with an explicit return type, such as
349 // `fun foo(): Modifier.element = Bar`. Replace the type with `Modifier`.
350 report(
351 LintFix.create()
352 .replace()
353 .name("Change return type to Modifier")
354 .range(context.getLocation(this))
355 .text(source.returnTypeString)
356 .with(Names.Ui.Modifier.shortName)
357 .autoFix()
358 .build()
359 )
360 return
361 }
362 if (source is KtPropertyAccessor) {
363 // Getter declaration with an explicit return type on the getter, such as
364 // `val foo get(): Modifier.Element = Bar`. Replace the type with `Modifier`.
365 val getterReturnType = source.returnTypeReference?.text
366
367 if (getterReturnType != null) {
368 report(
369 LintFix.create()
370 .replace()
371 .name("Change return type to Modifier")
372 .range(context.getLocation(this))
373 .text(getterReturnType)
374 .with(Names.Ui.Modifier.shortName)
375 .autoFix()
376 .build()
377 )
378 return
379 }
380 // Getter declaration with an implicit return type from the property, such as
381 // `val foo: Modifier.Element get() = Bar`. Replace the type with `Modifier`.
382 val propertyType = source.property.returnTypeString
383
384 if (propertyType != null) {
385 report(
386 LintFix.create()
387 .replace()
388 .name("Change return type to Modifier")
389 .range(context.getLocation(source.property))
390 .text(propertyType)
391 .with(Names.Ui.Modifier.shortName)
392 .autoFix()
393 .build()
394 )
395 return
396 }
397 }
398 if (source is KtDeclarationWithBody) {
399 // Declaration without an explicit return type, such as `fun foo() = Bar`
400 // or val foo get() = Bar
401 // Replace the `=` with `: Modifier =`
402 report(
403 LintFix.create()
404 .replace()
405 .name("Add explicit Modifier return type")
406 .range(context.getLocation(this))
407 .pattern("[ \\t\\n]+=")
408 .with(": ${Names.Ui.Modifier.shortName} =")
409 .autoFix()
410 .build()
411 )
412 return
413 }
414 }
415
416 /**
417 * TODO: UMethod.returnTypeReference is not available in LINT_API_MIN, so instead use this with a
418 * [KtCallableDeclaration]. See
419 * [org.jetbrains.uast.kotlin.declarations.KotlinUMethod.returnTypeReference] on newer UAST
420 * versions.
421 */
422 private val KtCallableDeclaration.returnTypeString: String?
423 get() {
424 return typeReference?.text
425 }
426