• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.text
18 
19 import com.android.tools.metalava.model.CallableItem
20 import com.android.tools.metalava.model.StripJavaLangPrefix
21 import com.android.tools.metalava.reporter.FileLocation
22 import java.io.LineNumberReader
23 import java.io.Reader
24 import java.nio.file.Path
25 import java.util.Locale
26 
27 /**
28  * Encapsulates all the information related to the format of a signature file.
29  *
30  * Some of these will be initialized from the version specific defaults and some will be overridden
31  * on the command line.
32  */
33 data class FileFormat(
34     val version: Version,
35     /**
36      * If specified then it contains property defaults that have been specified on the command line
37      * and whose value should be used as the default for any property that has not been specified in
38      * this format.
39      *
40      * Not every property is eligible to have its default overridden on the command line. Only those
41      * that have a property getter to provide the default.
42      */
43     val formatDefaults: FileFormat? = null,
44 
45     /**
46      * If non-null then it specifies the name of the API.
47      *
48      * It must start with a lower case letter, contain any number of lower case letters, numbers and
49      * hyphens, and end with either a lowercase letter or number.
50      *
51      * Its purpose is to provide information to metalava and to a lesser extent the owner of the
52      * file about which API the file contains. The exact meaning of the API name is determined by
53      * the owner, metalava simply uses this as an identifier for comparison.
54      */
55     val name: String? = null,
56 
57     /**
58      * If non-null then it specifies the name of the API surface.
59      *
60      * It must start with a lower case letter, contain any number of lower case letters, numbers and
61      * hyphens, and end with either a lowercase letter or number.
62      *
63      * Its purpose is to provide information to metalava and to a lesser extent the owner of the
64      * file about which API surface the file contains. The exact meaning of the API surface name is
65      * determined by the owner, metalava simply uses this as an identifier for comparison.
66      */
67     val surface: String? = null,
68 
69     /**
70      * If non-null then it indicates the target language for signature files.
71      *
72      * Although kotlin and java can interoperate reasonably well an API created from Java files is
73      * generally targeted for use by Java code and vice versa.
74      */
75     val language: Language? = null,
76     val specifiedOverloadedMethodOrder: OverloadedMethodOrder? = null,
77 
78     /**
79      * Whether to include type-use annotations in the signature file. Type-use annotations can only
80      * be included when [kotlinNameTypeOrder] is true, because the Java order makes it ambiguous
81      * whether an annotation is type-use.
82      */
83     val includeTypeUseAnnotations: Boolean = false,
84 
85     /**
86      * Whether to order the names and types of APIs using Kotlin-style syntax (`name: type`) or
87      * Java-style syntax (`type name`).
88      *
89      * When Kotlin ordering is used, all method parameters without public names will be given the
90      * placeholder name of `_`, which cannot be used as a Java identifier.
91      *
92      * For example, the following is an example of a method signature with Kotlin ordering:
93      * ```
94      * method public foo(_: int, _: char, _: String[]): String;
95      * ```
96      *
97      * And the following is the equivalent Java ordering:
98      * ```
99      * method public String foo(int, char, String[]);
100      * ```
101      */
102     val kotlinNameTypeOrder: Boolean = false,
103     val kotlinStyleNulls: Boolean,
104     /**
105      * If non-null then it indicates that the file format is being used to migrate a signature file
106      * to fix a bug that causes a change in the signature file contents but not a change in version.
107      * e.g. This would be used when migrating a 2.0 file format that currently uses source order for
108      * overloaded methods (using a command line parameter to override the default order of
109      * signature) to a 2.0 file that uses signature order.
110      *
111      * This should be used to provide an explanation as to what is being migrated and why. It should
112      * be relatively concise, e.g. something like:
113      * ```
114      * "See <short-url> for details"
115      * ```
116      *
117      * This value cannot use `,` (because it is a separator between properties in [specifier]) or
118      * `\n` (because it is the terminator of the signature format line).
119      */
120     val migrating: String? = null,
121     val includeDefaultParameterValues: Boolean,
122     val specifiedAddAdditionalOverrides: Boolean? = null,
123 
124     /** See [CustomizableProperty.NORMALIZE_FINAL_MODIFIER]. */
125     val specifiedNormalizeFinalModifier: Boolean? = null,
126 
127     /**
128      * Indicates whether the whole extends list for an interface is sorted.
129      *
130      * Previously, the first type in the extends list was used as the super type and if it was
131      * present in the API then it would always be output first to the signature files. The code has
132      * been refactored so that is no longer necessary but the previous behavior is maintained to
133      * avoid churn in the API signature files.
134      *
135      * By default, this property preserves the previous behavior but if set to `true` then it will
136      * stop treating the first interface specially and just sort all the interface types. The
137      * sorting is by the full name (without the package) of the class.
138      */
139     val specifiedSortWholeExtendsList: Boolean? = null,
140 
141     /**
142      * Indicates which of the possible approaches to `java.lang.` prefix stripping available in
143      * [StripJavaLangPrefix] is used when outputting types to signature files.
144      */
145     val specifiedStripJavaLangPrefix: StripJavaLangPrefix? = null,
146 
147     /**
148      * Indicates how type arguments should be formatted when outputting types to signature files.
149      */
150     val specifiedTypeArgumentSpacing: TypeArgumentSpacing? = null,
151 ) {
152     init {
153         if (migrating != null && "[,\n]".toRegex().find(migrating) != null) {
154             throw IllegalStateException(
155                 """invalid value for property 'migrating': '$migrating' contains at least one invalid character from the set {',', '\n'}"""
156             )
157         }
158 
159         validateIdentifier(name, "name")
160         validateIdentifier(surface, "surface")
161 
162         if (includeTypeUseAnnotations && !kotlinNameTypeOrder) {
163             throw IllegalStateException(
164                 "Type-use annotations can only be included in signatures when `kotlin-name-type-order=yes` is set"
165             )
166         }
167     }
168 
169     /** Check that the supplied identifier is valid. */
170     private fun validateIdentifier(identifier: String?, propertyName: String) {
171         identifier ?: return
172         if ("[a-z]([a-z0-9-]*[a-z0-9])?".toRegex().matchEntire(identifier) == null) {
173             throw IllegalStateException(
174                 """invalid value for property '$propertyName': '$identifier' must start with a lower case letter, contain any number of lower case letters, numbers and hyphens, and end with either a lowercase letter or number"""
175             )
176         }
177     }
178 
179     /**
180      * Compute the effective value of an optional property whose default can be overridden.
181      *
182      * This returns the first non-null value in the following:
183      * 1. This [FileFormat]'s property value.
184      * 2. The [formatDefaults]'s property value
185      * 3. The [default] value.
186      *
187      * @param getter a getter for the optional property's value.
188      * @param default the default value.
189      */
190     private inline fun <T> effectiveValue(getter: FileFormat.() -> T?, default: T): T {
191         return this.getter() ?: formatDefaults?.getter() ?: default
192     }
193 
194     // This defaults to SIGNATURE but can be overridden on the command line.
195     val overloadedMethodOrder
196         get() = effectiveValue({ specifiedOverloadedMethodOrder }, OverloadedMethodOrder.SIGNATURE)
197 
198     // This defaults to false but can be overridden on the command line.
199     val addAdditionalOverrides
200         get() = effectiveValue({ specifiedAddAdditionalOverrides }, false)
201 
202     // This defaults to false but can be overridden on the command line.
203     val normalizeFinalModifier
204         get() = effectiveValue({ specifiedNormalizeFinalModifier }, false)
205 
206     // This defaults to false but can be overridden on the command line.
207     val sortWholeExtendsList
208         get() = effectiveValue({ specifiedSortWholeExtendsList }, default = false)
209 
210     // This defaults to LEGACY but can be overridden on the command line.
211     val stripJavaLangPrefix
212         get() = effectiveValue({ specifiedStripJavaLangPrefix }, StripJavaLangPrefix.LEGACY)
213 
214     // This defaults to LEGACY but can be overridden on the command line.
215     val typeArgumentSpacing
216         get() = effectiveValue({ specifiedTypeArgumentSpacing }, TypeArgumentSpacing.LEGACY)
217 
218     /** The base version of the file format. */
219     enum class Version(
220         /** The version number of this as a string, e.g. "3.0". */
221         val versionNumber: String,
222         /** The optional legacy alias used on the command line, for the `--format` option. */
223         val legacyCommandLineAlias: String? = null,
224 
225         /** Indicates whether the version supports properties fully or just for migrating. */
226         internal val propertySupport: PropertySupport = PropertySupport.FOR_MIGRATING_ONLY,
227 
228         /**
229          * Factory used to create a [FileFormat] instance encapsulating the defaults of this
230          * version.
231          */
232         factory: (Version) -> FileFormat,
233         /** Help text to use on the command line. */
234         val help: String,
235     ) {
236         V2(
237             versionNumber = "2.0",
238             legacyCommandLineAlias = "v2",
239             factory = { version ->
240                 FileFormat(
241                     version = version,
242                     kotlinStyleNulls = false,
243                     includeDefaultParameterValues = false,
244                 )
245             },
246             help =
247                 """
248                     This is the base version (more details in `FORMAT.md`) on which all the others
249                     are based. It sets the properties as follows:
250                     ```
251                     + kotlin-style-nulls = no
252                     + include-default-parameter-values = no
253                     ```
254                 """,
255         ),
256         V4(
257             versionNumber = "4.0",
258             legacyCommandLineAlias = "v4",
259             factory = { version ->
260                 V2.defaults.copy(
261                     version = version,
262                     // This adds kotlinStyleNulls = true
263                     kotlinStyleNulls = true,
264                     // This adds conciseDefaultValues = true
265                     includeDefaultParameterValues = true,
266                 )
267             },
268             help =
269                 """
270                     This is `2.0` plus `kotlin-style-nulls = yes` and `include-default-parameter-values = yes`
271                     giving the following properties:
272                     ```
273                     + kotlin-style-nulls = yes
274                     + include-default-parameter-values = yes
275                     ```
276                 """,
277         ),
278         V5(
279             versionNumber = "5.0",
280             // This adds full property support.
281             propertySupport = PropertySupport.FULL,
282             factory = { version ->
283                 V4.defaults.copy(
284                     version = version,
285                     // This does not add any property defaults, just full property support.
286                 )
287             },
288             help =
289                 """
290                     This is the first version that has full support for properties in the signature
291                     header. As such it does not add any new defaults to `4.0`. The intent is that
292                     properties will be explicitly defined in the signature file avoiding reliance on
293                     version specific defaults.
294                 """,
295         );
296 
297         /**
298          * The defaults associated with this version.
299          *
300          * It is initialized via a factory to break the cycle where the [Version] constructor
301          * depends on the [FileFormat] constructor and vice versa.
302          */
303         val defaults = factory(this)
304 
305         /**
306          * Get the version defaults plus any language defaults, if available.
307          *
308          * @param language the optional language whose defaults should be applied to the version
309          *   defaults.
310          */
311         internal fun defaultsIncludingLanguage(language: Language?): FileFormat {
312             language ?: return defaults
313             return Builder(defaults).let {
314                 language.applyLanguageDefaults(it)
315                 it.build()
316             }
317         }
318     }
319 
320     internal enum class PropertySupport {
321         /**
322          * The version only supports properties being temporarily specified in the signature file to
323          * aid migration.
324          */
325         FOR_MIGRATING_ONLY,
326 
327         /**
328          * The version supports properties fully, both for migration and permanent customization in
329          * the signature file.
330          */
331         FULL
332     }
333 
334     /**
335      * The language which the signature targets. While a Java API can be used by Kotlin, and vice
336      * versa, each API typically targets a specific language and this specifies that.
337      *
338      * This is independent of the [Version].
339      */
340     enum class Language(
341         private val includeDefaultParameterValues: Boolean,
342         private val kotlinStyleNulls: Boolean,
343     ) {
344         JAVA(includeDefaultParameterValues = false, kotlinStyleNulls = false),
345         KOTLIN(includeDefaultParameterValues = true, kotlinStyleNulls = true);
346 
347         internal fun applyLanguageDefaults(builder: Builder) {
348             if (builder.includeDefaultParameterValues == null) {
349                 builder.includeDefaultParameterValues = includeDefaultParameterValues
350             }
351             if (builder.kotlinStyleNulls == null) {
352                 builder.kotlinStyleNulls = kotlinStyleNulls
353             }
354         }
355     }
356 
357     enum class OverloadedMethodOrder(val comparator: Comparator<CallableItem>) {
358         /** Sort overloaded methods according to source order. */
359         SOURCE(CallableItem.sourceOrderForOverloadedMethodsComparator),
360 
361         /** Sort overloaded methods by their signature. */
362         SIGNATURE(CallableItem.comparator)
363     }
364 
365     /** Different ways of spacing out type arguments in [TypeItem.toTypeString]. */
366     enum class TypeArgumentSpacing {
367         /** No spacing added between type arguments. */
368         NONE,
369 
370         /**
371          * No spacing added between type arguments unless they are in the bounds of a type
372          * parameter.
373          */
374         LEGACY,
375 
376         /** A single space added after the comma that separates type arguments. */
377         SPACE,
378     }
379 
380     /**
381      * Get the header for the signature file that corresponds to this format.
382      *
383      * This always starts with the signature format prefix, and the version number, following by a
384      * newline and some option property assignments (e.g. `property=value`), one per line prefixed
385      * with [PROPERTY_LINE_PREFIX].
386      */
387     fun header(): String {
388         return buildString {
389             append(SIGNATURE_FORMAT_PREFIX)
390             append(version.versionNumber)
391             append("\n")
392             // Only output properties if the version supports them fully or it is migrating.
393             if (version.propertySupport == PropertySupport.FULL || migrating != null) {
394                 iterateOverCustomizableProperties { property, value ->
395                     append(PROPERTY_LINE_PREFIX)
396                     append(property)
397                     append("=")
398                     append(value)
399                     append("\n")
400                 }
401             }
402         }
403     }
404 
405     /**
406      * Get the specifier for this format.
407      *
408      * It starts with the version number followed by an optional `:` followed by at least one comma
409      * separate `property=value` pair. This is used on the command line for the `--format` option.
410      */
411     fun specifier(): String {
412         return buildString {
413             append(version.versionNumber)
414 
415             var separator = VERSION_PROPERTIES_SEPARATOR
416             iterateOverCustomizableProperties { property, value ->
417                 append(separator)
418                 separator = ","
419                 append(property)
420                 append("=")
421                 append(value)
422             }
423         }
424     }
425 
426     /**
427      * Iterate over all the properties of this format which have different values to the values in
428      * this format's [Version.defaultsIncludingLanguage], invoking the [consumer] with each
429      * property, value pair.
430      */
431     private fun iterateOverCustomizableProperties(consumer: (String, String) -> Unit) {
432         val defaults = version.defaultsIncludingLanguage(language)
433         if (this@FileFormat != defaults) {
434             CustomizableProperty.entries.forEach { prop ->
435                 // Get the string value of this property, if null then it was not specified so skip
436                 // the property.
437                 val thisValue = prop.stringFromFormat(this@FileFormat) ?: return@forEach
438                 val defaultValue = prop.stringFromFormat(defaults)
439                 if (thisValue != defaultValue) {
440                     consumer(prop.propertyName, thisValue)
441                 }
442             }
443         }
444     }
445 
446     /**
447      * Validate the format
448      *
449      * @param exceptionContext information to add to the start of the exception message that
450      *   provides context for the user.
451      * @param migratingAllowed true if the [migrating] option is allowed, false otherwise. If it is
452      *   allowed then it will also be required if [Version.propertySupport] is
453      *   [PropertySupport.FOR_MIGRATING_ONLY].
454      */
455     private fun validate(exceptionContext: String = "", migratingAllowed: Boolean) {
456         // If after applying all the properties the format matches its version defaults then
457         // there is nothing else to check.
458         if (this == version.defaults) {
459             return
460         }
461 
462         if (migratingAllowed) {
463             // If the version does not support properties (except when migrating) and the
464             // version defaults have been overridden then the `migrating` property is mandatory
465             // when migrating is allowed.
466             if (version.propertySupport != PropertySupport.FULL && migrating == null) {
467                 throw ApiParseException(
468                     "${exceptionContext}must provide a 'migrating' property when customizing version ${version.versionNumber}"
469                 )
470             }
471         } else if (migrating != null) {
472             throw ApiParseException("${exceptionContext}must not contain a 'migrating' property")
473         }
474     }
475 
476     companion object {
477         private val allDefaults = Version.entries.map { it.defaults }.toList()
478 
479         private val versionByNumber = Version.entries.associateBy { it.versionNumber }
480 
481         // The defaults associated with version 2.0.
482         val V2 = Version.V2.defaults
483 
484         // The defaults associated with version 4.0.
485         val V4 = Version.V4.defaults
486 
487         // The defaults associated with version 5.0.
488         val V5 = Version.V5.defaults
489 
490         // The defaults associated with the latest version.
491         val LATEST = allDefaults.last()
492 
493         const val SIGNATURE_FORMAT_PREFIX = "// Signature format: "
494 
495         /**
496          * The size of the buffer and read ahead limit.
497          *
498          * Should be big enough to handle any first package line, even one with lots of annotations.
499          */
500         private const val BUFFER_SIZE = 1024
501 
502         /**
503          * Parse the start of the contents provided by [reader] to obtain the [FileFormat]
504          *
505          * @param path the [Path] of the file from which the content is being read.
506          * @param reader the reader to use to read the file contents.
507          * @param formatForLegacyFiles the optional format to use if the file uses a legacy, and now
508          *   unsupported file format.
509          * @return the [FileFormat] or null if the reader was blank.
510          */
511         fun parseHeader(
512             path: Path,
513             reader: Reader,
514             formatForLegacyFiles: FileFormat? = null
515         ): FileFormat? {
516             val lineNumberReader =
517                 if (reader is LineNumberReader) reader else LineNumberReader(reader, BUFFER_SIZE)
518 
519             try {
520                 return parseHeader(lineNumberReader, formatForLegacyFiles)
521             } catch (cause: ApiParseException) {
522                 // Wrap the exception and add contextual information to help user identify and fix
523                 // the problem. This is done here instead of when throwing the exception as the
524                 // original thrower does not have that context.
525                 throw ApiParseException(
526                     "Signature format error - ${cause.message}",
527                     FileLocation.createLocation(path, lineNumberReader.lineNumber),
528                     cause,
529                 )
530             }
531         }
532 
533         /**
534          * Parse the start of the contents provided by [reader] to obtain the [FileFormat]
535          *
536          * This consumes only the content that makes up the header. So, the rest of the file
537          * contents can be read from the reader.
538          *
539          * @return the [FileFormat] or null if the reader was blank.
540          */
541         private fun parseHeader(
542             reader: LineNumberReader,
543             formatForLegacyFiles: FileFormat?
544         ): FileFormat? {
545             // Remember the starting position of the reader just in case it is necessary to reset
546             // it back to this point.
547             reader.mark(BUFFER_SIZE)
548 
549             // This reads the minimal amount to determine whether this is likely to be a
550             // signature file.
551             val prefixLength = SIGNATURE_FORMAT_PREFIX.length
552             val buffer = CharArray(prefixLength)
553             val prefix =
554                 reader.read(buffer, 0, prefixLength).let { count ->
555                     if (count == -1) {
556                         // An empty file.
557                         return null
558                     }
559                     String(buffer, 0, count)
560                 }
561 
562             if (prefix != SIGNATURE_FORMAT_PREFIX) {
563                 // If the prefix is blank then either the whole file is blank in which case it is
564                 // handled specially, or the file is not blank and is not a signature file in which
565                 // case it is an error.
566                 if (prefix.isBlank()) {
567                     var line = reader.readLine()
568                     while (line != null && line.isBlank()) {
569                         line = reader.readLine()
570                     }
571                     // If the line is null then te whole file is blank which is handled specially.
572                     if (line == null) {
573                         return null
574                     }
575                 }
576 
577                 // If formatForLegacyFiles has been provided then check to see if the file adheres
578                 // to a legacy format and if it does behave as if it was formatForLegacyFiles.
579                 if (formatForLegacyFiles != null) {
580                     // Check for version 1.0, i.e. no header at all.
581                     if (prefix.startsWith("package ")) {
582                         reader.reset()
583                         return formatForLegacyFiles
584                     }
585                 }
586 
587                 // An error occurred as the prefix did not match. A valid prefix must appear on a
588                 // single line so just in case what was read contains multiple lines trim it down to
589                 // a single line for error reporting. The LineNumberReader has translated non-unix
590                 // newline characters into `\n` so this is safe.
591                 val firstLine = prefix.substringBefore("\n")
592                 // As the error is going to be reported for the first line, even though possibly
593                 // multiple lines have been read set the line number to 1.
594                 reader.lineNumber = 1
595                 throw ApiParseException(
596                     "invalid prefix, found '$firstLine', expected '$SIGNATURE_FORMAT_PREFIX'"
597                 )
598             }
599 
600             // Read the rest of the line after the SIGNATURE_FORMAT_PREFIX which should just be the
601             // version.
602             val versionNumber = reader.readLine()
603             val version = getVersionFromNumber(versionNumber)
604 
605             val format = parseProperties(reader, version)
606             format.validate(migratingAllowed = true)
607             return format
608         }
609 
610         private const val VERSION_PROPERTIES_SEPARATOR = ":"
611 
612         /**
613          * Parse a format specifier string and create a corresponding [FileFormat].
614          *
615          * The [specifier] consists of a version, e.g. `4.0`, followed by an optional list of comma
616          * separate properties. If the properties are provided then they are separated from the
617          * version with a `:`. A property is expressed as a property assignment, e.g.
618          * `property=value`.
619          *
620          * This extracts the version and then if no properties are provided returns its defaults. If
621          * properties are provided then each property is checked to make sure that it is a valid
622          * property with a valid value and then it is applied on top of the version defaults. The
623          * result of that is returned.
624          *
625          * @param specifier the specifier string that defines a [FileFormat].
626          * @param migratingAllowed indicates whether the `migrating` property is allowed in the
627          *   specifier.
628          * @param extraVersions extra versions to add to the error message if a version is not
629          *   supported but otherwise ignored. This allows the caller to handle some additional
630          *   versions first but still report a helpful message.
631          */
632         fun parseSpecifier(
633             specifier: String,
634             migratingAllowed: Boolean = false,
635             extraVersions: Set<String> = emptySet(),
636         ): FileFormat {
637             val specifierParts = specifier.split(VERSION_PROPERTIES_SEPARATOR, limit = 2)
638             val versionNumber = specifierParts[0]
639             val version = getVersionFromNumber(versionNumber, extraVersions)
640             val versionDefaults = version.defaults
641             if (specifierParts.size == 1) {
642                 return versionDefaults
643             }
644 
645             val properties = specifierParts[1]
646 
647             val builder = Builder(versionDefaults)
648             properties.trim().split(",").forEach { parsePropertyAssignment(builder, it) }
649             val format = builder.build()
650 
651             format.validate(
652                 exceptionContext = "invalid format specifier: '$specifier' - ",
653                 migratingAllowed = migratingAllowed,
654             )
655 
656             return format
657         }
658 
659         /**
660          * Get the [Version] from the number.
661          *
662          * @param versionNumber the version number as a string.
663          * @param extraVersions extra versions to add to the error message if a version is not
664          *   supported but otherwise ignored. This allows the caller to handle some additional
665          *   versions first but still report a helpful message.
666          */
667         private fun getVersionFromNumber(
668             versionNumber: String,
669             extraVersions: Set<String> = emptySet(),
670         ): Version =
671             versionByNumber[versionNumber]
672                 ?: let {
673                     val allVersions = versionByNumber.keys + extraVersions
674                     val possibilities = allVersions.joinToString { "'$it'" }
675                     throw ApiParseException(
676                         "invalid version, found '$versionNumber', expected one of $possibilities"
677                     )
678                 }
679 
680         /**
681          * Parse a property assignment of the form `property=value`, updating the appropriate
682          * property in [builder], or throwing an exception if there was a problem.
683          *
684          * @param builder the [Builder] into which the property's value will be added.
685          * @param assignment the string of the form `property=value`.
686          * @param propertyFilter optional filter that determines the set of allowable properties;
687          *   defaults to all properties.
688          */
689         private fun parsePropertyAssignment(
690             builder: Builder,
691             assignment: String,
692             propertyFilter: (CustomizableProperty) -> Boolean = { true },
693         ) {
694             val propertyParts = assignment.split("=")
695             if (propertyParts.size != 2) {
696                 throw ApiParseException("expected <property>=<value> but found '$assignment'")
697             }
698             val name = propertyParts[0]
699             val value = propertyParts[1]
700             val customizable = CustomizableProperty.getByName(name, propertyFilter)
701             customizable.setFromString(builder, value)
702         }
703 
704         private const val PROPERTY_LINE_PREFIX = "// - "
705 
706         /**
707          * Parse property pairs, one per line, each of which must be prefixed with
708          * [PROPERTY_LINE_PREFIX], apply them to the supplied [version]s
709          * [Version.defaultsIncludingLanguage] and returning the result.
710          */
711         private fun parseProperties(reader: LineNumberReader, version: Version): FileFormat {
712             val builder = Builder(version.defaults)
713             do {
714                 reader.mark(BUFFER_SIZE)
715                 val line = reader.readLine() ?: break
716                 if (line.startsWith("package ")) {
717                     reader.reset()
718                     break
719                 }
720 
721                 // If the line does not start with "// - " then it is not a property so assume the
722                 // header is ended.
723                 val remainder = line.removePrefix(PROPERTY_LINE_PREFIX)
724                 if (remainder == line) {
725                     reader.reset()
726                     break
727                 }
728 
729                 parsePropertyAssignment(builder, remainder)
730             } while (true)
731 
732             return builder.build()
733         }
734 
735         /**
736          * Parse the supplied set of defaults and construct a [FileFormat].
737          *
738          * @param defaults comma separated list of property assignments that
739          */
740         fun parseDefaults(defaults: String): FileFormat {
741             val builder = Builder(V2)
742             defaults.trim().split(",").forEach {
743                 parsePropertyAssignment(
744                     builder,
745                     it,
746                     { it.defaultable },
747                 )
748             }
749             return builder.build()
750         }
751 
752         /**
753          * Get the names of the [CustomizableProperty] that are [CustomizableProperty.defaultable].
754          */
755         fun defaultableProperties(): List<String> {
756             return CustomizableProperty.entries
757                 .filter { it.defaultable }
758                 .map { it.propertyName }
759                 .sorted()
760                 .toList()
761         }
762     }
763 
764     /** A builder for [FileFormat] that applies some optional values to a base [FileFormat]. */
765     internal class Builder(private val base: FileFormat) {
766         var addAdditionalOverrides: Boolean? = null
767         var includeDefaultParameterValues: Boolean? = null
768         var includeTypeUseAnnotations: Boolean? = null
769         var kotlinNameTypeOrder: Boolean? = null
770         var kotlinStyleNulls: Boolean? = null
771         var language: Language? = null
772         var migrating: String? = null
773         var name: String? = null
774         var normalizeFinalModifier: Boolean? = null
775         var overloadedMethodOrder: OverloadedMethodOrder? = null
776         var sortWholeExtendsList: Boolean? = null
777         var stripJavaLangPrefix: StripJavaLangPrefix? = null
778         var typeArgumentSpacing: TypeArgumentSpacing? = null
779         var surface: String? = null
780 
781         fun build(): FileFormat {
782             // Apply any language defaults first as they take priority over version defaults.
783             language?.applyLanguageDefaults(this)
784             return base.copy(
785                 includeDefaultParameterValues = includeDefaultParameterValues
786                         ?: base.includeDefaultParameterValues,
787                 includeTypeUseAnnotations = includeTypeUseAnnotations
788                         ?: base.includeTypeUseAnnotations,
789                 kotlinNameTypeOrder = kotlinNameTypeOrder ?: base.kotlinNameTypeOrder,
790                 kotlinStyleNulls = kotlinStyleNulls ?: base.kotlinStyleNulls,
791                 language = language ?: base.language,
792                 migrating = migrating ?: base.migrating,
793                 name = name ?: base.name,
794                 specifiedAddAdditionalOverrides = addAdditionalOverrides
795                         ?: base.specifiedAddAdditionalOverrides,
796                 specifiedNormalizeFinalModifier = normalizeFinalModifier
797                         ?: base.specifiedNormalizeFinalModifier,
798                 specifiedOverloadedMethodOrder = overloadedMethodOrder
799                         ?: base.specifiedOverloadedMethodOrder,
800                 specifiedSortWholeExtendsList = sortWholeExtendsList
801                         ?: base.specifiedSortWholeExtendsList,
802                 specifiedStripJavaLangPrefix = stripJavaLangPrefix
803                         ?: base.specifiedStripJavaLangPrefix,
804                 specifiedTypeArgumentSpacing = typeArgumentSpacing
805                         ?: base.specifiedTypeArgumentSpacing,
806                 surface = surface ?: base.surface,
807             )
808         }
809     }
810 
811     /** Information about the different customizable properties in [FileFormat]. */
812     enum class CustomizableProperty(
813         val defaultable: Boolean = false,
814         /** Syntax of command line values. */
815         val valueSyntax: String = "",
816         /** Help text to use on the command line. */
817         val help: String = "",
818     ) {
819         // The order of values in this is significant as it determines the order of the properties
820         // in signature headers. The values in this block are not in alphabetical order because it
821         // is important that they are at the start of the signature header.
822 
823         NAME {
824             override fun setFromString(builder: Builder, value: String) {
825                 builder.name = value
826             }
827 
828             override fun stringFromFormat(format: FileFormat): String? = format.name
829         },
830         SURFACE {
831             override fun setFromString(builder: Builder, value: String) {
832                 builder.surface = value
833             }
834 
835             override fun stringFromFormat(format: FileFormat): String? = format.surface
836         },
837 
838         /** language=[java|kotlin] */
839         LANGUAGE {
840             override fun setFromString(builder: Builder, value: String) {
841                 builder.language = enumFromString<Language>(value)
842             }
843 
844             override fun stringFromFormat(format: FileFormat): String? =
845                 format.language?.stringFromEnum()
846         },
847 
848         // The following values must be in alphabetical order.
849 
850         /** add-additional-overrides=[yes|no] */
851         ADD_ADDITIONAL_OVERRIDES(defaultable = true) {
852             override fun setFromString(builder: Builder, value: String) {
853                 builder.addAdditionalOverrides = yesNo(value)
854             }
855 
856             override fun stringFromFormat(format: FileFormat): String? =
857                 format.specifiedAddAdditionalOverrides?.let { yesNo(it) }
858         },
859         /** include-default-parameter-values=[yes|no] */
860         INCLUDE_DEFAULT_PARAMETER_VALUES(
861             valueSyntax = "yes|no",
862             help =
863                 """
864                     If `no` then the signature file will not include any information about default
865                     parameter values. If `yes` then it will use the pseudo modifier `optional` to
866                     indicate a parameter that has a default value.
867                 """,
868         ) {
869             override fun setFromString(builder: Builder, value: String) {
870                 builder.includeDefaultParameterValues = yesNo(value)
871             }
872 
873             override fun stringFromFormat(format: FileFormat): String =
874                 yesNo(format.includeDefaultParameterValues)
875         },
876         /** include-type-use-annotations=[yes|no] */
877         INCLUDE_TYPE_USE_ANNOTATIONS {
878             override fun setFromString(builder: Builder, value: String) {
879                 builder.includeTypeUseAnnotations = yesNo(value)
880             }
881 
882             override fun stringFromFormat(format: FileFormat): String =
883                 yesNo(format.includeTypeUseAnnotations)
884         },
885         /** kotlin-name-type-order=[yes|no] */
886         KOTLIN_NAME_TYPE_ORDER {
887             override fun setFromString(builder: Builder, value: String) {
888                 builder.kotlinNameTypeOrder = yesNo(value)
889             }
890 
891             override fun stringFromFormat(format: FileFormat): String =
892                 yesNo(format.kotlinNameTypeOrder)
893         },
894         /** kotlin-style-nulls=[yes|no] */
895         KOTLIN_STYLE_NULLS(
896             valueSyntax = "yes|no",
897             help =
898                 """
899                     If `no` then the signature file will use `@Nullable` and `@NonNull` annotations
900                     to indicate that the annotated item accepts `null` and does not accept `null`
901                     respectively and neither indicates that it's not defined.
902 
903                     If `yes` then the signature file will use a type suffix of `?`, no type suffix
904                     and a type suffix of `!` to indicate the that the type accepts `null`, does not
905                     accept `null` or it's not defined respectively.
906                 """,
907         ) {
908             override fun setFromString(builder: Builder, value: String) {
909                 builder.kotlinStyleNulls = yesNo(value)
910             }
911 
912             override fun stringFromFormat(format: FileFormat): String =
913                 yesNo(format.kotlinStyleNulls)
914         },
915         MIGRATING {
916             override fun setFromString(builder: Builder, value: String) {
917                 builder.migrating = value
918             }
919 
920             override fun stringFromFormat(format: FileFormat): String? = format.migrating
921         },
922         NORMALIZE_FINAL_MODIFIER(
923             defaultable = true,
924             valueSyntax = "yes|no",
925             help =
926                 """
927                     Specifies how the `final` modifier is handled on `final` methods. If this is
928                     `yes` and the method's containing class is `final` then the `final` modifier is
929                     not written out, otherwise it is.
930                 """,
931         ) {
932             override fun setFromString(builder: Builder, value: String) {
933                 builder.normalizeFinalModifier = yesNo(value)
934             }
935 
936             override fun stringFromFormat(format: FileFormat): String? =
937                 format.specifiedNormalizeFinalModifier?.let { yesNo(it) }
938         },
939         /** overloaded-method-other=[source|signature] */
940         OVERLOADED_METHOD_ORDER(
941             defaultable = true,
942             valueSyntax = "source|signature",
943             help =
944                 """
945                     Specifies the order of overloaded methods in signature files. Applies to the
946                     contents of the files specified on `--api` and `--removed-api`.
947 
948                     `source` - preserves the order in which overloaded methods appear in the source
949                     files. This means that refactorings of the source files which change the order
950                     but not the API can cause unnecessary changes in the API signature files.
951 
952                     `signature` (default) - sorts overloaded methods by their signature. This means
953                     that refactorings of the source files which change the order but not the API
954                     will have no effect on the API signature files.
955                 """,
956         ) {
957             override fun setFromString(builder: Builder, value: String) {
958                 builder.overloadedMethodOrder = enumFromString<OverloadedMethodOrder>(value)
959             }
960 
961             override fun stringFromFormat(format: FileFormat): String? =
962                 format.specifiedOverloadedMethodOrder?.stringFromEnum()
963         },
964         SORT_WHOLE_EXTENDS_LIST(defaultable = true) {
965             override fun setFromString(builder: Builder, value: String) {
966                 builder.sortWholeExtendsList = yesNo(value)
967             }
968 
969             override fun stringFromFormat(format: FileFormat): String? =
970                 format.specifiedSortWholeExtendsList?.let { yesNo(it) }
971         },
972         STRIP_JAVA_LANG_PREFIX(defaultable = true) {
973             override fun setFromString(builder: Builder, value: String) {
974                 builder.stripJavaLangPrefix = enumFromString<StripJavaLangPrefix>(value)
975             }
976 
977             override fun stringFromFormat(format: FileFormat): String? =
978                 format.specifiedStripJavaLangPrefix?.stringFromEnum()
979         },
980         TYPE_ARGUMENT_SPACING(
981             defaultable = true,
982             valueSyntax = "legacy|none|space",
983             help =
984                 """
985                     Specifies the spacing between the type arguments of a generic type. e.g.
986                     `Map<String, Integer>`. The default is `legacy`.
987 
988                     `legacy` - adds no spaces between type arguments except those used in the bounds
989                     of a type parameter. e.g. `Map<String,Integer>` will have no space except in
990                     `class Foo<M extends Map<String, Integer>`.
991 
992                     `none` - adds no spaces between any type arguments.
993 
994                     `space` - adds a single space between every type argument.
995 
996                     Note: This does not affect the spacing of type parameters in a type parameter
997                     list, e.g. `interface Map<K, V>`. They always have a space separator.
998                 """,
999         ) {
1000             override fun setFromString(builder: Builder, value: String) {
1001                 builder.typeArgumentSpacing = enumFromString<TypeArgumentSpacing>(value)
1002             }
1003 
1004             override fun stringFromFormat(format: FileFormat): String? =
1005                 format.specifiedTypeArgumentSpacing?.stringFromEnum()
1006         },
1007         ;
1008 
1009         /** The property name in the [parseSpecifier] input. */
1010         val propertyName: String = name.lowercase(Locale.US).replace("_", "-")
1011 
1012         /**
1013          * Set the corresponding property in the supplied [Builder] to the value corresponding to
1014          * the string representation [value].
1015          */
1016         internal abstract fun setFromString(builder: Builder, value: String)
1017 
1018         /**
1019          * Get the string representation of the corresponding property from the supplied
1020          * [FileFormat].
1021          */
1022         internal abstract fun stringFromFormat(format: FileFormat): String?
1023 
1024         /** Inline function to map from a string value to an enum value of the required type. */
1025         inline fun <reified T : Enum<T>> enumFromString(value: String): T {
1026             val enumValues = enumValues<T>()
1027             return nonInlineEnumFromString(enumValues, value)
1028         }
1029 
1030         /**
1031          * Non-inline portion of the function to map from a string value to an enum value of the
1032          * required type.
1033          */
1034         fun <T : Enum<T>> nonInlineEnumFromString(enumValues: Array<T>, value: String): T {
1035             return enumValues.firstOrNull { it.stringFromEnum() == value }
1036                 ?: let {
1037                     val possibilities = enumValues.possibilitiesList { "'${it.stringFromEnum()}'" }
1038                     throw ApiParseException(
1039                         "unexpected value for $propertyName, found '$value', expected one of $possibilities"
1040                     )
1041                 }
1042         }
1043 
1044         /**
1045          * Extension function to convert an enum value to an external string.
1046          *
1047          * It simply returns the lowercase version of the enum name with `_` replaced with `-`.
1048          */
1049         fun <T : Enum<T>> T.stringFromEnum(): String {
1050             return name.lowercase(Locale.US).replace("_", "-")
1051         }
1052 
1053         /**
1054          * Intermediate enum used to map from string to [Boolean]
1055          *
1056          * The instances are not used directly but are used via [YesNo.values].
1057          */
1058         enum class YesNo(val b: Boolean) {
1059             @Suppress("UNUSED") YES(true),
1060             @Suppress("UNUSED") NO(false)
1061         }
1062 
1063         /** Convert a "yes|no" string into a boolean. */
1064         fun yesNo(value: String): Boolean {
1065             return enumFromString<YesNo>(value).b
1066         }
1067 
1068         /** Convert a boolean into a `yes|no` string. */
1069         fun yesNo(value: Boolean): String = if (value) "yes" else "no"
1070 
1071         companion object {
1072             val byPropertyName = entries.associateBy { it.propertyName }
1073 
1074             /**
1075              * Get the [CustomizableProperty] by name, throwing an [ApiParseException] if it could
1076              * not be found.
1077              *
1078              * @param name the name of the property.
1079              * @param propertyFilter optional filter that determines the set of allowable
1080              *   properties.
1081              */
1082             fun getByName(
1083                 name: String,
1084                 propertyFilter: (CustomizableProperty) -> Boolean,
1085             ): CustomizableProperty =
1086                 byPropertyName[name]?.let { if (propertyFilter(it)) it else null }
1087                     ?: let {
1088                         val possibilities =
1089                             byPropertyName
1090                                 .filter { (_, property) -> propertyFilter(property) }
1091                                 .keys
1092                                 .sorted()
1093                                 .joinToString("', '")
1094                         throw ApiParseException(
1095                             "unknown format property name `$name`, expected one of '$possibilities'"
1096                         )
1097                     }
1098         }
1099     }
1100 }
1101 
1102 /**
1103  * Given an array of items return a list of possibilities.
1104  *
1105  * The last pair of items are separated by " or ", the other pairs are separated by ", ".
1106  */
possibilitiesListnull1107 fun <T> Array<T>.possibilitiesList(transform: (T) -> String): String {
1108     val allButLast = dropLast(1)
1109     val last = last()
1110     val options = buildString {
1111         allButLast.joinTo(this, transform = transform)
1112         append(" or ")
1113         append(transform(last))
1114     }
1115     return options
1116 }
1117