• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.stub
18 
19 import com.android.tools.metalava.model.CallableItem
20 import com.android.tools.metalava.model.ClassItem
21 import com.android.tools.metalava.model.ConstructorItem
22 import com.android.tools.metalava.model.DelegatedVisitor
23 import com.android.tools.metalava.model.FieldItem
24 import com.android.tools.metalava.model.Item
25 import com.android.tools.metalava.model.ItemVisitor
26 import com.android.tools.metalava.model.MethodItem
27 import com.android.tools.metalava.model.ModifierListWriter
28 import com.android.tools.metalava.model.PackageItem
29 import com.android.tools.metalava.model.item.ResourceFile
30 import com.android.tools.metalava.model.psi.trimDocIndent
31 import com.android.tools.metalava.model.visitors.ApiFilters
32 import com.android.tools.metalava.model.visitors.ApiPredicate
33 import com.android.tools.metalava.model.visitors.ApiVisitor
34 import com.android.tools.metalava.model.visitors.FilteringApiVisitor
35 import com.android.tools.metalava.model.visitors.MatchOverridingMethodPredicate
36 import com.android.tools.metalava.reporter.Issues
37 import com.android.tools.metalava.reporter.Reporter
38 import java.io.BufferedWriter
39 import java.io.File
40 import java.io.FileWriter
41 import java.io.IOException
42 import java.io.PrintWriter
43 import java.io.Writer
44 
45 internal class StubWriter(
46     private val stubsDir: File,
47     private val generateAnnotations: Boolean = false,
48     private val docStubs: Boolean,
49     private val reporter: Reporter,
50     private val config: StubWriterConfig,
51     private val stubConstructorManager: StubConstructorManager,
52 ) : DelegatedVisitor {
53 
54     /**
55      * Stubs need to preserve class nesting when visiting otherwise nested classes will not be
56      * nested inside their containing class properly.
57      */
58     override val requiresClassNesting: Boolean
59         get() = true
60 
visitPackagenull61     override fun visitPackage(pkg: PackageItem) {
62         getPackageDir(pkg, create = true)
63 
64         writePackageInfo(pkg)
65 
66         if (docStubs) {
67             pkg.overviewDocumentation?.let { writeDocOverview(pkg, it) }
68         }
69     }
70 
writeDocOverviewnull71     fun writeDocOverview(pkg: PackageItem, resourceFile: ResourceFile) {
72         val content = resourceFile.content
73         if (content.isBlank()) {
74             return
75         }
76 
77         val sourceFile = File(getPackageDir(pkg), "overview.html")
78         val overviewWriter =
79             try {
80                 PrintWriter(BufferedWriter(FileWriter(sourceFile)))
81             } catch (e: IOException) {
82                 reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
83                 return
84             }
85 
86         // Should we include this in our stub list?
87         //     startFile(sourceFile)
88 
89         overviewWriter.println(content)
90         overviewWriter.flush()
91         overviewWriter.close()
92     }
93 
writePackageInfonull94     private fun writePackageInfo(pkg: PackageItem) {
95         val annotations = pkg.modifiers.annotations()
96         val writeAnnotations = annotations.isNotEmpty() && generateAnnotations
97         val writeDocumentation =
98             config.includeDocumentationInStubs && pkg.documentation.isNotBlank()
99         if (writeAnnotations || writeDocumentation) {
100             val sourceFile = File(getPackageDir(pkg), "package-info.java")
101             val packageInfoWriter =
102                 try {
103                     PrintWriter(BufferedWriter(FileWriter(sourceFile)))
104                 } catch (e: IOException) {
105                     reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
106                     return
107                 }
108 
109             appendDocumentation(pkg, packageInfoWriter, config)
110 
111             if (annotations.isNotEmpty()) {
112                 // Write the modifier list even though the package info does not actually have
113                 // modifiers as that will write the annotations which it does have and ignore the
114                 // modifiers.
115                 ModifierListWriter.forStubs(
116                         writer = packageInfoWriter,
117                         docStubs = docStubs,
118                     )
119                     .write(pkg)
120             }
121             packageInfoWriter.println("package ${pkg.qualifiedName()};")
122 
123             packageInfoWriter.flush()
124             packageInfoWriter.close()
125         }
126     }
127 
getPackageDirnull128     private fun getPackageDir(packageItem: PackageItem, create: Boolean = true): File {
129         val relative = packageItem.qualifiedName().replace('.', File.separatorChar)
130         val dir = File(stubsDir, relative)
131         if (create && !dir.isDirectory) {
132             val ok = dir.mkdirs()
133             if (!ok) {
134                 throw IOException("Could not create $dir")
135             }
136         }
137 
138         return dir
139     }
140 
getClassFilenull141     private fun getClassFile(classItem: ClassItem): File {
142         assert(classItem.containingClass() == null) { "Should only be called on top level classes" }
143         val packageDir = getPackageDir(classItem.containingPackage())
144 
145         // Kotlin stub generation is not supported.
146         return File(packageDir, "${classItem.simpleName()}.java")
147     }
148 
149     /**
150      * Between top level class files the [textWriter] field doesn't point to a real file; it points
151      * to this writer, which redirects to the error output. Nothing should be written to the writer
152      * at that time.
153      */
154     private var errorTextWriter =
155         PrintWriter(
156             object : Writer() {
closenull157                 override fun close() {
158                     throw IllegalStateException(
159                         "Attempt to close 'textWriter' outside top level class"
160                     )
161                 }
162 
flushnull163                 override fun flush() {
164                     throw IllegalStateException(
165                         "Attempt to flush 'textWriter' outside top level class"
166                     )
167                 }
168 
writenull169                 override fun write(cbuf: CharArray, off: Int, len: Int) {
170                     throw IllegalStateException(
171                         "Attempt to write to 'textWriter' outside top level class\n'${String(cbuf, off, len)}'"
172                     )
173                 }
174             }
175         )
176 
177     /** The writer to write the stubs file to */
178     private var textWriter: PrintWriter = errorTextWriter
179 
180     private var stubWriter: DelegatedVisitor? = null
181 
visitClassnull182     override fun visitClass(cls: ClassItem) {
183         if (cls.isTopLevelClass()) {
184             val sourceFile = getClassFile(cls)
185             textWriter =
186                 try {
187                     PrintWriter(BufferedWriter(FileWriter(sourceFile)))
188                 } catch (e: IOException) {
189                     reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
190                     errorTextWriter
191                 }
192 
193             val modifierListWriter =
194                 ModifierListWriter.forStubs(
195                     writer = textWriter,
196                     docStubs = docStubs,
197                     runtimeAnnotationsOnly = !generateAnnotations,
198                 )
199 
200             stubWriter =
201                 JavaStubWriter(
202                     textWriter,
203                     modifierListWriter,
204                     config,
205                     stubConstructorManager,
206                 )
207 
208             // Copyright statements from the original file?
209             cls.sourceFile()?.getHeaderComments()?.let { textWriter.println(it) }
210         }
211         stubWriter?.visitClass(cls)
212 
213         dispatchStubsConstructorIfAvailable(cls)
214     }
215 
216     /**
217      * Stubs that have no accessible constructor may still need to generate one and that constructor
218      * is available from [StubConstructorManager.optionalSyntheticConstructor].
219      */
dispatchStubsConstructorIfAvailablenull220     private fun dispatchStubsConstructorIfAvailable(cls: ClassItem) {
221         // If a special constructor had to be synthesized for the class then it will not be in the
222         // ClassItem's list of constructors that would be visited automatically. So, this will visit
223         // it explicitly to make sure it appears in the stubs.
224         val syntheticConstructor = stubConstructorManager.optionalSyntheticConstructor(cls)
225         if (syntheticConstructor != null) {
226             visitConstructor(syntheticConstructor)
227         }
228     }
229 
afterVisitClassnull230     override fun afterVisitClass(cls: ClassItem) {
231         stubWriter?.afterVisitClass(cls)
232 
233         if (cls.isTopLevelClass()) {
234             textWriter.flush()
235             textWriter.close()
236             textWriter = errorTextWriter
237             stubWriter = null
238         }
239     }
240 
visitConstructornull241     override fun visitConstructor(constructor: ConstructorItem) {
242         stubWriter?.visitConstructor(constructor)
243     }
244 
visitMethodnull245     override fun visitMethod(method: MethodItem) {
246         stubWriter?.visitMethod(method)
247     }
248 
visitFieldnull249     override fun visitField(field: FieldItem) {
250         stubWriter?.visitField(field)
251     }
252 }
253 
254 /**
255  * Create an [ApiVisitor] that will filter the [Item] to which is applied according to the supplied
256  * parameters and in a manner appropriate for writing stubs, e.g. nesting classes. It will delegate
257  * any visitor calls that pass through its filter to this [StubWriter] instance.
258  */
createFilteringVisitorForStubsnull259 fun createFilteringVisitorForStubs(
260     delegate: DelegatedVisitor,
261     docStubs: Boolean,
262     preFiltered: Boolean,
263     apiPredicateConfig: ApiPredicate.Config,
264     ignoreEmit: Boolean = false,
265 ): ItemVisitor {
266     val filterReference =
267         ApiPredicate(
268             includeDocOnly = docStubs,
269             config = apiPredicateConfig.copy(ignoreShown = true),
270         )
271     val filterEmit = MatchOverridingMethodPredicate(filterReference)
272     val apiFilters =
273         ApiFilters(
274             emit = filterEmit,
275             reference = filterReference,
276         )
277     return FilteringApiVisitor(
278         delegate = delegate,
279         inlineInheritedFields = true,
280         // Sort methods in stubs based on their signature. The order of methods in stubs is
281         // irrelevant, e.g. it does not affect compilation or document generation. However, having a
282         // consistent order will prevent churn in the generated stubs caused by changes to Metalava
283         // itself or changes to the order of methods in the sources.
284         callableComparator = CallableItem.comparator,
285         apiFilters = apiFilters,
286         preFiltered = preFiltered,
287         ignoreEmit = ignoreEmit,
288     )
289 }
290 
appendDocumentationnull291 internal fun appendDocumentation(item: Item, writer: PrintWriter, config: StubWriterConfig) {
292     if (config.includeDocumentationInStubs) {
293         val documentation = item.documentation
294         val text = documentation.fullyQualifiedDocumentation()
295         if (text.isNotBlank()) {
296             val trimmed = trimDocIndent(text)
297             writer.println(trimmed)
298             writer.println()
299         }
300     }
301 }
302