1 /*
2  * Copyright 2022 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.build.lint
18 
19 import com.android.tools.lint.client.api.UElementHandler
20 import com.android.tools.lint.detector.api.Category
21 import com.android.tools.lint.detector.api.Detector
22 import com.android.tools.lint.detector.api.Implementation
23 import com.android.tools.lint.detector.api.Incident
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.LintFix
27 import com.android.tools.lint.detector.api.Scope
28 import com.android.tools.lint.detector.api.Severity
29 import com.intellij.psi.PsiElement
30 import org.jetbrains.kotlin.asJava.toLightSetter
31 import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
32 import org.jetbrains.kotlin.lexer.KtTokens
33 import org.jetbrains.kotlin.psi.KtAnnotated
34 import org.jetbrains.kotlin.psi.KtAnnotationEntry
35 import org.jetbrains.kotlin.psi.KtClass
36 import org.jetbrains.kotlin.psi.KtClassBody
37 import org.jetbrains.kotlin.psi.KtConstructor
38 import org.jetbrains.kotlin.psi.KtFile
39 import org.jetbrains.kotlin.psi.KtModifierListOwner
40 import org.jetbrains.kotlin.psi.KtParameter
41 import org.jetbrains.kotlin.psi.KtProperty
42 import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
43 import org.jetbrains.kotlin.psi.psiUtil.isPrivate
44 import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter
45 import org.jetbrains.uast.UAnnotation
46 import org.jetbrains.uast.UElement
47 import org.jetbrains.uast.UParameter
48 import org.jetbrains.uast.toUElement
49 
50 class ExperimentalPropertyAnnotationDetector : Detector(), Detector.UastScanner {
51 
getApplicableUastTypesnull52     override fun getApplicableUastTypes(): List<Class<out UElement>> =
53         listOf(UAnnotation::class.java, UParameter::class.java)
54 
55     override fun createUastHandler(context: JavaContext): UElementHandler =
56         object : UElementHandler() {
57             /**
58              * Work around for b/406850340: annotations with the `property` use site target on
59              * parameters aren't visited directly with [visitAnnotation]. This finds those
60              * annotations and calls [visitAnnotation] on them.
61              */
62             override fun visitParameter(node: UParameter) {
63                 val ktParameter = node.sourcePsi as? KtParameter ?: return
64                 val propertyAnnotations =
65                     ktParameter.annotationEntries
66                         .filter {
67                             it.useSiteTarget?.getAnnotationUseSiteTarget() ==
68                                 AnnotationUseSiteTarget.PROPERTY
69                         }
70                         .map { it.toUElement() }
71                         .filterIsInstance<UAnnotation>()
72                 for (propertyAnnotation in propertyAnnotations) {
73                     visitAnnotation(propertyAnnotation)
74                 }
75             }
76 
77             override fun visitAnnotation(node: UAnnotation) {
78                 val neededTargets =
79                     mutableSetOf(
80                         AnnotationUseSiteTarget.PROPERTY,
81                         AnnotationUseSiteTarget.PROPERTY_GETTER,
82                         AnnotationUseSiteTarget.PROPERTY_SETTER
83                     )
84 
85                 // If this annotation is not annotated with an experimental annotation, return
86                 val resolved = node.resolve()
87                 if (
88                     BanInappropriateExperimentalUsage.APPLICABLE_ANNOTATIONS.all {
89                         context.evaluator.getAnnotation(resolved, it) == null
90                     }
91                 ) {
92                     return
93                 }
94 
95                 val type = node.qualifiedName ?: return
96                 val source = node.sourcePsi as? KtAnnotationEntry ?: return
97 
98                 // Check that the annotation is applied to a property. Properties can also be
99                 // defined as constructor parameters.
100                 val parent = source.parent?.parent
101                 when (parent) {
102                     // Check if this check shouldn't apply to the property/parameter.
103                     is KtProperty -> if (!appliesToProperty(parent)) return
104                     is KtParameter -> if (!appliesToParameter(parent)) return
105                     else -> return
106                 }
107                 val propertyParent = parent.parent
108 
109                 // Don't apply the lint to private properties
110                 // parent is either a KtProperty or KtParameter, both are KtModifierListOwner
111                 if ((parent as KtModifierListOwner).isPrivate()) return
112 
113                 // Don't apply the lint to properties in private classes
114                 if (propertyParent.getParentOfType<KtClass>(true)?.isPrivate() == true) return
115 
116                 // Annotation on setter is only needed for mutable property with non-private setter
117                 // Getter annotation is needed because the getter can't be private if the property
118                 // isn't
119                 if (
120                     !((parent as? KtProperty)?.hasVisibleSetter()
121                         ?: (parent as KtParameter).hasSetter())
122                 ) {
123                     neededTargets.remove(AnnotationUseSiteTarget.PROPERTY_SETTER)
124                 }
125 
126                 // Find all usages of this annotation on the property
127                 // parent is either a KtProperty or KtParameter, both are KtAnnotated
128                 val existingTargets =
129                     (parent as KtAnnotated)
130                         .annotationEntries
131                         .filter { type.endsWith(it.shortName?.identifier ?: "") }
132                         .map { it.useSiteTarget?.getAnnotationUseSiteTarget() }
133 
134                 val existingTargetSet =
135                     existingTargets
136                         // A null target means the default, which is the property target
137                         // Note this is true for parameters because experimental annotations don't
138                         // apply to params.
139                         .map { it ?: AnnotationUseSiteTarget.PROPERTY }
140                         .toSet()
141                 val missingTargets = neededTargets - existingTargetSet
142 
143                 if (missingTargets.isEmpty()) return
144 
145                 // If not all annotations are present but more than one is, only report the error on
146                 // the first annotation to prevent duplicate errors
147                 val target = source.useSiteTarget?.getAnnotationUseSiteTarget()
148                 if (existingTargets.size > 1 && existingTargets.indexOf(target) != 0) return
149 
150                 val fix = createFix(type, parent, missingTargets)
151                 val message =
152                     "This property does not have all required annotations to correctly mark" +
153                         " it as experimental."
154                 val location = context.getLocation(node)
155                 val incident = Incident(ISSUE, node, location, message, fix)
156                 context.report(incident)
157             }
158 
159             /**
160              * Whether the lint check should apply to [property]. The check only applies to top
161              * level or class properties, and does not apply to const, @JvmField, or delegated
162              * properties.
163              */
164             fun appliesToProperty(property: KtProperty): Boolean {
165                 // Only applies to properties defined at the top level or in classes
166                 val propertyParent = property.parent
167                 if ((propertyParent !is KtClassBody && propertyParent !is KtFile)) return false
168 
169                 // Don't apply lint to const properties, because they are static fields in java
170                 if (property.modifierList?.node?.findChildByType(KtTokens.CONST_KEYWORD) != null)
171                     return false
172                 // Don't apply lint to @JvmField properties, because they are fields in java
173                 if (property.annotationEntries.any { it.shortName.toString() == "JvmField" })
174                     return false
175 
176                 // Don't apply lint to delegated properties
177                 if (property.delegate != null) return false
178 
179                 return true
180             }
181 
182             /**
183              * Whether the lint check should apply to [parameter]. The check only applies to
184              * constructor property parameters, and should not apply if the constructor is private.
185              */
186             fun appliesToParameter(parameter: KtParameter): Boolean {
187                 if (!parameter.isPropertyParameter()) return false
188 
189                 // Don't apply to parameters of private constructors
190                 if (parameter.getParentOfType<KtConstructor<*>>(true)?.isPrivate() == true)
191                     return false
192 
193                 return true
194             }
195 
196             fun KtProperty.hasVisibleSetter() = isVar && setter?.isPrivate() != true
197 
198             fun KtParameter.hasSetter() = toLightSetter() != null
199 
200             private fun createFix(
201                 annotation: String,
202                 annotated: PsiElement,
203                 missingTargets: Set<AnnotationUseSiteTarget>
204             ): LintFix {
205                 val fix = fix().name("Add missing annotations").composite()
206 
207                 for (target in missingTargets) {
208                     // There's a compilation error when an experimental annotation is applied to a
209                     // getter:
210                     // https://kotlinlang.org/docs/opt-in-requirements.html#mark-api-elements
211                     // Add it anyway because metalava needs it and suppress the error
212                     if (target == AnnotationUseSiteTarget.PROPERTY_GETTER) {
213                         val addSuppression =
214                             fix()
215                                 .annotate(
216                                     "kotlin.Suppress(\"OPT_IN_MARKER_ON_WRONG_TARGET\")",
217                                     context,
218                                     annotated
219                                 )
220                                 .build()
221                         fix.add(addSuppression)
222                     }
223 
224                     val addAnnotation =
225                         fix()
226                             // With replace = true, the existing annotation with a different target
227                             // would
228                             // be replaced. There shouldn't be an existing annotation with this
229                             // target.
230                             .annotate(
231                                 target.renderName + ":" + annotation,
232                                 context,
233                                 annotated,
234                                 replace = false
235                             )
236                             .build()
237                     fix.add(addAnnotation)
238                 }
239 
240                 return fix.build().autoFix()
241             }
242         }
243 
244     companion object {
245         val ISSUE =
246             Issue.create(
247                 "ExperimentalPropertyAnnotation",
248                 "Experimental properties need to have annotations targeting the" +
249                     " property, getter, and (if applicable) setter.",
250                 "Annotations on Kotlin properties which don't specify a use-site will " +
251                     "only apply to the private backing field itself, and not to the getter or setter " +
252                     "(see https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets). " +
253                     "Annotating the property use-site is required by the Kotlin compiler, the get " +
254                     "use-site is required by Metalava, and the set use-site is required by Java " +
255                     "clients, so all use-sites must be annotated.",
256                 Category.CORRECTNESS,
257                 5,
258                 Severity.ERROR,
259                 Implementation(
260                     ExperimentalPropertyAnnotationDetector::class.java,
261                     Scope.JAVA_FILE_SCOPE
262                 )
263             )
264     }
265 }
266