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