1 /*
2 * Copyright 2023 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.SdkConstants.ATTR_VALUE
20 import com.android.tools.lint.detector.api.AnnotationInfo
21 import com.android.tools.lint.detector.api.AnnotationUsageInfo
22 import com.android.tools.lint.detector.api.AnnotationUsageType
23 import com.android.tools.lint.detector.api.AnnotationUsageType.ASSIGNMENT_LHS
24 import com.android.tools.lint.detector.api.AnnotationUsageType.ASSIGNMENT_RHS
25 import com.android.tools.lint.detector.api.Category
26 import com.android.tools.lint.detector.api.Implementation
27 import com.android.tools.lint.detector.api.Issue
28 import com.android.tools.lint.detector.api.JavaContext
29 import com.android.tools.lint.detector.api.Scope
30 import com.android.tools.lint.detector.api.Severity
31 import com.android.tools.lint.detector.api.SourceCodeScanner
32 import com.android.tools.lint.detector.api.isKotlin
33 import com.android.tools.lint.model.DefaultLintModelMavenName
34 import com.android.tools.lint.model.LintModelLibrary
35 import com.android.tools.lint.model.LintModelMavenName
36 import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
37 import com.intellij.psi.PsiClass
38 import com.intellij.psi.PsiCompiledElement
39 import com.intellij.psi.PsiField
40 import com.intellij.psi.PsiMember
41 import com.intellij.psi.PsiMethod
42 import com.intellij.psi.impl.compiled.ClsAnnotationImpl
43 import com.intellij.psi.util.PsiTypesUtil
44 import org.jetbrains.uast.UAnnotated
45 import org.jetbrains.uast.UAnnotation
46 import org.jetbrains.uast.UCallExpression
47 import org.jetbrains.uast.UClass
48 import org.jetbrains.uast.UElement
49 import org.jetbrains.uast.UExpression
50 import org.jetbrains.uast.UReferenceExpression
51 import org.jetbrains.uast.UTypeReferenceExpression
52 import org.jetbrains.uast.UastEmptyExpression
53 import org.jetbrains.uast.getParentOfType
54 import org.jetbrains.uast.util.isArrayInitializer
55
56 /** Adapted from com/android/tools/lint/checks/RestrictToDetector.kt in Android Studio repo. */
57 class RestrictToDetector : AbstractAnnotationDetector(), SourceCodeScanner {
applicableAnnotationsnull58 override fun applicableAnnotations(): List<String> = listOf(RESTRICT_TO_ANNOTATION)
59
60 override fun inheritAnnotation(annotation: String): Boolean {
61 // Require restriction annotations to be annotated everywhere
62 return false
63 }
64
isApplicableAnnotationUsagenull65 override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean {
66 return type != ASSIGNMENT_LHS &&
67 type != ASSIGNMENT_RHS &&
68 super.isApplicableAnnotationUsage(type)
69 }
70
visitAnnotationUsagenull71 override fun visitAnnotationUsage(
72 context: JavaContext,
73 element: UElement,
74 annotationInfo: AnnotationInfo,
75 usageInfo: AnnotationUsageInfo
76 ) {
77 val type = usageInfo.type
78 if (type == AnnotationUsageType.EXTENDS && element is UTypeReferenceExpression) {
79 // If it's a constructor reference we don't need to also check the type
80 // reference. Ideally we'd do a "parent is KtConstructorCalleeExpression"
81 // here, but that points to impl classes in its hierarchy which leads to
82 // class loading trouble.
83 val sourcePsi = element.sourcePsi
84 if (
85 sourcePsi != null &&
86 isKotlin(sourcePsi.language) &&
87 sourcePsi.parent?.toString() == "CONSTRUCTOR_CALLEE"
88 ) {
89 return
90 }
91 }
92
93 val member = usageInfo.referenced as? PsiMember
94 val annotation = annotationInfo.annotation
95 val qualifiedName = annotationInfo.qualifiedName
96 if (RESTRICT_TO_ANNOTATION == qualifiedName) {
97 checkRestrictTo(context, element, member, annotation, usageInfo)
98 }
99 }
100
isTestContextnull101 private fun isTestContext(context: JavaContext, element: UElement): Boolean {
102 var current = element
103 // (1) Is this compilation unit in a test source path?
104 if (context.isTestSource) {
105 return true
106 }
107
108 // (2) Is this AST node surrounded by a test-only annotation?
109 while (true) {
110 val owner = current.getParentOfType<UAnnotated>(true) ?: break
111
112 //noinspection AndroidLintExternalAnnotations
113 for (annotation in owner.uAnnotations) {
114 val name = annotation.qualifiedName ?: continue
115 when {
116 RESTRICT_TO_ANNOTATION == name -> {
117 val restrictionScope = getRestrictionScope(annotation)
118 if (restrictionScope and RESTRICT_TO_TESTS != 0) {
119 return true
120 }
121 }
122 name.endsWith(VISIBLE_FOR_TESTING_SUFFIX) -> return true
123 name.endsWith(".TestOnly") -> return true
124 }
125 }
126
127 current = owner
128 }
129
130 return false
131 }
132
133 // TODO: Test XML access of restricted classes
checkRestrictTonull134 private fun checkRestrictTo(
135 context: JavaContext,
136 node: UElement,
137 method: PsiMember?,
138 annotation: UAnnotation,
139 usageInfo: AnnotationUsageInfo
140 ) {
141 val scope = getRestrictionScope(annotation)
142 if (scope != 0) {
143 checkRestrictTo(context, node, method, usageInfo, scope)
144 }
145 }
146
checkRestrictTonull147 private fun checkRestrictTo(
148 context: JavaContext,
149 node: UElement,
150 member: PsiMember?,
151 usageInfo: AnnotationUsageInfo,
152 scope: Int
153 ) {
154
155 val containingClass =
156 when {
157 node is UTypeReferenceExpression -> PsiTypesUtil.getPsiClass(node.type)
158 member != null -> member.containingClass
159 node is UCallExpression -> node.classReference?.resolve() as? PsiClass?
160 node is PsiClass -> node
161 else -> null
162 }
163
164 containingClass ?: return
165
166 if (usageInfo.anyCloser { it.qualifiedName == RESTRICT_TO_ANNOTATION }) {
167 return
168 }
169
170 if (scope and RESTRICT_TO_LIBRARY_GROUP != 0 && member != null) {
171 val evaluator = context.evaluator
172 val thisCoordinates = evaluator.getLibrary(node) ?: context.project.mavenCoordinate
173 val methodCoordinates = context.findMavenCoordinate(member)
174 val thisGroup = thisCoordinates?.groupId
175 val methodGroup = methodCoordinates?.groupId
176 if (thisGroup != methodGroup && methodGroup != null) {
177 val thisGroupDisplayText = thisGroup ?: "<unknown>"
178 val where =
179 "from within the same library group (referenced groupId=`$methodGroup` from " +
180 "groupId=`$thisGroupDisplayText`)"
181 reportRestriction(where, containingClass, member, context, node, usageInfo)
182 }
183 } else if (scope and RESTRICT_TO_LIBRARY_GROUP_PREFIX != 0 && member != null) {
184 val evaluator = context.evaluator
185 val thisCoordinates = evaluator.getLibrary(node) ?: context.project.mavenCoordinate
186 val methodCoordinates =
187 evaluator.getLibrary(member)
188 ?: run {
189 if (thisCoordinates != null && member !is PsiCompiledElement) {
190 // Local source?
191 context.evaluator.getProject(member)?.mavenCoordinate
192 } else {
193 null
194 }
195 }
196 val thisGroup = thisCoordinates?.groupId
197 val methodGroup = methodCoordinates?.groupId
198 if (
199 methodGroup != null &&
200 (thisGroup == null || !sameLibraryGroupPrefix(thisGroup, methodGroup))
201 ) {
202 val expectedPrefix =
203 methodGroup.lastIndexOf('.').let {
204 if (it < 0) {
205 "\"\""
206 } else {
207 methodGroup.substring(0, it)
208 }
209 }
210 val where =
211 "from within the same library group prefix (referenced " +
212 "groupId=`$methodGroup` with prefix $expectedPrefix" +
213 "${if (thisGroup != null) " from groupId=`$thisGroup`" else ""})"
214 reportRestriction(where, containingClass, member, context, node, usageInfo)
215 }
216 } else if (scope and RESTRICT_TO_LIBRARY != 0 && member != null) {
217 val evaluator = context.evaluator
218 val thisCoordinates = evaluator.getLibrary(node) ?: context.project.mavenCoordinate
219 val methodCoordinates = context.findMavenCoordinate(member)
220 val thisGroup = thisCoordinates?.groupId
221 val methodGroup = methodCoordinates?.groupId
222 if (thisGroup != methodGroup && methodGroup != null) {
223 val thisArtifact = thisCoordinates?.artifactId
224 val methodArtifact = methodCoordinates.artifactId
225 if (thisArtifact != methodArtifact) {
226 val name =
227 if (methodGroup == "__local_aars__") {
228 "missing Maven coordinate due to repackaging"
229 } else {
230 "$methodGroup:$methodArtifact"
231 }
232 val where = "from within the same library ($name)"
233 reportRestriction(where, containingClass, member, context, node, usageInfo)
234 }
235 } else if (member !is PsiCompiledElement) {
236 // If the resolved method is source, make sure they're part
237 // of the same Gradle project
238 val project = context.evaluator.getProject(member)
239 if (project != null && project != context.project) {
240 val coordinates = project.mavenCoordinate
241 val name =
242 if (coordinates != null) {
243 "${coordinates.groupId}:${coordinates.artifactId}"
244 } else {
245 project.name
246 }
247 val where = "from within the same library ($name)"
248 reportRestriction(where, containingClass, member, context, node, usageInfo)
249 }
250 }
251 }
252
253 if (scope and RESTRICT_TO_TESTS != 0) {
254 if (!isTestContext(context, node)) {
255 reportRestriction("from tests", containingClass, member, context, node, usageInfo)
256 }
257 }
258
259 if (scope and RESTRICT_TO_SUBCLASSES != 0) {
260 val qualifiedName = containingClass.qualifiedName
261 if (qualifiedName != null) {
262 val evaluator = context.evaluator
263
264 var outer: UClass?
265 var isSubClass = false
266 var prev = node
267
268 while (true) {
269 outer = prev.getParentOfType(UClass::class.java, true)
270 if (outer == null) {
271 break
272 }
273 if (evaluator.inheritsFrom(outer, qualifiedName, false)) {
274 isSubClass = true
275 break
276 }
277
278 if (evaluator.isStatic(outer)) {
279 break
280 }
281 prev = outer
282 }
283
284 if (!isSubClass) {
285 reportRestriction(
286 "from subclasses",
287 containingClass,
288 member,
289 context,
290 node,
291 usageInfo
292 )
293 }
294 }
295 }
296 }
297
reportRestrictionnull298 private fun reportRestriction(
299 where: String?,
300 containingClass: PsiClass,
301 member: PsiMember?,
302 context: JavaContext,
303 node: UElement,
304 usageInfo: AnnotationUsageInfo
305 ) {
306 var api: String
307 api =
308 if (member == null || member is PsiMethod && member.isConstructor) {
309 member?.name ?: (containingClass.name + " constructor")
310 } else
311 //noinspection LintImplPsiEquals
312 if (containingClass == member) {
313 member.name ?: "class"
314 } else {
315 containingClass.name + "." + member.name
316 }
317
318 var locationNode = node
319 if (node is UCallExpression) {
320 val nameElement = node.methodIdentifier
321 if (nameElement != null) {
322 locationNode = nameElement
323 }
324
325 // If the annotation was reported on the class, and the left hand side
326 // expression is that class, use it as the name node?
327 val annotation = usageInfo.annotations[usageInfo.index]
328 val annotated = annotation.annotated
329 //noinspection LintImplPsiEquals
330 if (where == null && annotated is PsiClass && annotated != usageInfo.referenced) {
331 val qualifier = node.receiver
332 val className = annotated.name
333 if (
334 qualifier != null &&
335 className != null &&
336 qualifier.asSourceString() == className
337 ) {
338 locationNode = qualifier
339 api = className
340 }
341 }
342 }
343
344 // If this error message changes, you need to also update
345 // ResourceTypeInspection#guessLintIssue
346 var message: String
347 if (where == null) {
348 message = "$api is marked as internal and should not be accessed from apps"
349 } else {
350 val refType = if (member is PsiMethod) "called" else "accessed"
351 message = "$api can only be $refType $where"
352
353 // Most users will encounter this for the support library; let's have a clearer error
354 // message
355 // for that specific scenario
356 if (where == "from within the same library (groupId=com.android.support)") {
357 // If this error message changes, you need to also update
358 // ResourceTypeInspection#guessLintIssue
359 message =
360 "This API is marked as internal to the support library and should not be " +
361 "accessed from apps"
362 }
363 }
364
365 val location =
366 if (locationNode is UCallExpression) {
367 context.getCallLocation(
368 locationNode,
369 includeReceiver = false,
370 includeArguments = false
371 )
372 } else {
373 context.getLocation(locationNode)
374 }
375 report(context, RESTRICTED, node, location, message, null)
376 }
377
378 companion object {
379 private val IMPLEMENTATION =
380 Implementation(RestrictToDetector::class.java, Scope.JAVA_FILE_SCOPE)
381
382 private const val RESTRICT_TO_ANNOTATION = "androidx.annotation.RestrictTo"
383 private const val VISIBLE_FOR_TESTING_SUFFIX = ".VisibleForTesting"
384
385 /** `RestrictTo(RestrictTo.Scope.GROUP_ID` */
386 private const val RESTRICT_TO_LIBRARY_GROUP = 1 shl 0
387
388 /** `RestrictTo(RestrictTo.Scope.GROUP_ID` */
389 private const val RESTRICT_TO_LIBRARY = 1 shl 1
390
391 /** `RestrictTo(RestrictTo.Scope.GROUP_ID` */
392 private const val RESTRICT_TO_LIBRARY_GROUP_PREFIX = 1 shl 2
393
394 /** `RestrictTo(RestrictTo.Scope.TESTS` */
395 private const val RESTRICT_TO_TESTS = 1 shl 3
396
397 /** `RestrictTo(RestrictTo.Scope.SUBCLASSES` */
398 private const val RESTRICT_TO_SUBCLASSES = 1 shl 4
399
getRestrictionScopenull400 private fun getRestrictionScope(annotation: UAnnotation): Int {
401 val value = annotation.findDeclaredAttributeValue(ATTR_VALUE)
402 if (value != null) {
403 return getRestrictionScope(value, annotation)
404 }
405 return 0
406 }
407
408 @Suppress("UnstableApiUsage") // JvmAnnotation.findAttribute()
getRestrictionScopenull409 private fun getRestrictionScope(expression: UExpression?, annotation: UAnnotation): Int {
410 var scope = 0
411 if (expression != null) {
412 if (expression.isArrayInitializer()) {
413 val initializerExpression = expression as UCallExpression?
414 val initializers = initializerExpression!!.valueArguments
415 for (initializer in initializers) {
416 scope = scope or getRestrictionScope(initializer, annotation)
417 }
418 } else if (expression is UReferenceExpression) {
419 val resolved = expression.resolve()
420 if (resolved is PsiField) {
421 val name = resolved.name
422 scope =
423 when (name) {
424 "GROUP_ID",
425 "LIBRARY_GROUP" -> RESTRICT_TO_LIBRARY_GROUP
426 "SUBCLASSES" -> RESTRICT_TO_SUBCLASSES
427 "TESTS" -> RESTRICT_TO_TESTS
428 "LIBRARY" -> RESTRICT_TO_LIBRARY
429 "LIBRARY_GROUP_PREFIX" -> RESTRICT_TO_LIBRARY_GROUP_PREFIX
430 else -> 0
431 }
432 }
433 } else if (expression is UastEmptyExpression) {
434 // See JavaUAnnotation.findDeclaredAttributeValue
435 val psi = annotation.sourcePsi
436 if (psi is ClsAnnotationImpl) {
437 val otherwise = psi.findAttribute(ATTR_VALUE)
438 val v = otherwise?.attributeValue
439 if (v is JvmAnnotationConstantValue) {
440 val constant = v.constantValue
441 if (constant is Number) {
442 scope = constant.toInt()
443 }
444 }
445 }
446 }
447 }
448
449 return scope
450 }
451
452 /**
453 * Implements the group prefix equality that is described in the documentation for the
454 * RestrictTo.Scope.LIBRARY_GROUP_PREFIX enum constant.
455 */
sameLibraryGroupPrefixnull456 fun sameLibraryGroupPrefix(group1: String, group2: String): Boolean {
457 // TODO: Allow group1.startsWith(group2) || group2.startsWith(group1) ?
458
459 if (group1 == group2) {
460 return true
461 }
462
463 // Implementation for AndroidX differs from the standard RestrictToDetector, since we
464 // treat LIBRARY_GROUP_PREFIX as anything in the androidx.* package. See b/297047524.
465 if (group1.startsWith(ANDROIDX_PREFIX) && group2.startsWith(ANDROIDX_PREFIX)) {
466 return true
467 }
468
469 val i1 = group1.lastIndexOf('.')
470 val i2 = group2.lastIndexOf('.')
471 if (i2 != i1 || i1 == -1) {
472 return false
473 }
474
475 return group1.regionMatches(0, group2, 0, i1)
476 }
477
478 private const val ANDROIDX_PREFIX = "androidx."
479
480 /** Using a restricted API. */
481 @JvmField
482 val RESTRICTED =
483 Issue.create(
484 id = "RestrictedApiAndroidX",
485 briefDescription = "Restricted API",
486 explanation =
487 """
488 This API has been flagged with a restriction that has not been met.
489
490 Examples of API restrictions:
491 * Method can only be invoked by a subclass
492 * Method can only be accessed from within the same library (defined by the Gradle library group id)
493 * Method can only be accessed from tests.
494
495 You can add your own API restrictions with the `@RestrictTo` annotation.""",
496 category = Category.CORRECTNESS,
497 priority = 4,
498 severity = Severity.ERROR,
499 implementation = IMPLEMENTATION
500 )
501 }
502 }
503
504 /** Attempts to parse an unversioned Maven name from the library identifier. */
getMavenNameFromIdentifiernull505 internal fun LintModelLibrary.getMavenNameFromIdentifier(): LintModelMavenName? {
506 val indexOfSentinel = identifier.indexOf(":@@:")
507 if (indexOfSentinel < 0) return null
508
509 // May be suffixed with something like ::debug.
510 val project = identifier.substring(indexOfSentinel + 4).substringBefore("::")
511 val indexOfLastSeparator = project.lastIndexOf(':')
512 if (indexOfLastSeparator < 0) return null
513
514 val groupId = project.substring(0, indexOfLastSeparator).replace(':', '.')
515 val artifactId = project.substring(indexOfLastSeparator + 1)
516 return DefaultLintModelMavenName("androidx.$groupId", artifactId)
517 }
518