• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2017 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
18 
19 import com.android.SdkConstants
20 import com.android.SdkConstants.FN_FRAMEWORK_LIBRARY
21 import com.android.tools.lint.detector.api.isJdkFolder
22 import com.android.tools.metalava.cli.common.CommonOptions
23 import com.android.tools.metalava.cli.common.DefaultSignatureFileLoader
24 import com.android.tools.metalava.cli.common.ExecutionEnvironment
25 import com.android.tools.metalava.cli.common.IssueReportingOptions
26 import com.android.tools.metalava.cli.common.PreviouslyReleasedApi
27 import com.android.tools.metalava.cli.common.SourceOptions
28 import com.android.tools.metalava.cli.common.Terminal
29 import com.android.tools.metalava.cli.common.TerminalColor
30 import com.android.tools.metalava.cli.common.Verbosity
31 import com.android.tools.metalava.cli.common.cliError
32 import com.android.tools.metalava.cli.common.enumOption
33 import com.android.tools.metalava.cli.common.existingFile
34 import com.android.tools.metalava.cli.common.fileForPathInner
35 import com.android.tools.metalava.cli.common.map
36 import com.android.tools.metalava.cli.common.stringToExistingDir
37 import com.android.tools.metalava.cli.common.stringToExistingFile
38 import com.android.tools.metalava.cli.common.stringToNewDir
39 import com.android.tools.metalava.cli.common.stringToNewFile
40 import com.android.tools.metalava.cli.compatibility.CompatibilityCheckOptions
41 import com.android.tools.metalava.cli.compatibility.CompatibilityCheckOptions.CheckRequest
42 import com.android.tools.metalava.cli.lint.ApiLintOptions
43 import com.android.tools.metalava.cli.signature.SignatureFormatOptions
44 import com.android.tools.metalava.doc.ApiVersionFilter
45 import com.android.tools.metalava.doc.ApiVersionLabelProvider
46 import com.android.tools.metalava.manifest.Manifest
47 import com.android.tools.metalava.manifest.emptyManifest
48 import com.android.tools.metalava.model.AnnotationManager
49 import com.android.tools.metalava.model.Codebase
50 import com.android.tools.metalava.model.Item
51 import com.android.tools.metalava.model.PackageFilter
52 import com.android.tools.metalava.model.PackageItem
53 import com.android.tools.metalava.model.TypedefMode
54 import com.android.tools.metalava.model.annotation.DefaultAnnotationManager
55 import com.android.tools.metalava.model.source.DEFAULT_JAVA_LANGUAGE_LEVEL
56 import com.android.tools.metalava.model.source.DEFAULT_KOTLIN_LANGUAGE_LEVEL
57 import com.android.tools.metalava.model.text.ApiClassResolution
58 import com.android.tools.metalava.model.text.EmitFileHeader
59 import com.android.tools.metalava.model.visitors.ApiPredicate
60 import com.android.tools.metalava.reporter.Baseline
61 import com.android.tools.metalava.reporter.DefaultReporter
62 import com.android.tools.metalava.reporter.IssueConfiguration
63 import com.android.tools.metalava.reporter.Issues
64 import com.android.tools.metalava.reporter.Reportable
65 import com.android.tools.metalava.reporter.Reporter
66 import com.android.tools.metalava.stub.StubWriterConfig
67 import com.android.utils.SdkUtils.wrap
68 import com.github.ajalt.clikt.core.NoSuchOption
69 import com.github.ajalt.clikt.parameters.groups.OptionGroup
70 import com.github.ajalt.clikt.parameters.options.default
71 import com.github.ajalt.clikt.parameters.options.deprecated
72 import com.github.ajalt.clikt.parameters.options.multiple
73 import com.github.ajalt.clikt.parameters.options.option
74 import com.github.ajalt.clikt.parameters.options.unique
75 import com.github.ajalt.clikt.parameters.types.choice
76 import com.github.ajalt.clikt.parameters.types.file
77 import java.io.File
78 import java.io.PrintWriter
79 import java.io.StringWriter
80 import java.util.Optional
81 import java.util.function.Predicate
82 import kotlin.properties.ReadWriteProperty
83 import kotlin.reflect.KProperty
84 import org.jetbrains.jps.model.java.impl.JavaSdkUtil
85 
86 /**
87  * A [ReadWriteProperty] that is used as the delegate for [options].
88  *
89  * It provides read/write methods and also a [disallowAccess] method which when called will cause
90  * any attempt to read the [options] property to fail. This allows code to ensure that any code
91  * which it calls does not access the deprecated [options] property.
92  */
93 object OptionsDelegate : ReadWriteProperty<Nothing?, Options> {
94 
95     /**
96      * The value of this delegate.
97      *
98      * Is `null` if [setValue] has not been called since the last call to [disallowAccess]. In that
99      * case any attempt to read the value of this delegate will fail.
100      */
101     private var possiblyNullOptions: Options? = Options()
102 
103     /**
104      * The stack trace of the last caller to [disallowAccess] (if any) to make it easy to determine
105      * why a read of [options] failed.
106      */
107     private var disallowerStackTrace: Throwable? = null
108 
109     /** Prevent all future reads of [options] until the [setValue] method is next called. */
110     fun disallowAccess() {
111         disallowerStackTrace = UnexpectedOptionsAccess("Global options property cleared")
112         possiblyNullOptions = null
113     }
114 
115     override fun setValue(thisRef: Nothing?, property: KProperty<*>, value: Options) {
116         disallowerStackTrace = null
117         possiblyNullOptions = value
118     }
119 
120     override fun getValue(thisRef: Nothing?, property: KProperty<*>): Options {
121         return possiblyNullOptions
122             ?: throw UnexpectedOptionsAccess("options is not set", disallowerStackTrace!!)
123     }
124 }
125 
126 /** A private class to try and avoid it being caught and ignored. */
127 private class UnexpectedOptionsAccess(message: String, cause: Throwable? = null) :
128     RuntimeException(message, cause)
129 
130 /**
131  * Global options for the metadata extraction tool
132  *
133  * This is an empty options which is created to avoid having a nullable options. It is replaced with
134  * the actual options to use, either created from the command line arguments for the main process or
135  * with arguments supplied by tests.
136  */
137 @Deprecated(
138     """
139     Do not add any more usages of this and please remove any existing uses that you find. Global
140     variables tightly couple all the code that uses them making them hard to test, modularize and
141     reuse. Which is why there is an ongoing process to remove usages of global variables and
142     eventually the global variable itself.
143     """
144 )
145 var options by OptionsDelegate
146 
147 private const val INDENT_WIDTH = 45
148 
149 const val ARG_CLASS_PATH = "--classpath"
150 const val ARG_SOURCE_FILES = "--source-files"
151 const val ARG_API_CLASS_RESOLUTION = "--api-class-resolution"
152 const val ARG_SDK_VALUES = "--sdk-values"
153 const val ARG_MERGE_QUALIFIER_ANNOTATIONS = "--merge-qualifier-annotations"
154 const val ARG_MERGE_INCLUSION_ANNOTATIONS = "--merge-inclusion-annotations"
155 const val ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS = "--validate-nullability-from-merged-stubs"
156 const val ARG_VALIDATE_NULLABILITY_FROM_LIST = "--validate-nullability-from-list"
157 const val ARG_NULLABILITY_WARNINGS_TXT = "--nullability-warnings-txt"
158 const val ARG_NULLABILITY_ERRORS_NON_FATAL = "--nullability-errors-non-fatal"
159 const val ARG_DOC_STUBS = "--doc-stubs"
160 /** Used by Firebase, see b/116185431#comment15, not used by Android Platform or AndroidX */
161 const val ARG_PROGUARD = "--proguard"
162 const val ARG_EXTRACT_ANNOTATIONS = "--extract-annotations"
163 const val ARG_EXCLUDE_DOCUMENTATION_FROM_STUBS = "--exclude-documentation-from-stubs"
164 const val ARG_ENHANCE_DOCUMENTATION = "--enhance-documentation"
165 const val ARG_SKIP_READING_COMMENTS = "--ignore-comments"
166 const val ARG_MANIFEST = "--manifest"
167 const val ARG_MIGRATE_NULLNESS = "--migrate-nullness"
168 const val ARG_SUPPRESS_COMPATIBILITY_META_ANNOTATION = "--suppress-compatibility-meta-annotation"
169 const val ARG_APPLY_API_LEVELS = "--apply-api-levels"
170 const val ARG_JAVA_SOURCE = "--java-source"
171 const val ARG_KOTLIN_SOURCE = "--kotlin-source"
172 const val ARG_SDK_HOME = "--sdk-home"
173 const val ARG_JDK_HOME = "--jdk-home"
174 const val ARG_COMPILE_SDK_VERSION = "--compile-sdk-version"
175 const val ARG_INCLUDE_SOURCE_RETENTION = "--include-source-retention"
176 const val ARG_PASS_THROUGH_ANNOTATION = "--pass-through-annotation"
177 const val ARG_EXCLUDE_ANNOTATION = "--exclude-annotation"
178 const val ARG_DELETE_EMPTY_REMOVED_SIGNATURES = "--delete-empty-removed-signatures"
179 const val ARG_SUBTRACT_API = "--subtract-api"
180 const val ARG_TYPEDEFS_IN_SIGNATURES = "--typedefs-in-signatures"
181 const val ARG_IGNORE_CLASSES_ON_CLASSPATH = "--ignore-classes-on-classpath"
182 const val ARG_USE_K2_UAST = "--Xuse-k2-uast"
183 const val ARG_PROJECT = "--project"
184 const val ARG_SOURCE_MODEL_PROVIDER = "--source-model-provider"
185 
186 class Options(
187     private val executionEnvironment: ExecutionEnvironment = ExecutionEnvironment(),
188     private val commonOptions: CommonOptions = CommonOptions(),
189     private val sourceOptions: SourceOptions = SourceOptions(),
190     private val issueReportingOptions: IssueReportingOptions =
191         IssueReportingOptions(commonOptions = commonOptions),
192     private val generalReportingOptions: GeneralReportingOptions = GeneralReportingOptions(),
193     internal val configFileOptions: ConfigFileOptions = ConfigFileOptions(),
194     val apiSelectionOptions: ApiSelectionOptions = ApiSelectionOptions(),
195     val apiLintOptions: ApiLintOptions = ApiLintOptions(),
196     private val compatibilityCheckOptions: CompatibilityCheckOptions = CompatibilityCheckOptions(),
197     signatureFileOptions: SignatureFileOptions = SignatureFileOptions(),
198     signatureFormatOptions: SignatureFormatOptions = SignatureFormatOptions(),
199     stubGenerationOptions: StubGenerationOptions = StubGenerationOptions(),
200     internal val apiLevelsGenerationOptions: ApiLevelsGenerationOptions =
201         ApiLevelsGenerationOptions(),
202 ) : OptionGroup() {
203     /** Writer to direct output to. */
204     val stdout: PrintWriter
205         get() = executionEnvironment.stdout
206     /** Writer to direct error messages to. */
207     val stderr: PrintWriter
208         get() = executionEnvironment.stderr
209 
210     /** Internal list backing [sources] */
211     private val mutableSources: MutableList<File> = mutableListOf()
212     /** Internal list backing [classpath] */
213     private val mutableClassPath: MutableList<File> = mutableListOf()
214     /** Internal list backing [mergeQualifierAnnotations] */
215     private val mutableMergeQualifierAnnotations: MutableList<File> = mutableListOf()
216     /** Internal list backing [mergeInclusionAnnotations] */
217     private val mutableMergeInclusionAnnotations: MutableList<File> = mutableListOf()
218     /** Internal list backing [passThroughAnnotations] */
219     private val mutablePassThroughAnnotations: MutableSet<String> = mutableSetOf()
220     /** Internal list backing [excludeAnnotations] */
221     private val mutableExcludeAnnotations: MutableSet<String> = mutableSetOf()
222 
223     /** API to subtract from signature and stub generation. Corresponds to [ARG_SUBTRACT_API]. */
224     var subtractApi: File? = null
225 
226     /**
227      * Backing property for [nullabilityAnnotationsValidator]
228      *
229      * This uses [Optional] to wrap the value as [lazy] cannot handle nullable values as it uses
230      * `null` as a special value.
231      *
232      * Creates [NullabilityAnnotationsValidator] lazily as it depends on a number of different
233      * options which may be supplied in different orders.
234      */
<lambda>null235     private val optionalNullabilityAnnotationsValidator by lazy {
236         Optional.ofNullable(
237             if (validateNullabilityFromMergedStubs || validateNullabilityFromList != null) {
238                 NullabilityAnnotationsValidator(
239                     reporter,
240                     nullabilityErrorsFatal,
241                     nullabilityWarningsTxt
242                 )
243             } else null
244         )
245     }
246 
247     /** Validator for nullability annotations, if validation is enabled. */
248     val nullabilityAnnotationsValidator: NullabilityAnnotationsValidator?
249         get() = optionalNullabilityAnnotationsValidator.orElse(null)
250 
251     /** Whether nullability validation errors should be considered fatal. */
252     private var nullabilityErrorsFatal = true
253 
254     /**
255      * A file to write non-fatal nullability validation issues to. If null, all issues are treated
256      * as fatal or else logged as warnings, depending on the value of [nullabilityErrorsFatal].
257      */
258     private var nullabilityWarningsTxt: File? = null
259 
260     /**
261      * Whether to validate nullability for all the classes where we are merging annotations from
262      * external java stub files. If true, [nullabilityAnnotationsValidator] must be set.
263      */
264     var validateNullabilityFromMergedStubs = false
265 
266     /**
267      * A file containing a list of classes whose nullability annotations should be validated. If
268      * set, [nullabilityAnnotationsValidator] must also be set.
269      */
270     var validateNullabilityFromList: File? = null
271 
272     /**
273      * Whether to include element documentation (javadoc and KDoc) is in the generated stubs.
274      * (Copyright notices are not affected by this, they are always included. Documentation stubs
275      * (--doc-stubs) are not affected.)
276      */
277     private var includeDocumentationInStubs = true
278 
279     /**
280      * Enhance documentation in various ways, for example auto-generating documentation based on
281      * source annotations present in the code. This is implied by --doc-stubs.
282      */
283     var enhanceDocumentation = false
284 
285     /**
286      * Whether to allow reading comments If false, any attempts by Metalava to read a PSI comment
287      * will return "" This can help callers to be sure that comment-only changes shouldn't affect
288      * Metalava output
289      */
290     var allowReadingComments = true
291 
292     /** The list of source roots */
293     val sourcePath: List<File> by sourceOptions::sourcePath
294 
295     /** The list of dependency jars */
296     val classpath: List<File> = mutableClassPath
297 
298     /** All source files to parse */
299     var sources: List<File> = mutableSources
300 
301     /** Lint project description that describes project's module structure in details */
302     var projectDescription: File? = null
303 
304     val apiClassResolution by
305         enumOption(
306             help =
307                 """
308                 Determines how class resolution is performed when loading API signature files. Any
309                 classes that cannot be found will be treated as empty.",
310             """
311                     .trimIndent(),
<lambda>null312             enumValueHelpGetter = { it.help },
313             default = ApiClassResolution.API_CLASSPATH,
<lambda>null314             key = { it.optionValue },
315         )
316 
317     val allShowAnnotations by apiSelectionOptions::allShowAnnotations
318 
319     /**
320      * Whether to include unannotated elements if {@link #showAnnotations} is set. Note: This only
321      * applies to signature files, not stub files.
322      */
323     val showUnannotated
324         get() = apiSelectionOptions.showUnannotated
325 
326     val apiSurfaces
327         get() = apiSelectionOptions.apiSurfaces
328 
329     /** Packages to include in the API (if null, include all) */
330     val apiPackages: PackageFilter? by sourceOptions::apiPackages
331 
332     /**
333      * An optional [Reportable] predicate that will ignore issues from (i.e. return false for)
334      * [Item]s that do not match the [apiPackages] filter. If no [apiPackages] filter is provided
335      * then this will be `null`.
336      */
337     private val reportableFilter: Predicate<Reportable>? by
<lambda>null338         lazy(LazyThreadSafetyMode.NONE) {
339             apiPackages?.let { packageFilter ->
340                 Predicate { reportable ->
341                     // If we are only emitting some packages (--stub-packages), don't report
342                     // issues from other packages
343                     (reportable as? Item)?.let { item ->
344                         val pkg = (item as? PackageItem) ?: item.containingPackage()
345                         pkg == null || packageFilter.matches(pkg)
346                     }
347                         ?: true
348                 }
349             }
350         }
351 
352     /** Packages that we should skip generating even if not hidden; typically only used by tests */
353     val skipEmitPackages
354         get() = executionEnvironment.testEnvironment?.skipEmitPackages ?: emptyList()
355 
<lambda>null356     private val annotationManager: AnnotationManager by lazy {
357         DefaultAnnotationManager(
358             DefaultAnnotationManager.Config(
359                 passThroughAnnotations = passThroughAnnotations,
360                 allShowAnnotations = allShowAnnotations,
361                 showAnnotations = apiSelectionOptions.showAnnotations,
362                 showSingleAnnotations = apiSelectionOptions.showSingleAnnotations,
363                 showForStubPurposesAnnotations = apiSelectionOptions.showForStubPurposesAnnotations,
364                 hideAnnotations = apiSelectionOptions.hideAnnotations,
365                 suppressCompatibilityMetaAnnotations = suppressCompatibilityMetaAnnotations,
366                 excludeAnnotations = excludeAnnotations,
367                 typedefMode = typedefMode,
368                 apiPredicate = ApiPredicate(config = apiPredicateConfig),
369                 previouslyReleasedCodebaseProvider = { previouslyReleasedCodebase },
370                 apiFlags = ApiFlagsCreator.createFromConfig(configFileOptions.config.apiFlags),
371             )
372         )
373     }
374 
375     /** Make this available for testing purposes. */
376     internal val previouslyReleasedCodebase
377         get() = compatibilityCheckOptions.previouslyReleasedCodebase(signatureFileCache)
378 
379     internal val codebaseConfig by
<lambda>null380         lazy(LazyThreadSafetyMode.NONE) {
381             Codebase.Config(
382                 annotationManager = annotationManager,
383                 apiSurfaces = apiSurfaces,
384                 reporter = reporter,
385             )
386         }
387 
388     internal val signatureFileLoader by
<lambda>null389         lazy(LazyThreadSafetyMode.NONE) { DefaultSignatureFileLoader(codebaseConfig) }
390 
391     internal val signatureFileCache by
<lambda>null392         lazy(LazyThreadSafetyMode.NONE) { SignatureFileCache(signatureFileLoader) }
393 
394     /** Meta-annotations for which annotated APIs should not be checked for compatibility. */
395     private val suppressCompatibilityMetaAnnotations by
396         option(
397                 ARG_SUPPRESS_COMPATIBILITY_META_ANNOTATION,
398                 help =
399                     """
400                        Suppress compatibility checks for any elements within the scope of an
401                        annotation which is itself annotated with the given meta-annotation.
402                     """
403                         .trimIndent(),
404                 metavar = "<meta-annotation class>",
405             )
406             .multiple()
407             .unique()
408 
409     /**
410      * Whether the generated API can contain classes that are not present in the source but are
411      * present on the classpath. Defaults to true for backwards compatibility but is set to false if
412      * any API signatures are imported as they must provide a complete set of all classes required
413      * but not provided by the generated API.
414      *
415      * Once all APIs are either self-contained or imported all the required references this will be
416      * removed and no classes will be allowed from the classpath JARs.
417      */
418     private var allowClassesFromClasspath = true
419 
420     /** The configuration options for the [ApiAnalyzer] class. */
<lambda>null421     val apiAnalyzerConfig by lazy {
422         ApiAnalyzer.Config(
423             manifest = manifest,
424             skipEmitPackages = skipEmitPackages,
425             mergeQualifierAnnotations = mergeQualifierAnnotations,
426             mergeInclusionAnnotations = mergeInclusionAnnotations,
427             allShowAnnotations = allShowAnnotations,
428             apiPredicateConfig = apiPredicateConfig,
429         )
430     }
431 
<lambda>null432     val apiPredicateConfig by lazy {
433         ApiPredicate.Config(
434             ignoreShown = showUnannotated,
435             allowClassesFromClasspath = allowClassesFromClasspath,
436             addAdditionalOverrides = signatureFileFormat.addAdditionalOverrides,
437         )
438     }
439 
440     /** This is set directly by [preprocessArgv]. */
441     private var verbosity: Verbosity = Verbosity.NORMAL
442 
443     /** Whether to report warnings and other diagnostics along the way */
444     val quiet: Boolean
445         get() = verbosity.quiet
446 
447     /**
448      * Whether to report extra diagnostics along the way (note that verbose isn't the same as not
449      * quiet)
450      */
451     val verbose: Boolean
452         get() = verbosity.verbose
453 
<lambda>null454     internal val stubWriterConfig by lazy {
455         StubWriterConfig(
456             includeDocumentationInStubs = includeDocumentationInStubs,
457         )
458     }
459 
460     val stubsDir by stubGenerationOptions::stubsDir
461     val forceConvertToWarningNullabilityAnnotations by
462         stubGenerationOptions::forceConvertToWarningNullabilityAnnotations
463     val generateAnnotations by stubGenerationOptions::includeAnnotations
464 
465     /**
466      * If set, a directory to write documentation stub files to. Corresponds to the --stubs/-stubs
467      * flag.
468      */
469     var docStubsDir: File? = null
470 
471     /** Proguard Keep list file to write */
472     var proguard: File? = null
473 
474     val apiFile by signatureFileOptions::apiFile
475     val removedApiFile by signatureFileOptions::removedApiFile
476     val signatureFileFormat by signatureFormatOptions::fileFormat
477 
478     /** Path to directory to write SDK values to */
479     var sdkValueDir: File? = null
480 
481     /**
482      * If set, a file to write extracted annotations to. Corresponds to the --extract-annotations
483      * flag.
484      */
485     var externalAnnotations: File? = null
486 
487     /** An optional manifest [File]. */
488     private val manifestFile by
489         option(
490                 ARG_MANIFEST,
491                 help =
492                     """
493         A manifest file, used to check permissions to cross check APIs and retrieve min_sdk_version.
494         (default: no manifest)
495                     """
496                         .trimIndent()
497             )
498             .file(mustExist = true, canBeDir = false, mustBeReadable = true)
499 
500     /**
501      * A [Manifest] object to look up available permissions and min_sdk_version.
502      *
503      * Created lazily to make sure that the [reporter] has been initialized.
504      */
<lambda>null505     val manifest by lazy { manifestFile?.let { Manifest(it, reporter) } ?: emptyManifest }
506 
507     /** The set of annotation classes that should be passed through unchanged */
508     private var passThroughAnnotations = mutablePassThroughAnnotations
509 
510     /** The set of annotation classes that should be removed from all outputs */
511     private var excludeAnnotations = mutableExcludeAnnotations
512 
513     /** A signature file to migrate nullness data from */
514     val migrateNullsFrom by
515         option(
516                 ARG_MIGRATE_NULLNESS,
517                 metavar = "<api file>",
518                 help =
519                     """
520                         Compare nullness information with the previous stable API
521                         and mark newly annotated APIs as under migration.
522                     """
523                         .trimIndent()
524             )
525             .existingFile()
526             .multiple()
<lambda>null527             .map {
528                 PreviouslyReleasedApi.optionalPreviouslyReleasedApi(
529                     ARG_MIGRATE_NULLNESS,
530                     it,
531                     onlyUseLastForMainApiSurface = false
532                 )
533             }
534 
535     /** The list of compatibility checks to run */
536     val compatibilityChecks: List<CheckRequest> by compatibilityCheckOptions::compatibilityChecks
537 
538     /** The set of annotation classes that should be treated as API compatibility important */
539     val apiCompatAnnotations by compatibilityCheckOptions::apiCompatAnnotations
540 
541     /** Existing external annotation files to merge in */
542     private var mergeQualifierAnnotations: List<File> = mutableMergeQualifierAnnotations
543     private var mergeInclusionAnnotations: List<File> = mutableMergeInclusionAnnotations
544 
545     val apiVersionLabelProvider: ApiVersionLabelProvider =
546         apiLevelsGenerationOptions::getApiVersionLabel
547 
548     val includeApiLevelInDocumentation: ApiVersionFilter =
549         apiLevelsGenerationOptions::includeApiVersionInDocumentation
550 
551     /** Reads API XML file to apply into documentation */
552     var applyApiLevelsXml: File? = null
553 
554     /** Whether to include the signature file format version header in removed signature files */
555     val includeSignatureFormatVersionRemoved: EmitFileHeader
556         get() =
557             if (deleteEmptyRemovedSignatures) {
558                 EmitFileHeader.IF_NONEMPTY_FILE
559             } else {
560                 EmitFileHeader.ALWAYS
561             }
562 
563     var allBaselines: List<Baseline> = emptyList()
564 
565     /** [IssueConfiguration] used by all reporters. */
566     val issueConfiguration by issueReportingOptions::issueConfiguration
567 
568     /** [Reporter] that will redirect [Issues.Issue] depending on their [Issues.Category]. */
569     lateinit var reporter: Reporter
570         private set
571 
572     internal var allReporters: List<DefaultReporter> = emptyList()
573 
574     /** If generating a removed signature file, and it is empty, delete it */
575     var deleteEmptyRemovedSignatures = false
576 
577     /** The language level to use for Java files, set with [ARG_JAVA_SOURCE] */
578     var javaLanguageLevelAsString: String = DEFAULT_JAVA_LANGUAGE_LEVEL
579 
580     /** The language level to use for Kotlin files, set with [ARG_KOTLIN_SOURCE] */
581     var kotlinLanguageLevelAsString: String = DEFAULT_KOTLIN_LANGUAGE_LEVEL
582 
583     /**
584      * The JDK to use as a platform, if set with [ARG_JDK_HOME]. This is only set when metalava is
585      * used for non-Android projects.
586      */
587     var jdkHome: File? = null
588 
589     /**
590      * The JDK to use as a platform, if set with [ARG_SDK_HOME]. If this is set along with
591      * [ARG_COMPILE_SDK_VERSION], metalava will automatically add the platform's android.jar file to
592      * the classpath if it does not already find the android.jar file in the classpath.
593      */
594     private var sdkHome: File? = null
595 
596     /**
597      * The compileSdkVersion, set by [ARG_COMPILE_SDK_VERSION]. For example, for R it would be "29".
598      * For R preview, it would be "R".
599      */
600     private var compileSdkVersion: String? = null
601 
602     /**
603      * How to handle typedef annotations in signature files; corresponds to
604      * $ARG_TYPEDEFS_IN_SIGNATURES
605      */
606     private val typedefMode by
607         enumOption(
608             ARG_TYPEDEFS_IN_SIGNATURES,
609             help = """Whether to include typedef annotations in signature files.""",
<lambda>null610             enumValueHelpGetter = { it.help },
611             default = TypedefMode.NONE,
<lambda>null612             key = { it.optionValue },
613         )
614 
615     /** Temporary folder to use instead of the JDK default, if any */
616     private var tempFolder: File? = null
617 
618     var useK2Uast: Boolean? = null
619 
620     val sourceModelProvider by
621         option(
622                 ARG_SOURCE_MODEL_PROVIDER,
623                 hidden = true,
624             )
625             .choice("psi", "turbine")
626             .default("psi")
627             .deprecated(
628                 """WARNING: The turbine model is under work and not usable for now. Eventually this option can be used to set the source model provider to either turbine or psi. The default is psi. """
629                     .trimIndent()
630             )
631 
parsenull632     fun parse(args: Array<String>) {
633         var index = 0
634         while (index < args.size) {
635             when (val arg = args[index]) {
636                 // For now, we don't distinguish between bootclasspath and classpath
637                 ARG_CLASS_PATH -> {
638                     val path = getValue(args, ++index)
639                     mutableClassPath.addAll(stringToExistingDirsOrJars(path))
640                 }
641                 ARG_SOURCE_FILES -> {
642                     val listString = getValue(args, ++index)
643                     listString.split(",").forEach { path ->
644                         mutableSources.addAll(stringToExistingFiles(path))
645                     }
646                 }
647                 ARG_SUBTRACT_API -> {
648                     if (subtractApi != null) {
649                         cliError("Only one $ARG_SUBTRACT_API can be supplied")
650                     }
651                     subtractApi = stringToExistingFile(getValue(args, ++index))
652                 }
653 
654                 // TODO: Remove the legacy --merge-annotations flag once it's no longer used to
655                 // update P docs
656                 ARG_MERGE_QUALIFIER_ANNOTATIONS,
657                 "--merge-zips",
658                 "--merge-annotations" ->
659                     mutableMergeQualifierAnnotations.addAll(
660                         stringToExistingDirsOrFiles(getValue(args, ++index))
661                     )
662                 ARG_MERGE_INCLUSION_ANNOTATIONS ->
663                     mutableMergeInclusionAnnotations.addAll(
664                         stringToExistingDirsOrFiles(getValue(args, ++index))
665                     )
666                 ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS -> {
667                     validateNullabilityFromMergedStubs = true
668                 }
669                 ARG_VALIDATE_NULLABILITY_FROM_LIST -> {
670                     validateNullabilityFromList = stringToExistingFile(getValue(args, ++index))
671                 }
672                 ARG_NULLABILITY_WARNINGS_TXT ->
673                     nullabilityWarningsTxt = stringToNewFile(getValue(args, ++index))
674                 ARG_NULLABILITY_ERRORS_NON_FATAL -> nullabilityErrorsFatal = false
675                 ARG_SDK_VALUES -> sdkValueDir = stringToNewDir(getValue(args, ++index))
676                 ARG_DOC_STUBS -> docStubsDir = stringToNewDir(getValue(args, ++index))
677                 ARG_EXCLUDE_DOCUMENTATION_FROM_STUBS -> includeDocumentationInStubs = false
678                 ARG_ENHANCE_DOCUMENTATION -> enhanceDocumentation = true
679                 ARG_SKIP_READING_COMMENTS -> allowReadingComments = false
680                 ARG_PASS_THROUGH_ANNOTATION -> {
681                     val annotations = getValue(args, ++index)
682                     annotations.split(",").forEach { path ->
683                         mutablePassThroughAnnotations.add(path)
684                     }
685                 }
686                 ARG_EXCLUDE_ANNOTATION -> {
687                     val annotations = getValue(args, ++index)
688                     annotations.split(",").forEach { path -> mutableExcludeAnnotations.add(path) }
689                 }
690                 ARG_PROGUARD -> proguard = stringToNewFile(getValue(args, ++index))
691                 ARG_IGNORE_CLASSES_ON_CLASSPATH -> {
692                     allowClassesFromClasspath = false
693                 }
694                 ARG_DELETE_EMPTY_REMOVED_SIGNATURES -> deleteEmptyRemovedSignatures = true
695                 ARG_EXTRACT_ANNOTATIONS ->
696                     externalAnnotations = stringToNewFile(getValue(args, ++index))
697 
698                 // Extracting API levels
699                 ARG_APPLY_API_LEVELS -> {
700                     applyApiLevelsXml =
701                         if (apiLevelsGenerationOptions.generateApiLevelXml != null) {
702                             // If generating the API file at the same time, it doesn't have
703                             // to already exist
704                             stringToNewFile(getValue(args, ++index))
705                         } else {
706                             stringToExistingFile(getValue(args, ++index))
707                         }
708                 }
709                 ARG_JAVA_SOURCE -> {
710                     val value = getValue(args, ++index)
711                     javaLanguageLevelAsString = value
712                 }
713                 ARG_KOTLIN_SOURCE -> {
714                     val value = getValue(args, ++index)
715                     kotlinLanguageLevelAsString = value
716                 }
717                 ARG_JDK_HOME -> {
718                     jdkHome = stringToExistingDir(getValue(args, ++index))
719                 }
720                 ARG_SDK_HOME -> {
721                     sdkHome = stringToExistingDir(getValue(args, ++index))
722                 }
723                 ARG_COMPILE_SDK_VERSION -> {
724                     compileSdkVersion = getValue(args, ++index)
725                 }
726                 ARG_USE_K2_UAST -> useK2Uast = true
727                 ARG_PROJECT -> {
728                     projectDescription = stringToExistingFile(getValue(args, ++index))
729                 }
730                 "--temp-folder" -> {
731                     tempFolder = stringToNewOrExistingDir(getValue(args, ++index))
732                 }
733 
734                 // Option only meant for tests (not documented); doesn't work in all cases (to do
735                 // that we'd
736                 // need JNA to call libc)
737                 "--pwd" -> {
738                     val pwd = stringToExistingDir(getValue(args, ++index)).absoluteFile
739                     System.setProperty("user.dir", pwd.path)
740                 }
741                 else -> {
742                     if (arg.startsWith("-")) {
743                         // Some other argument: display usage info and exit
744                         throw NoSuchOption(givenName = arg)
745                     } else {
746                         // All args that don't start with "-" are taken to be filenames
747                         mutableSources.addAll(stringToExistingFiles(arg))
748                     }
749                 }
750             }
751 
752             ++index
753         }
754 
755         // Initialize the reporters.
756         val baseline = generalReportingOptions.baseline
757         val reporterUnknown =
758             createReporter(
759                 executionEnvironment = executionEnvironment,
760                 baseline = baseline,
761                 errorMessage = null,
762             )
763 
764         val reporterApiLint =
765             createReporter(
766                 executionEnvironment = executionEnvironment,
767                 baseline = apiLintOptions.baseline ?: baseline,
768                 errorMessage = apiLintOptions.errorMessage,
769             )
770 
771         // [Reporter] for "check-compatibility:*:released".
772         // i.e.
773         //      [ARG_CHECK_COMPATIBILITY_API_RELEASED] and
774         //      [ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED].
775         val reporterCompatibilityReleased =
776             createReporter(
777                 executionEnvironment = executionEnvironment,
778                 baseline = compatibilityCheckOptions.baseline ?: baseline,
779                 errorMessage = compatibilityCheckOptions.errorMessage,
780             )
781 
782         // A Reporter that will redirect issues to the appropriate reporter based on the issue's
783         // Category.
784         reporter =
785             CategoryRedirectingReporter(
786                 defaultReporter = reporterUnknown,
787                 apiLintReporter = reporterApiLint,
788                 compatibilityReporter = reporterCompatibilityReleased,
789             )
790 
791         // Build "all baselines" and "all reporters"
792 
793         // Baselines are nullable, so selectively add to the list.
794         allBaselines =
795             listOfNotNull(baseline, apiLintOptions.baseline, compatibilityCheckOptions.baseline)
796 
797         // Reporters are non-null.
798         allReporters =
799             listOf(
800                 reporterUnknown,
801                 reporterApiLint,
802                 reporterCompatibilityReleased,
803             )
804 
805         updateClassPath()
806 
807         // Make sure that any config files are processed.
808         configFileOptions.config
809     }
810 
811     /**
812      * Create a [Reporter] that checks for known issues in [baseline] and prints [errorMessage], if
813      * provided, when errors have been reported.
814      */
createReporternull815     private fun createReporter(
816         executionEnvironment: ExecutionEnvironment,
817         baseline: Baseline?,
818         errorMessage: String?,
819     ) =
820         DefaultReporter(
821             environment = executionEnvironment.reporterEnvironment,
822             issueConfiguration = issueConfiguration,
823             baseline = baseline,
824             errorMessage = errorMessage,
825             reportableFilter = reportableFilter,
826             config = issueReportingOptions.reporterConfig,
827         )
828 
829     /** Update the classpath to insert android.jar or JDK classpath elements if necessary */
830     private fun updateClassPath() {
831         val sdkHome = sdkHome
832         val jdkHome = jdkHome
833 
834         if (
835             sdkHome != null &&
836                 compileSdkVersion != null &&
837                 classpath.none { it.name == FN_FRAMEWORK_LIBRARY }
838         ) {
839             val jar = File(sdkHome, "platforms/android-$compileSdkVersion")
840             if (jar.isFile) {
841                 mutableClassPath.add(jar)
842             } else {
843                 cliError(
844                     "Could not find android.jar for API level $compileSdkVersion in SDK $sdkHome: $jar does not exist"
845                 )
846             }
847             if (jdkHome != null) {
848                 cliError("Do not specify both $ARG_SDK_HOME and $ARG_JDK_HOME")
849             }
850         } else if (jdkHome != null) {
851             val isJre = !isJdkFolder(jdkHome)
852             val roots = JavaSdkUtil.getJdkClassesRoots(jdkHome.toPath(), isJre).map { it.toFile() }
853             mutableClassPath.addAll(roots)
854         }
855     }
856 
getValuenull857     private fun getValue(args: Array<String>, index: Int): String {
858         if (index >= args.size) {
859             cliError("Missing argument for ${args[index - 1]}")
860         }
861         return args[index]
862     }
863 
stringToExistingDirsOrJarsnull864     private fun stringToExistingDirsOrJars(value: String): List<File> {
865         val files = mutableListOf<File>()
866         for (path in value.split(File.pathSeparatorChar)) {
867             val file = fileForPathInner(path)
868             if (!file.isDirectory && !(file.path.endsWith(SdkConstants.DOT_JAR) && file.isFile)) {
869                 cliError("$file is not a jar or directory")
870             }
871             files.add(file)
872         }
873         return files
874     }
875 
stringToExistingDirsOrFilesnull876     private fun stringToExistingDirsOrFiles(value: String): List<File> {
877         val files = mutableListOf<File>()
878         for (path in value.split(File.pathSeparatorChar)) {
879             val file = fileForPathInner(path)
880             if (!file.exists()) {
881                 cliError("$file does not exist")
882             }
883             files.add(file)
884         }
885         return files
886     }
887 
888     @Suppress("unused")
stringToExistingFileOrDirnull889     private fun stringToExistingFileOrDir(value: String): File {
890         val file = fileForPathInner(value)
891         if (!file.exists()) {
892             cliError("$file is not a file or directory")
893         }
894         return file
895     }
896 
stringToExistingFilesnull897     private fun stringToExistingFiles(value: String): List<File> {
898         return value
899             .split(File.pathSeparatorChar)
900             .map { fileForPathInner(it) }
901             .map { file ->
902                 if (!file.isFile) {
903                     cliError("$file is not a file")
904                 }
905                 file
906             }
907     }
908 
stringToNewOrExistingDirnull909     private fun stringToNewOrExistingDir(value: String): File {
910         val dir = fileForPathInner(value)
911         if (!dir.isDirectory) {
912             val ok = dir.mkdirs()
913             if (!ok) {
914                 cliError("Could not create $dir")
915             }
916         }
917         return dir
918     }
919 }
920 
921 object OptionsHelp {
getUsagenull922     fun getUsage(terminal: Terminal, width: Int): String {
923         val usage = StringWriter()
924         val printWriter = PrintWriter(usage)
925         usage(printWriter, terminal, width)
926         return usage.toString()
927     }
928 
usagenull929     private fun usage(out: PrintWriter, terminal: Terminal, width: Int) {
930         val args =
931             arrayOf(
932                 "",
933                 "API sources:",
934                 "$ARG_SOURCE_FILES <files>",
935                 "A comma separated list of source files to be parsed. Can also be " +
936                     "@ followed by a path to a text file containing paths to the full set of files to parse.",
937                 "$ARG_CLASS_PATH <paths>",
938                 "One or more directories or jars (separated by " +
939                     "`${File.pathSeparator}`) containing classes that should be on the classpath when parsing the " +
940                     "source files",
941                 "$ARG_PROJECT <xmlfile>",
942                 "Project description written in XML according to Lint's project model.",
943                 "$ARG_MERGE_QUALIFIER_ANNOTATIONS <file>",
944                 "An external annotations file to merge and overlay " +
945                     "the sources, or a directory of such files. Should be used for annotations intended for " +
946                     "inclusion in the API to be written out, e.g. nullability. Formats supported are: IntelliJ's " +
947                     "external annotations database format, .jar or .zip files containing those, Android signature " +
948                     "files, and Java stub files.",
949                 "$ARG_MERGE_INCLUSION_ANNOTATIONS <file>",
950                 "An external annotations file to merge and overlay " +
951                     "the sources, or a directory of such files. Should be used for annotations which determine " +
952                     "inclusion in the API to be written out, i.e. show and hide. The only format supported is " +
953                     "Java stub files.",
954                 ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS,
955                 "Triggers validation of nullability annotations " +
956                     "for any class where $ARG_MERGE_QUALIFIER_ANNOTATIONS includes a Java stub file.",
957                 ARG_VALIDATE_NULLABILITY_FROM_LIST,
958                 "Triggers validation of nullability annotations " +
959                     "for any class listed in the named file (one top-level class per line, # prefix for comment line).",
960                 "$ARG_NULLABILITY_WARNINGS_TXT <file>",
961                 "Specifies where to write warnings encountered during " +
962                     "validation of nullability annotations. (Does not trigger validation by itself.)",
963                 ARG_NULLABILITY_ERRORS_NON_FATAL,
964                 "Specifies that errors encountered during validation of " +
965                     "nullability annotations should not be treated as errors. They will be written out to the " +
966                     "file specified in $ARG_NULLABILITY_WARNINGS_TXT instead.",
967                 "$ARG_JAVA_SOURCE <level>",
968                 "Sets the source level for Java source files; default is $DEFAULT_JAVA_LANGUAGE_LEVEL.",
969                 "$ARG_KOTLIN_SOURCE <level>",
970                 "Sets the source level for Kotlin source files; default is $DEFAULT_KOTLIN_LANGUAGE_LEVEL.",
971                 "$ARG_SDK_HOME <dir>",
972                 "If set, locate the `android.jar` file from the given Android SDK",
973                 "$ARG_COMPILE_SDK_VERSION <api>",
974                 "Use the given API level",
975                 "$ARG_JDK_HOME <dir>",
976                 "If set, add the Java APIs from the given JDK to the classpath",
977                 "$ARG_SUBTRACT_API <api file>",
978                 "Subtracts the API in the given signature or jar file from the " +
979                     "current API being emitted via $ARG_API, $ARG_STUBS, $ARG_DOC_STUBS, etc. " +
980                     "Note that the subtraction only applies to classes; it does not subtract members.",
981                 ARG_IGNORE_CLASSES_ON_CLASSPATH,
982                 "Prevents references to classes on the classpath from being added to " +
983                     "the generated stub files.",
984                 ARG_SKIP_READING_COMMENTS,
985                 "Ignore any comments in source files.",
986                 "",
987                 "Extracting Signature Files:",
988                 // TODO: Document --show-annotation!
989                 "$ARG_PROGUARD <file>",
990                 "Write a ProGuard keep file for the API",
991                 "$ARG_SDK_VALUES <dir>",
992                 "Write SDK values files to the given directory",
993                 "",
994                 "Generating Stubs:",
995                 "$ARG_DOC_STUBS <dir>",
996                 "Generate documentation stub source files for the API. Documentation stub " +
997                     "files are similar to regular stub files, but there are some differences. For example, in " +
998                     "the stub files, we'll use special annotations like @RecentlyNonNull instead of @NonNull to " +
999                     "indicate that an element is recently marked as non null, whereas in the documentation stubs we'll " +
1000                     "just list this as @NonNull. Another difference is that @doconly elements are included in " +
1001                     "documentation stubs, but not regular stubs, etc.",
1002                 "$ARG_PASS_THROUGH_ANNOTATION <annotation classes>",
1003                 "A comma separated list of fully qualified names of " +
1004                     "annotation classes that must be passed through unchanged.",
1005                 "$ARG_EXCLUDE_ANNOTATION <annotation classes>",
1006                 "A comma separated list of fully qualified names of " +
1007                     "annotation classes that must be stripped from metalava's outputs.",
1008                 ARG_ENHANCE_DOCUMENTATION,
1009                 "Enhance documentation in various ways, for example auto-generating documentation based on source " +
1010                     "annotations present in the code. This is implied by --doc-stubs.",
1011                 ARG_EXCLUDE_DOCUMENTATION_FROM_STUBS,
1012                 "Exclude element documentation (javadoc and kdoc) " +
1013                     "from the generated stubs. (Copyright notices are not affected by this, they are always included. " +
1014                     "Documentation stubs (--doc-stubs) are not affected.)",
1015                 "",
1016                 "Extracting Annotations:",
1017                 "$ARG_EXTRACT_ANNOTATIONS <zipfile>",
1018                 "Extracts source annotations from the source files and writes " +
1019                     "them into the given zip file",
1020                 ARG_INCLUDE_SOURCE_RETENTION,
1021                 "If true, include source-retention annotations in the stub files. Does " +
1022                     "not apply to signature files. Source retention annotations are extracted into the external " +
1023                     "annotations files instead.",
1024                 "",
1025                 "Injecting API Levels:",
1026                 "$ARG_APPLY_API_LEVELS <api-versions.xml>",
1027                 "Reads an XML file containing API level descriptions " +
1028                     "and merges the information into the documentation",
1029                 "",
1030                 "Environment Variables:",
1031                 ENV_VAR_METALAVA_DUMP_ARGV,
1032                 "Set to true to have metalava emit all the arguments it was invoked with. " +
1033                     "Helpful when debugging or reproducing under a debugger what the build system is doing.",
1034                 ENV_VAR_METALAVA_PREPEND_ARGS,
1035                 "One or more arguments (concatenated by space) to insert into the " +
1036                     "command line, before the documentation flags.",
1037                 ENV_VAR_METALAVA_APPEND_ARGS,
1038                 "One or more arguments (concatenated by space) to append to the " +
1039                     "end of the command line, after the generate documentation flags."
1040             )
1041 
1042         val indent = " ".repeat(INDENT_WIDTH)
1043 
1044         var i = 0
1045         while (i < args.size) {
1046             val arg = args[i]
1047             if (arg.isEmpty()) {
1048                 val groupTitle = args[i + 1]
1049                 out.println("\n")
1050                 out.println(terminal.colorize(groupTitle, TerminalColor.YELLOW))
1051             } else {
1052                 val description = "\n" + args[i + 1]
1053                 val formattedArg = terminal.bold(arg)
1054                 val invisibleChars = formattedArg.length - arg.length
1055                 // +invisibleChars: the extra chars in the above are counted but don't
1056                 // contribute to width so allow more space
1057                 val formatString = "%1$-" + (INDENT_WIDTH + invisibleChars) + "s%2\$s"
1058 
1059                 val output =
1060                     wrap(
1061                         String.format(formatString, formattedArg, description),
1062                         width + invisibleChars,
1063                         width,
1064                         indent
1065                     )
1066 
1067                 // Remove trailing whitespace
1068                 val lines = output.lines()
1069                 lines.forEachIndexed { index, line ->
1070                     out.print(line.trimEnd())
1071                     if (index < lines.size - 1) {
1072                         out.println()
1073                     }
1074                 }
1075             }
1076             i += 2
1077         }
1078     }
1079 }
1080