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