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