blob: bf611880a54edc76c6b1cc3f3cb91fbcb061f245 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION")
package com.android.tools.metalava.stub
import com.android.tools.metalava.ApiPredicate
import com.android.tools.metalava.FilterPredicate
import com.android.tools.metalava.model.AnnotationTarget
import com.android.tools.metalava.model.BaseItemVisitor
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.ConstructorItem
import com.android.tools.metalava.model.FieldItem
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.ModifierList
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.psi.trimDocIndent
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.android.tools.metalava.options
import com.android.tools.metalava.reporter.Issues
import com.android.tools.metalava.reporter.Reporter
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.io.PrintWriter
import java.io.Writer
class StubWriter(
private val codebase: Codebase,
private val stubsDir: File,
private val generateAnnotations: Boolean = false,
private val preFiltered: Boolean = true,
private val docStubs: Boolean,
private val reporter: Reporter,
) :
ApiVisitor(
visitConstructorsAsMethods = false,
nestInnerClasses = true,
inlineInheritedFields = true,
fieldComparator = FieldItem.comparator,
// Methods are by default sorted in source order in stubs, to encourage methods
// that are near each other in the source to show up near each other in the documentation
methodComparator = MethodItem.sourceOrderComparator,
filterEmit = FilterPredicate(apiPredicate(docStubs)),
filterReference = apiPredicate(docStubs),
includeEmptyOuterClasses = true
) {
private val annotationTarget =
if (docStubs) AnnotationTarget.DOC_STUBS_FILE else AnnotationTarget.SDK_STUBS_FILE
private val sourceList = StringBuilder(20000)
/** Writes a source file list of the generated stubs */
fun writeSourceList(target: File, root: File?) {
target.parentFile?.mkdirs()
val contents =
if (root != null) {
val path = root.path.replace('\\', '/') + "/"
sourceList.toString().replace(path, "")
} else {
sourceList.toString()
}
target.writeText(contents)
}
private fun startFile(sourceFile: File) {
if (sourceList.isNotEmpty()) {
sourceList.append(' ')
}
sourceList.append(sourceFile.path.replace('\\', '/'))
}
override fun visitPackage(pkg: PackageItem) {
getPackageDir(pkg, create = true)
writePackageInfo(pkg)
if (docStubs) {
codebase.getPackageDocs()?.let { packageDocs ->
packageDocs.getOverviewDocumentation(pkg)?.let { writeDocOverview(pkg, it) }
}
}
}
fun writeDocOverview(pkg: PackageItem, content: String) {
if (content.isBlank()) {
return
}
val sourceFile = File(getPackageDir(pkg), "overview.html")
val overviewWriter =
try {
PrintWriter(BufferedWriter(FileWriter(sourceFile)))
} catch (e: IOException) {
reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
return
}
// Should we include this in our stub list?
// startFile(sourceFile)
overviewWriter.println(content)
overviewWriter.flush()
overviewWriter.close()
}
private fun writePackageInfo(pkg: PackageItem) {
val annotations = pkg.modifiers.annotations()
val writeAnnotations = annotations.isNotEmpty() && generateAnnotations
val writeDocumentation = docStubs && pkg.documentation.isNotBlank()
if (writeAnnotations || writeDocumentation) {
val sourceFile = File(getPackageDir(pkg), "package-info.java")
val packageInfoWriter =
try {
PrintWriter(BufferedWriter(FileWriter(sourceFile)))
} catch (e: IOException) {
reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
return
}
startFile(sourceFile)
appendDocumentation(pkg, packageInfoWriter, docStubs)
if (annotations.isNotEmpty()) {
ModifierList.writeAnnotations(
list = pkg.modifiers,
separateLines = true,
// Some bug in UAST triggers duplicate nullability annotations
// here; make sure they are filtered out
filterDuplicates = true,
target = annotationTarget,
writer = packageInfoWriter
)
}
packageInfoWriter.println("package ${pkg.qualifiedName()};")
packageInfoWriter.flush()
packageInfoWriter.close()
}
}
private fun getPackageDir(packageItem: PackageItem, create: Boolean = true): File {
val relative = packageItem.qualifiedName().replace('.', File.separatorChar)
val dir = File(stubsDir, relative)
if (create && !dir.isDirectory) {
val ok = dir.mkdirs()
if (!ok) {
throw IOException("Could not create $dir")
}
}
return dir
}
private fun getClassFile(classItem: ClassItem): File {
assert(classItem.containingClass() == null) { "Should only be called on top level classes" }
val packageDir = getPackageDir(classItem.containingPackage())
// Kotlin From-text stub generation is not supported.
// This method will raise an error if
// options.kotlinStubs == true and classItem is TextClassItem.
return if (options.kotlinStubs && classItem.isKotlin()) {
File(packageDir, "${classItem.simpleName()}.kt")
} else {
File(packageDir, "${classItem.simpleName()}.java")
}
}
/**
* Between top level class files the [textWriter] field doesn't point to a real file; it points
* to this writer, which redirects to the error output. Nothing should be written to the writer
* at that time.
*/
private var errorTextWriter =
PrintWriter(
object : Writer() {
override fun close() {
throw IllegalStateException(
"Attempt to close 'textWriter' outside top level class"
)
}
override fun flush() {
throw IllegalStateException(
"Attempt to flush 'textWriter' outside top level class"
)
}
override fun write(cbuf: CharArray, off: Int, len: Int) {
throw IllegalStateException(
"Attempt to write to 'textWriter' outside top level class\n'${String(cbuf, off, len)}'"
)
}
}
)
/** The writer to write the stubs file to */
private var textWriter: PrintWriter = errorTextWriter
private var stubWriter: BaseItemVisitor? = null
override fun visitClass(cls: ClassItem) {
if (cls.isTopLevelClass()) {
val sourceFile = getClassFile(cls)
textWriter =
try {
PrintWriter(BufferedWriter(FileWriter(sourceFile)))
} catch (e: IOException) {
reporter.report(Issues.IO_ERROR, sourceFile, "Cannot open file for write.")
errorTextWriter
}
startFile(sourceFile)
stubWriter =
if (options.kotlinStubs && cls.isKotlin()) {
KotlinStubWriter(
textWriter,
filterReference,
generateAnnotations,
preFiltered,
docStubs
)
} else {
JavaStubWriter(
textWriter,
filterEmit,
filterReference,
generateAnnotations,
preFiltered,
docStubs
)
}
// Copyright statements from the original file?
cls.getSourceFile()?.getHeaderComments()?.let { textWriter.println(it) }
}
stubWriter?.visitClass(cls)
}
override fun afterVisitClass(cls: ClassItem) {
stubWriter?.afterVisitClass(cls)
if (cls.isTopLevelClass()) {
textWriter.flush()
textWriter.close()
textWriter = errorTextWriter
stubWriter = null
}
}
override fun visitConstructor(constructor: ConstructorItem) {
stubWriter?.visitConstructor(constructor)
}
override fun afterVisitConstructor(constructor: ConstructorItem) {
stubWriter?.afterVisitConstructor(constructor)
}
override fun visitMethod(method: MethodItem) {
stubWriter?.visitMethod(method)
}
override fun afterVisitMethod(method: MethodItem) {
stubWriter?.afterVisitMethod(method)
}
override fun visitField(field: FieldItem) {
stubWriter?.visitField(field)
}
override fun afterVisitField(field: FieldItem) {
stubWriter?.afterVisitField(field)
}
}
private fun apiPredicate(docStubs: Boolean) =
ApiPredicate(
includeDocOnly = docStubs,
config = options.apiPredicateConfig.copy(ignoreShown = true)
)
internal fun appendDocumentation(item: Item, writer: PrintWriter, docStubs: Boolean) {
if (options.includeDocumentationInStubs || docStubs) {
val documentation = item.fullyQualifiedDocumentation()
if (documentation.isNotBlank()) {
val trimmed = trimDocIndent(documentation)
writer.println(trimmed)
writer.println()
}
}
}