• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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