1 /*
2  * Copyright (C) 2018 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 com.android.tools.metalava.cli.internal
18 
19 import com.android.SdkConstants
20 import com.android.tools.metalava.ANDROID_FLAGGED_API
21 import com.android.tools.metalava.ANDROID_NONNULL
22 import com.android.tools.metalava.ANDROID_NULLABLE
23 import com.android.tools.metalava.ANDROID_SDK_CONSTANT
24 import com.android.tools.metalava.RECENTLY_NONNULL
25 import com.android.tools.metalava.RECENTLY_NULLABLE
26 import com.android.tools.metalava.model.AnnotationRetention
27 import com.android.tools.metalava.model.Codebase
28 import java.io.File
29 import kotlin.text.Charsets.UTF_8
30 
31 /**
32  * Converts public stub annotation sources into package private annotation sources. This is needed
33  * for the stub sources, where we want to reference annotations that aren't public, but (a) they
34  * need to be public during compilation, and (b) they need to be package private when compiled and
35  * packaged on their own such that annotation processors can find them. See b/110532131 for details.
36  */
37 internal class RewriteAnnotations {
38     /** Modifies annotation source files such that they are package private */
modifyAnnotationSourcesnull39     fun modifyAnnotationSources(codebase: Codebase?, source: File, target: File, pkg: String = "") {
40         val fileName = source.name
41         if (fileName.endsWith(SdkConstants.DOT_JAVA)) {
42             // Only copy non-source retention annotation classes
43             val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
44             if (hasSourceRetention(source, codebase, qualifiedName)) {
45                 return
46             }
47 
48             // Copy and convert
49             target.parentFile.mkdirs()
50             target.writeText(source.readText(UTF_8).replace("\npublic @interface", "\n@interface"))
51         } else if (source.isDirectory) {
52             val newPackage = if (pkg.isEmpty()) fileName else "$pkg.$fileName"
53             source.listFiles()?.forEach {
54                 modifyAnnotationSources(codebase, it, File(target, it.name), newPackage)
55             }
56         }
57     }
58 
59     /**
60      * Returns true if the given annotation class name has source retention as far as the stub
61      * annotations are concerned.
62      */
hasSourceRetentionnull63     private fun hasSourceRetention(
64         source: File,
65         codebase: Codebase?,
66         qualifiedName: String
67     ): Boolean {
68         when {
69             qualifiedName == RECENTLY_NULLABLE ||
70                 qualifiedName == RECENTLY_NONNULL ||
71                 qualifiedName == ANDROID_NULLABLE ||
72                 qualifiedName == ANDROID_NONNULL ||
73                 qualifiedName == ANDROID_FLAGGED_API -> return false
74             qualifiedName == ANDROID_SDK_CONSTANT -> return true
75             qualifiedName.startsWith("androidx.annotation.") -> return true
76         }
77 
78         // See if the annotation is pointing to an annotation class that is part of the API; if not,
79         // skip it.
80         if (codebase != null) {
81             val cls = codebase.findClass(qualifiedName) ?: return true
82             return cls.isAnnotationType() && cls.getRetention() == AnnotationRetention.SOURCE
83         } else {
84             error("$source: Found annotation with unknown desired retention: $qualifiedName")
85         }
86     }
87 }
88