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