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.doclava1.ApiPredicate
20 import com.android.tools.metalava.doclava1.FilterPredicate
21 import com.android.tools.metalava.doclava1.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 if (annotations.isNotEmpty() && generateAnnotations || !pkg.documentation.isBlank()) {
120 val sourceFile = File(getPackageDir(pkg), "package-info.java")
121 val packageInfoWriter = try {
122 PrintWriter(BufferedWriter(FileWriter(sourceFile)))
123 } catch (e: IOException) {
124 reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
125 return
126 }
127 startFile(sourceFile)
128
129 appendDocumentation(pkg, packageInfoWriter, docStubs)
130
131 if (annotations.isNotEmpty()) {
132 ModifierList.writeAnnotations(
133 list = pkg.modifiers,
134 separateLines = true,
135 // Some bug in UAST triggers duplicate nullability annotations
136 // here; make sure the are filtered out
137 filterDuplicates = true,
138 target = annotationTarget,
139 writer = packageInfoWriter
140 )
141 }
142 packageInfoWriter.println("package ${pkg.qualifiedName()};")
143
144 packageInfoWriter.flush()
145 packageInfoWriter.close()
146 }
147 }
148
149 private fun getPackageDir(packageItem: PackageItem, create: Boolean = true): File {
150 val relative = packageItem.qualifiedName().replace('.', File.separatorChar)
151 val dir = File(stubsDir, relative)
152 if (create && !dir.isDirectory) {
153 val ok = dir.mkdirs()
154 if (!ok) {
155 throw IOException("Could not create $dir")
156 }
157 }
158
159 return dir
160 }
161
162 private fun getClassFile(classItem: ClassItem): File {
163 assert(classItem.containingClass() == null) { "Should only be called on top level classes" }
164 // TODO: Look up compilation unit language
165 return File(getPackageDir(classItem.containingPackage()), "${classItem.simpleName()}.java")
166 }
167
168 /**
169 * Between top level class files the [textWriter] field doesn't point to a real file; it
170 * points to this writer, which redirects to the error output. Nothing should be written
171 * to the writer at that time.
172 */
173 private var errorTextWriter = PrintWriter(options.stderr)
174
175 /** The writer to write the stubs file to */
176 private var textWriter: PrintWriter = errorTextWriter
177
178 private var stubWriter: ItemVisitor? = null
179
180 override fun visitClass(cls: ClassItem) {
181 if (cls.isTopLevelClass()) {
182 val sourceFile = getClassFile(cls)
183 textWriter = try {
184 PrintWriter(BufferedWriter(FileWriter(sourceFile)))
185 } catch (e: IOException) {
186 reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
187 errorTextWriter
188 }
189
190 startFile(sourceFile)
191
192 stubWriter = if (options.kotlinStubs && cls.isKotlin()) {
193 KotlinStubWriter(textWriter, filterEmit, filterReference, generateAnnotations, preFiltered, docStubs)
194 } else {
195 JavaStubWriter(textWriter, filterEmit, filterReference, generateAnnotations, preFiltered, docStubs)
196 }
197
198 // Copyright statements from the original file?
199 val compilationUnit = cls.getCompilationUnit()
200 compilationUnit?.getHeaderComments()?.let { textWriter.println(it) }
201 }
202 stubWriter?.visitClass(cls)
203 }
204
205 override fun afterVisitClass(cls: ClassItem) {
206 stubWriter?.afterVisitClass(cls)
207
208 if (cls.isTopLevelClass()) {
209 textWriter.flush()
210 textWriter.close()
211 textWriter = errorTextWriter
212 stubWriter = null
213 }
214 }
215
216 override fun visitConstructor(constructor: ConstructorItem) {
217 stubWriter?.visitConstructor(constructor)
218 }
219
220 override fun afterVisitConstructor(constructor: ConstructorItem) {
221 stubWriter?.afterVisitConstructor(constructor)
222 }
223
224 override fun visitMethod(method: MethodItem) {
225 stubWriter?.visitMethod(method)
226 }
227
228 override fun afterVisitMethod(method: MethodItem) {
229 stubWriter?.afterVisitMethod(method)
230 }
231
232 override fun visitField(field: FieldItem) {
233 stubWriter?.visitField(field)
234 }
235
236 override fun afterVisitField(field: FieldItem) {
237 stubWriter?.afterVisitField(field)
238 }
239 }
240
appendDocumentationnull241 internal fun appendDocumentation(
242 item: Item,
243 writer: PrintWriter,
244 docStubs: Boolean
245 ) {
246 if (options.includeDocumentationInStubs || docStubs) {
247 val documentation = if (docStubs) {
248 item.fullyQualifiedDocumentation()
249 } else {
250 item.documentation
251 }
252 if (documentation.isNotBlank()) {
253 val trimmed = trimDocIndent(documentation)
254 writer.println(trimmed)
255 writer.println()
256 }
257 }
258 }