| /* |
| * 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() |
| } |
| } |
| } |