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