1 /* 2 * Copyright (C) 2024 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.model.source.utils 18 19 import com.android.tools.metalava.model.ItemDocumentation.Companion.toItemDocumentationFactory 20 import com.android.tools.metalava.model.item.MutablePackageDoc 21 import com.android.tools.metalava.model.item.PackageDocs 22 import com.android.tools.metalava.model.item.ResourceFile 23 import com.android.tools.metalava.model.source.SourceSet 24 import com.android.tools.metalava.reporter.FileLocation 25 import com.android.tools.metalava.reporter.Issues 26 import com.android.tools.metalava.reporter.Reporter 27 import java.io.File 28 29 /** The kinds of package documentation file. */ 30 private enum class PackageDocumentationKind { 31 PACKAGE { updatenull32 override fun update(packageDoc: MutablePackageDoc, file: File) { 33 val contents = file.readText(Charsets.UTF_8) 34 packageDoc.commentFactory = packageHtmlToJavadoc(contents).toItemDocumentationFactory() 35 packageDoc.fileLocation = FileLocation.forFile(file) 36 } 37 }, 38 OVERVIEW { updatenull39 override fun update(packageDoc: MutablePackageDoc, file: File) { 40 packageDoc.overview = ResourceFile(file) 41 } 42 }; 43 44 /** Update kind appropriate property in [packageDoc] with [contents]. */ updatenull45 abstract fun update(packageDoc: MutablePackageDoc, file: File) 46 } 47 48 /** 49 * Gather javadoc related to packages from the [sourceSet] and a list of model specific 50 * [packageInfoFiles]. 51 * 52 * This will look for `package.html` and `overview.html` files within the source set and then map 53 * that back to a package. It will first check to see if there is a java class in the same directory 54 * and if so then extract the package name from that otherwise it will construct one from the 55 * directory, which may be wrong. 56 * 57 * If a `package.html` and `package-info.java` are provided for the same package then it will be 58 * reported as an error and the comment from the latter will win. 59 * 60 * @param P the model specific `package-info.java` file type. 61 * @param packageNameFilter a lambda that given a package name will return `true` if it is a valid 62 * package and `false` otherwise. This is used to filter out any packages incorrectly inferred 63 * from `package.html` files. 64 * @param packageInfoFiles a collection of model specific `package-info.java` files. 65 * @param packageInfoDocExtractor get a [MutablePackageDoc] from a model specific 66 * `package-info.java` file. 67 */ 68 fun <P> gatherPackageJavadoc( 69 reporter: Reporter, 70 sourceSet: SourceSet, 71 packageNameFilter: (String) -> Boolean, 72 packageInfoFiles: Collection<P>, 73 packageInfoDocExtractor: (P) -> MutablePackageDoc?, 74 ): PackageDocs { 75 val packages = mutableMapOf<String, MutablePackageDoc>() 76 val sortedSourceRoots = sourceSet.sourcePath.sortedBy { -it.name.length } 77 for (file in sourceSet.sources) { 78 val documentationFile = 79 when (file.name) { 80 PACKAGE_HTML -> { 81 PackageDocumentationKind.PACKAGE 82 } 83 OVERVIEW_HTML -> { 84 PackageDocumentationKind.OVERVIEW 85 } 86 else -> continue 87 } 88 89 // Figure out the package: if there is a java file in the same directory, get the package 90 // name from the java file. Otherwise, guess from the directory path + source roots. 91 // NOTE: This causes metalava to read files other than the ones explicitly passed to it. 92 var pkg = 93 file.parentFile 94 ?.listFiles() 95 ?.filter { it.name.endsWith(DOT_JAVA) } 96 ?.asSequence() 97 ?.mapNotNull { findPackage(it) } 98 ?.firstOrNull() 99 if (pkg == null) { 100 // Strip the longest prefix source root. 101 val prefix = sortedSourceRoots.firstOrNull { file.startsWith(it) }?.path ?: "" 102 pkg = file.parentFile.path.substring(prefix.length).trim('/').replace("/", ".") 103 } 104 105 // If the package name is invalid then skip it. 106 if (!packageNameFilter(pkg)) continue 107 108 val packageDoc = packages.computeIfAbsent(pkg, ::MutablePackageDoc) 109 110 documentationFile.update(packageDoc, file) 111 } 112 113 // Merge package-info.java documentation. 114 for (packageInfoFile in packageInfoFiles) { 115 val (packageName, fileLocation, modifiers, comment, _) = 116 packageInfoDocExtractor(packageInfoFile) ?: continue 117 118 val packageDoc = packages.computeIfAbsent(packageName, ::MutablePackageDoc) 119 if (packageDoc.commentFactory != null) { 120 reporter.report( 121 Issues.BOTH_PACKAGE_INFO_AND_HTML, 122 null, 123 "It is illegal to provide both a package-info.java file and " + 124 "a package.html file for the same package", 125 fileLocation, 126 ) 127 } 128 129 // Always set this as package-info.java is preferred over package.html. 130 packageDoc.fileLocation = fileLocation 131 packageDoc.modifiers = modifiers 132 packageDoc.commentFactory = comment 133 } 134 135 return PackageDocs(packages) 136 } 137