1 /* <lambda>null2 * Copyright (C) 2025 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.cli.historical 18 19 import com.android.tools.metalava.apilevels.ApiVersion 20 import com.android.tools.metalava.apilevels.MatchedPatternFile 21 import com.android.tools.metalava.apilevels.PatternNode 22 import com.android.tools.metalava.model.api.surface.ApiSurface 23 import com.android.tools.metalava.model.text.SignatureFile 24 import com.android.tools.metalava.reporter.Issues 25 import com.android.tools.metalava.reporter.Reporter 26 import java.io.File 27 28 /** 29 * Encapsulates information relating to a historical API version. 30 * 31 * Includes all API files related to [version], and the set of [ApiSurface]s. The [infoBySurface] 32 * maps from [ApiSurface] to [SurfaceInfo]. 33 */ 34 class HistoricalApiVersionInfo 35 private constructor( 36 /** The [ApiVersion] to which this refers. */ 37 val version: ApiVersion, 38 /** Information about each surface found for this version. */ 39 val infoBySurface: Map<ApiSurface, SurfaceInfo>, 40 ) { 41 companion object { 42 /** 43 * Scan for historical API version information and create a list of 44 * [HistoricalApiVersionInfo] representing that. 45 * 46 * @param reporter for errors found. 47 * @param jarFilePattern the pattern for jar files, see [PatternNode.parsePatterns]. 48 * @param signatureFilePattern the pattern for signature files, see 49 * [PatternNode.parsePatterns]. 50 * @param scanConfig see [PatternNode.scan]. 51 */ 52 internal fun scan( 53 reporter: Reporter, 54 jarFilePattern: String, 55 signatureFilePattern: String, 56 scanConfig: PatternNode.ScanConfig, 57 ): List<HistoricalApiVersionInfo> { 58 // Get all the matching jar and signature files. 59 val matchedPatternFiles = 60 scanForPattern(jarFilePattern, scanConfig) + 61 scanForPattern(signatureFilePattern, scanConfig) 62 63 // Construct a list of HistoricalApiVersionInfo from them. 64 return fromMatchedPatternFiles(reporter, matchedPatternFiles) 65 } 66 67 /** 68 * Construct a [PatternNode] for [pattern] and then use that to scan for files with 69 * [scanConfig]. 70 */ 71 private fun scanForPattern(pattern: String, scanConfig: PatternNode.ScanConfig) = 72 PatternNode.parsePatterns(listOf(pattern)).scan(scanConfig) 73 74 /** Construct a list of [HistoricalApiVersionInfo]s from a list of [MatchedPatternFile]s. */ 75 private fun fromMatchedPatternFiles( 76 reporter: Reporter, 77 matchedPatternFiles: List<MatchedPatternFile>, 78 ) = 79 // Group by versions. 80 matchedPatternFiles 81 .groupBy { it.version } 82 // Map to list of HistoricalApiVersionInfo 83 .mapNotNull { (version, versionFiles) -> 84 fromVersionFiles(reporter, version, versionFiles) 85 } 86 87 /** 88 * Construct a [HistoricalApiVersionInfo] from [versionFiles] for [version]. 89 * 90 * If an error is encountered which prevents it from being constructed then the error is 91 * reported to [reporter] and this returns `null`. 92 */ 93 private fun fromVersionFiles( 94 reporter: Reporter, 95 version: ApiVersion, 96 versionFiles: List<MatchedPatternFile>, 97 ): HistoricalApiVersionInfo? { 98 val noSurface = versionFiles.filter { it.surface == null } 99 if (noSurface.isNotEmpty()) { 100 reporter.report( 101 Issues.IO_ERROR, 102 reportable = null, 103 "All files must have a surface but found ${noSurface.size} files without:${ 104 noSurface.joinToString( 105 "\n ", 106 prefix = "\n " 107 ) { it.file.path } 108 }" 109 ) 110 return null 111 } 112 113 val infoBySurface = 114 buildMap<ApiSurface, SurfaceInfo> { 115 val bySurface = versionFiles.groupBy { it.surface!! } 116 for ((surface, surfaceFiles) in bySurface) { 117 fromSurfaceFiles(reporter, this, version, surface, surfaceFiles) 118 } 119 } 120 121 return HistoricalApiVersionInfo(version, infoBySurface) 122 } 123 124 /** 125 * Construct a [SurfaceInfo] from [surfaceFiles] for [surface] in [version] and add it to 126 * [infoBySurface]. 127 * 128 * If an error is encountered which prevents it from being constructed then the error is 129 * reported to [reporter] and this returns `null`. 130 */ 131 private fun fromSurfaceFiles( 132 reporter: Reporter, 133 infoBySurface: MutableMap<ApiSurface, SurfaceInfo>, 134 version: ApiVersion, 135 surface: ApiSurface, 136 surfaceFiles: List<MatchedPatternFile>, 137 ) { 138 // Partition into jar files and other files which are assumed to be signature files. 139 val (jarFiles, signatureFiles) = 140 surfaceFiles.map { it.file }.partition { it.extension == ("jar") } 141 142 val jarFile = 143 singleFileIfPossible(jarFiles, reporter, version, surface, "jar") ?: return 144 145 val signatureFile = 146 singleFileIfPossible(signatureFiles, reporter, version, surface, "signature") 147 ?: return 148 149 val extendsInfo = surface.extends?.let { infoBySurface[it] } 150 151 val info = SurfaceInfo(surface, jarFile, signatureFile, extendsInfo) 152 infoBySurface[surface] = info 153 } 154 155 /** 156 * Get a single file from [files]. 157 * 158 * If [files] is empty then it is an error and this will return `null` as it has no [File] 159 * to return. If it has more than one [File] then it is also an error, but it returns the 160 * first [File] in the list. Otherwise, it just returns the first [File]. 161 */ 162 private fun singleFileIfPossible( 163 files: List<File>, 164 reporter: Reporter, 165 version: ApiVersion, 166 surface: ApiSurface, 167 label: String, 168 ): File? { 169 val count = files.size 170 if (count != 1) { 171 if (count == 0) { 172 reporter.report( 173 Issues.IO_ERROR, 174 reportable = null, 175 "Expected exactly one $label file per version per surface but did not find any, skipping version $version, surface $surface" 176 ) 177 return null 178 } else { 179 reporter.report( 180 Issues.IO_ERROR, 181 reportable = null, 182 "Version $version: Expected exactly one $label file per version but found $count; using first:${ 183 files.joinToString( 184 "\n ", 185 prefix = "\n " 186 ) 187 }" 188 ) 189 } 190 } 191 192 return files.first() 193 } 194 } 195 } 196 197 /** Information related to a specific surface. */ 198 class SurfaceInfo( 199 /** The [ApiSurface] to which this refers. */ 200 val surface: ApiSurface, 201 202 /** The jar [File]. */ 203 val jarFile: File, 204 205 /** The signature [File]. */ 206 val signatureFile: File, 207 208 /** 209 * Optional [SurfaceInfo] that this extends. 210 * 211 * This refers to the [SurfaceInfo] corresponding to [surface]'s [ApiSurface.extends] property, 212 * if any. 213 */ 214 val extends: SurfaceInfo?, 215 ) { 216 /** 217 * Get the [SignatureFile]s that contribute to this surface. 218 * 219 * In order, such that the file for an extending surface comes after the files for the extended 220 * surface. 221 */ contributingSignatureFilesnull222 fun contributingSignatureFiles(): List<SignatureFile> { 223 val files = buildList { addContributingSignatureFiles(this) } 224 return SignatureFile.fromFiles(files) 225 } 226 227 /** 228 * Add extended [SurfaceInfo]s files that contribute to this surface, if any, followed by this 229 * surfaces file. 230 */ addContributingSignatureFilesnull231 private fun addContributingSignatureFiles(list: MutableList<File>) { 232 // Add any files for the extended surface, if any, first. 233 extends?.addContributingSignatureFiles(list) 234 235 // Then add the files for this surface. 236 list.add(signatureFile) 237 } 238 } 239