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