1 /*
<lambda>null2 * 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.ApiPredicate
20 import com.android.tools.metalava.FilterPredicate
21 import com.android.tools.metalava.Issues
22 import com.android.tools.metalava.model.AnnotationTarget
23 import com.android.tools.metalava.model.ClassItem
24 import com.android.tools.metalava.model.Codebase
25 import com.android.tools.metalava.model.ConstructorItem
26 import com.android.tools.metalava.model.FieldItem
27 import com.android.tools.metalava.model.Item
28 import com.android.tools.metalava.model.MethodItem
29 import com.android.tools.metalava.model.ModifierList
30 import com.android.tools.metalava.model.PackageItem
31 import com.android.tools.metalava.model.psi.trimDocIndent
32 import com.android.tools.metalava.model.visitors.ApiVisitor
33 import com.android.tools.metalava.model.visitors.ItemVisitor
34 import com.android.tools.metalava.options
35 import com.android.tools.metalava.reporter
36 import java.io.BufferedWriter
37 import java.io.File
38 import java.io.FileWriter
39 import java.io.IOException
40 import java.io.PrintWriter
41
42 class StubWriter(
43 private val codebase: Codebase,
44 private val stubsDir: File,
45 private val generateAnnotations: Boolean = false,
46 private val preFiltered: Boolean = true,
47 private val docStubs: Boolean
48 ) : ApiVisitor(
49 visitConstructorsAsMethods = false,
50 nestInnerClasses = true,
51 inlineInheritedFields = true,
52 fieldComparator = FieldItem.comparator,
53 // Methods are by default sorted in source order in stubs, to encourage methods
54 // that are near each other in the source to show up near each other in the documentation
55 methodComparator = MethodItem.sourceOrderComparator,
56 filterEmit = FilterPredicate(ApiPredicate(ignoreShown = true, includeDocOnly = docStubs)),
57 filterReference = ApiPredicate(ignoreShown = true, includeDocOnly = docStubs),
58 includeEmptyOuterClasses = true
59 ) {
60 private val annotationTarget =
61 if (docStubs) AnnotationTarget.DOC_STUBS_FILE else AnnotationTarget.SDK_STUBS_FILE
62
63 private val sourceList = StringBuilder(20000)
64
65 /** Writes a source file list of the generated stubs */
66 fun writeSourceList(target: File, root: File?) {
67 target.parentFile?.mkdirs()
68 val contents = if (root != null) {
69 val path = root.path.replace('\\', '/') + "/"
70 sourceList.toString().replace(path, "")
71 } else {
72 sourceList.toString()
73 }
74 target.writeText(contents)
75 }
76
77 private fun startFile(sourceFile: File) {
78 if (sourceList.isNotEmpty()) {
79 sourceList.append(' ')
80 }
81 sourceList.append(sourceFile.path.replace('\\', '/'))
82 }
83
84 override fun visitPackage(pkg: PackageItem) {
85 getPackageDir(pkg, create = true)
86
87 writePackageInfo(pkg)
88
89 if (docStubs) {
90 codebase.getPackageDocs()?.let { packageDocs ->
91 packageDocs.getOverviewDocumentation(pkg)?.let { writeDocOverview(pkg, it) }
92 }
93 }
94 }
95
96 fun writeDocOverview(pkg: PackageItem, content: String) {
97 if (content.isBlank()) {
98 return
99 }
100
101 val sourceFile = File(getPackageDir(pkg), "overview.html")
102 val overviewWriter = 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 // Should we include this in our stub list?
110 // startFile(sourceFile)
111
112 overviewWriter.println(content)
113 overviewWriter.flush()
114 overviewWriter.close()
115 }
116
117 private fun writePackageInfo(pkg: PackageItem) {
118 val annotations = pkg.modifiers.annotations()
119 val writeAnnotations = annotations.isNotEmpty() && generateAnnotations
120 val writeDocumentation = docStubs && pkg.documentation.isNotBlank()
121 if (writeAnnotations || writeDocumentation) {
122 val sourceFile = File(getPackageDir(pkg), "package-info.java")
123 val packageInfoWriter = try {
124 PrintWriter(BufferedWriter(FileWriter(sourceFile)))
125 } catch (e: IOException) {
126 reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
127 return
128 }
129 startFile(sourceFile)
130
131 appendDocumentation(pkg, packageInfoWriter, docStubs)
132
133 if (annotations.isNotEmpty()) {
134 ModifierList.writeAnnotations(
135 list = pkg.modifiers,
136 separateLines = true,
137 // Some bug in UAST triggers duplicate nullability annotations
138 // here; make sure the are filtered out
139 filterDuplicates = true,
140 target = annotationTarget,
141 writer = packageInfoWriter
142 )
143 }
144 packageInfoWriter.println("package ${pkg.qualifiedName()};")
145
146 packageInfoWriter.flush()
147 packageInfoWriter.close()
148 }
149 }
150
151 private fun getPackageDir(packageItem: PackageItem, create: Boolean = true): File {
152 val relative = packageItem.qualifiedName().replace('.', File.separatorChar)
153 val dir = File(stubsDir, relative)
154 if (create && !dir.isDirectory) {
155 val ok = dir.mkdirs()
156 if (!ok) {
157 throw IOException("Could not create $dir")
158 }
159 }
160
161 return dir
162 }
163
164 private fun getClassFile(classItem: ClassItem): File {
165 assert(classItem.containingClass() == null) { "Should only be called on top level classes" }
166 val packageDir = getPackageDir(classItem.containingPackage())
167
168 // Kotlin From-text stub generation is not supported.
169 // This method will raise an error if
170 // options.kotlinStubs == true and classItem is TextClassItem.
171 return if (options.kotlinStubs && classItem.isKotlin()) {
172 File(packageDir, "${classItem.simpleName()}.kt")
173 } else {
174 File(packageDir, "${classItem.simpleName()}.java")
175 }
176 }
177
178 /**
179 * Between top level class files the [textWriter] field doesn't point to a real file; it
180 * points to this writer, which redirects to the error output. Nothing should be written
181 * to the writer at that time.
182 */
183 private var errorTextWriter = PrintWriter(options.stderr)
184
185 /** The writer to write the stubs file to */
186 private var textWriter: PrintWriter = errorTextWriter
187
188 private var stubWriter: ItemVisitor? = null
189
190 override fun visitClass(cls: ClassItem) {
191 if (cls.isTopLevelClass()) {
192 val sourceFile = getClassFile(cls)
193 textWriter = try {
194 PrintWriter(BufferedWriter(FileWriter(sourceFile)))
195 } catch (e: IOException) {
196 reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
197 errorTextWriter
198 }
199
200 startFile(sourceFile)
201
202 stubWriter = if (options.kotlinStubs && cls.isKotlin()) {
203 KotlinStubWriter(textWriter, filterEmit, filterReference, generateAnnotations, preFiltered, docStubs)
204 } else {
205 JavaStubWriter(textWriter, filterEmit, filterReference, generateAnnotations, preFiltered, docStubs)
206 }
207
208 // Copyright statements from the original file?
209 cls.getSourceFile()?.getHeaderComments()?.let { textWriter.println(it) }
210 }
211 stubWriter?.visitClass(cls)
212 }
213
214 override fun afterVisitClass(cls: ClassItem) {
215 stubWriter?.afterVisitClass(cls)
216
217 if (cls.isTopLevelClass()) {
218 textWriter.flush()
219 textWriter.close()
220 textWriter = errorTextWriter
221 stubWriter = null
222 }
223 }
224
225 override fun visitConstructor(constructor: ConstructorItem) {
226 stubWriter?.visitConstructor(constructor)
227 }
228
229 override fun afterVisitConstructor(constructor: ConstructorItem) {
230 stubWriter?.afterVisitConstructor(constructor)
231 }
232
233 override fun visitMethod(method: MethodItem) {
234 stubWriter?.visitMethod(method)
235 }
236
237 override fun afterVisitMethod(method: MethodItem) {
238 stubWriter?.afterVisitMethod(method)
239 }
240
241 override fun visitField(field: FieldItem) {
242 stubWriter?.visitField(field)
243 }
244
245 override fun afterVisitField(field: FieldItem) {
246 stubWriter?.afterVisitField(field)
247 }
248 }
249
appendDocumentationnull250 internal fun appendDocumentation(
251 item: Item,
252 writer: PrintWriter,
253 docStubs: Boolean
254 ) {
255 if (options.includeDocumentationInStubs || docStubs) {
256 val documentation = item.fullyQualifiedDocumentation()
257 if (documentation.isNotBlank()) {
258 val trimmed = trimDocIndent(documentation)
259 writer.println(trimmed)
260 writer.println()
261 }
262 }
263 }
264