blob: 8c91b184837927bb2437d8887b3f4c96931b5321 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava
import com.android.SdkConstants
import com.android.SdkConstants.FN_FRAMEWORK_LIBRARY
import com.android.tools.lint.detector.api.isJdkFolder
import com.android.tools.metalava.CompatibilityCheck.CheckRequest
import com.android.tools.metalava.model.defaultConfiguration
import com.android.utils.SdkUtils.wrap
import com.google.common.base.CharMatcher
import com.google.common.base.Splitter
import com.google.common.io.Files
import com.intellij.pom.java.LanguageLevel
import org.jetbrains.jps.model.java.impl.JavaSdkUtil
import org.jetbrains.kotlin.config.ApiVersion
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
import java.io.File
import java.io.IOException
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.io.StringWriter
import java.util.Locale
import kotlin.text.Charsets.UTF_8
/** Global options for the metadata extraction tool */
var options = Options(emptyArray())
private const val MAX_LINE_WIDTH = 120
private const val INDENT_WIDTH = 45
const val ARG_FORMAT = "--format"
const val ARG_HELP = "--help"
const val ARG_VERSION = "--version"
const val ARG_QUIET = "--quiet"
const val ARG_VERBOSE = "--verbose"
const val ARG_CLASS_PATH = "--classpath"
const val ARG_SOURCE_PATH = "--source-path"
const val ARG_SOURCE_FILES = "--source-files"
const val ARG_API = "--api"
const val ARG_XML_API = "--api-xml"
const val ARG_CONVERT_TO_JDIFF = "--convert-to-jdiff"
const val ARG_CONVERT_NEW_TO_JDIFF = "--convert-new-to-jdiff"
const val ARG_CONVERT_TO_V1 = "--convert-to-v1"
const val ARG_CONVERT_TO_V2 = "--convert-to-v2"
const val ARG_CONVERT_NEW_TO_V1 = "--convert-new-to-v1"
const val ARG_CONVERT_NEW_TO_V2 = "--convert-new-to-v2"
const val ARG_DEX_API = "--dex-api"
const val ARG_SDK_VALUES = "--sdk-values"
const val ARG_REMOVED_API = "--removed-api"
const val ARG_MERGE_QUALIFIER_ANNOTATIONS = "--merge-qualifier-annotations"
const val ARG_MERGE_INCLUSION_ANNOTATIONS = "--merge-inclusion-annotations"
const val ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS = "--validate-nullability-from-merged-stubs"
const val ARG_VALIDATE_NULLABILITY_FROM_LIST = "--validate-nullability-from-list"
const val ARG_NULLABILITY_WARNINGS_TXT = "--nullability-warnings-txt"
const val ARG_NULLABILITY_ERRORS_NON_FATAL = "--nullability-errors-non-fatal"
const val ARG_INPUT_API_JAR = "--input-api-jar"
const val ARG_STUBS = "--stubs"
const val ARG_DOC_STUBS = "--doc-stubs"
const val ARG_KOTLIN_STUBS = "--kotlin-stubs"
const val ARG_STUBS_SOURCE_LIST = "--write-stubs-source-list"
const val ARG_DOC_STUBS_SOURCE_LIST = "--write-doc-stubs-source-list"
/**
* Used by Firebase, see b/116185431#comment15, not used by Android Platform or AndroidX
*/
const val ARG_PROGUARD = "--proguard"
const val ARG_EXTRACT_ANNOTATIONS = "--extract-annotations"
const val ARG_EXCLUDE_ALL_ANNOTATIONS = "--exclude-all-annotations"
const val ARG_EXCLUDE_DOCUMENTATION_FROM_STUBS = "--exclude-documentation-from-stubs"
const val ARG_ENHANCE_DOCUMENTATION = "--enhance-documentation"
const val ARG_HIDE_PACKAGE = "--hide-package"
const val ARG_MANIFEST = "--manifest"
const val ARG_MIGRATE_NULLNESS = "--migrate-nullness"
const val ARG_CHECK_COMPATIBILITY = "--check-compatibility"
const val ARG_CHECK_COMPATIBILITY_API_CURRENT = "--check-compatibility:api:current"
const val ARG_CHECK_COMPATIBILITY_API_RELEASED = "--check-compatibility:api:released"
const val ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT = "--check-compatibility:removed:current"
const val ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED = "--check-compatibility:removed:released"
const val ARG_CHECK_COMPATIBILITY_BASE_API = "--check-compatibility:base"
const val ARG_ALLOW_COMPATIBLE_DIFFERENCES = "--allow-compatible-differences"
const val ARG_NO_NATIVE_DIFF = "--no-native-diff"
const val ARG_INPUT_KOTLIN_NULLS = "--input-kotlin-nulls"
const val ARG_OUTPUT_KOTLIN_NULLS = "--output-kotlin-nulls"
const val ARG_OUTPUT_DEFAULT_VALUES = "--output-default-values"
const val ARG_WARNINGS_AS_ERRORS = "--warnings-as-errors"
const val ARG_LINTS_AS_ERRORS = "--lints-as-errors"
const val ARG_SHOW_ANNOTATION = "--show-annotation"
const val ARG_SHOW_SINGLE_ANNOTATION = "--show-single-annotation"
const val ARG_HIDE_ANNOTATION = "--hide-annotation"
const val ARG_HIDE_META_ANNOTATION = "--hide-meta-annotation"
const val ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION = "--show-for-stub-purposes-annotation"
const val ARG_SHOW_UNANNOTATED = "--show-unannotated"
const val ARG_COLOR = "--color"
const val ARG_NO_COLOR = "--no-color"
const val ARG_NO_BANNER = "--no-banner"
const val ARG_ERROR = "--error"
const val ARG_WARNING = "--warning"
const val ARG_LINT = "--lint"
const val ARG_HIDE = "--hide"
const val ARG_APPLY_API_LEVELS = "--apply-api-levels"
const val ARG_GENERATE_API_LEVELS = "--generate-api-levels"
const val ARG_ANDROID_JAR_PATTERN = "--android-jar-pattern"
const val ARG_CURRENT_VERSION = "--current-version"
const val ARG_FIRST_VERSION = "--first-version"
const val ARG_CURRENT_CODENAME = "--current-codename"
const val ARG_CURRENT_JAR = "--current-jar"
const val ARG_API_LINT = "--api-lint"
const val ARG_API_LINT_IGNORE_PREFIX = "--api-lint-ignore-prefix"
const val ARG_PUBLIC = "--public"
const val ARG_PROTECTED = "--protected"
const val ARG_PACKAGE = "--package"
const val ARG_PRIVATE = "--private"
const val ARG_HIDDEN = "--hidden"
const val ARG_JAVA_SOURCE = "--java-source"
const val ARG_KOTLIN_SOURCE = "--kotlin-source"
const val ARG_SDK_HOME = "--sdk-home"
const val ARG_JDK_HOME = "--jdk-home"
const val ARG_COMPILE_SDK_VERSION = "--compile-sdk-version"
const val ARG_REGISTER_ARTIFACT = "--register-artifact"
const val ARG_INCLUDE_ANNOTATIONS = "--include-annotations"
const val ARG_COPY_ANNOTATIONS = "--copy-annotations"
const val ARG_INCLUDE_ANNOTATION_CLASSES = "--include-annotation-classes"
const val ARG_REWRITE_ANNOTATIONS = "--rewrite-annotations"
const val ARG_INCLUDE_SOURCE_RETENTION = "--include-source-retention"
const val ARG_PASS_THROUGH_ANNOTATION = "--pass-through-annotation"
const val ARG_EXCLUDE_ANNOTATION = "--exclude-annotation"
const val ARG_INCLUDE_SIG_VERSION = "--include-signature-version"
const val ARG_UPDATE_API = "--only-update-api"
const val ARG_CHECK_API = "--only-check-api"
const val ARG_PASS_BASELINE_UPDATES = "--pass-baseline-updates"
const val ARG_REPLACE_DOCUMENTATION = "--replace-documentation"
const val ARG_BASELINE = "--baseline"
const val ARG_BASELINE_API_LINT = "--baseline:api-lint"
const val ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED = "--baseline:compatibility:released"
const val ARG_REPORT_EVEN_IF_SUPPRESSED = "--report-even-if-suppressed"
const val ARG_UPDATE_BASELINE = "--update-baseline"
const val ARG_UPDATE_BASELINE_API_LINT = "--update-baseline:api-lint"
const val ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED = "--update-baseline:compatibility:released"
const val ARG_MERGE_BASELINE = "--merge-baseline"
const val ARG_STUB_PACKAGES = "--stub-packages"
const val ARG_STUB_IMPORT_PACKAGES = "--stub-import-packages"
const val ARG_DELETE_EMPTY_BASELINES = "--delete-empty-baselines"
const val ARG_DELETE_EMPTY_REMOVED_SIGNATURES = "--delete-empty-removed-signatures"
const val ARG_SUBTRACT_API = "--subtract-api"
const val ARG_TYPEDEFS_IN_SIGNATURES = "--typedefs-in-signatures"
const val ARG_FORCE_CONVERT_TO_WARNING_NULLABILITY_ANNOTATIONS = "--force-convert-to-warning-nullability-annotations"
const val ARG_IGNORE_CLASSES_ON_CLASSPATH = "--ignore-classes-on-classpath"
const val ARG_ERROR_MESSAGE_API_LINT = "--error-message:api-lint"
const val ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED = "--error-message:compatibility:released"
const val ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_CURRENT = "--error-message:compatibility:current"
const val ARG_NO_IMPLICIT_ROOT = "--no-implicit-root"
const val ARG_STRICT_INPUT_FILES = "--strict-input-files"
const val ARG_STRICT_INPUT_FILES_STACK = "--strict-input-files:stack"
const val ARG_STRICT_INPUT_FILES_WARN = "--strict-input-files:warn"
const val ARG_STRICT_INPUT_FILES_EXEMPT = "--strict-input-files-exempt"
const val ARG_REPEAT_ERRORS_MAX = "--repeat-errors-max"
const val ARG_ENABLE_KOTLIN_PSI = "--enable-kotlin-psi"
class Options(
private val args: Array<String>,
/** Writer to direct output to */
var stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)),
/** Writer to direct error messages to */
var stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err))
) {
/** Internal list backing [sources] */
private val mutableSources: MutableList<File> = mutableListOf()
/** Internal list backing [sourcePath] */
private val mutableSourcePath: MutableList<File> = mutableListOf()
/** Internal list backing [classpath] */
private val mutableClassPath: MutableList<File> = mutableListOf()
/** Internal list backing [showAnnotations] */
private val mutableShowAnnotations = MutableAnnotationFilter()
/** Internal list backing [showSingleAnnotations] */
private val mutableShowSingleAnnotations = MutableAnnotationFilter()
/** Internal list backing [hideAnnotations] */
private val mutableHideAnnotations = MutableAnnotationFilter()
/** Internal list backing [hideMetaAnnotations] */
private val mutableHideMetaAnnotations: MutableList<String> = mutableListOf()
/** Internal list backing [showForStubPurposesAnnotations] */
private val mutableShowForStubPurposesAnnotation = MutableAnnotationFilter()
/** Internal list backing [stubImportPackages] */
private val mutableStubImportPackages: MutableSet<String> = mutableSetOf()
/** Internal list backing [mergeQualifierAnnotations] */
private val mutableMergeQualifierAnnotations: MutableList<File> = mutableListOf()
/** Internal list backing [mergeInclusionAnnotations] */
private val mutableMergeInclusionAnnotations: MutableList<File> = mutableListOf()
/** Internal list backing [annotationCoverageOf] */
private val mutableAnnotationCoverageOf: MutableList<File> = mutableListOf()
/** Internal list backing [hidePackages] */
private val mutableHidePackages: MutableList<String> = mutableListOf()
/** Internal list backing [skipEmitPackages] */
private val mutableSkipEmitPackages: MutableList<String> = mutableListOf()
/** Internal list backing [convertToXmlFiles] */
private val mutableConvertToXmlFiles: MutableList<ConvertFile> = mutableListOf()
/** Internal list backing [passThroughAnnotations] */
private val mutablePassThroughAnnotations: MutableSet<String> = mutableSetOf()
/** Internal list backing [excludeAnnotations] */
private val mutableExcludeAnnotations: MutableSet<String> = mutableSetOf()
/** Ignored flags we've already warned about - store here such that we don't keep reporting them */
private val alreadyWarned: MutableSet<String> = mutableSetOf()
/** API to subtract from signature and stub generation. Corresponds to [ARG_SUBTRACT_API]. */
var subtractApi: File? = null
/**
* Validator for nullability annotations, if validation is enabled.
*/
var nullabilityAnnotationsValidator: NullabilityAnnotationsValidator? = null
/**
* Whether nullability validation errors should be considered fatal.
*/
var nullabilityErrorsFatal = true
/**
* A file to write non-fatal nullability validation issues to. If null, all issues are treated
* as fatal or else logged as warnings, depending on the value of [nullabilityErrorsFatal].
*/
var nullabilityWarningsTxt: File? = null
/**
* Whether to validate nullability for all the classes where we are merging annotations from
* external java stub files. If true, [nullabilityAnnotationsValidator] must be set.
*/
var validateNullabilityFromMergedStubs = false
/**
* A file containing a list of classes whose nullability annotations should be validated. If
* set, [nullabilityAnnotationsValidator] must also be set.
*/
var validateNullabilityFromList: File? = null
/**
* Whether to include element documentation (javadoc and KDoc) is in the generated stubs.
* (Copyright notices are not affected by this, they are always included. Documentation stubs
* (--doc-stubs) are not affected.)
*/
var includeDocumentationInStubs = true
/**
* Enhance documentation in various ways, for example auto-generating documentation based on
* source annotations present in the code. This is implied by --doc-stubs.
*/
var enhanceDocumentation = false
/**
* Whether metalava is invoked as part of updating the API files. When this is true, metalava
* should *cancel* various other flags that are also being passed in, such as --check-compatibility.
* This is there to ease integration in the build system: for a given target, the build system will
* pass all the applicable flags (--stubs, --api, --check-compatibility, --generate-documentation, etc),
* and this integration is re-used for the update-api facility where we *only* want to generate the
* signature files. This avoids having duplicate metalava invocation logic where potentially newly
* added flags are missing in one of the invocations etc.
*/
var onlyUpdateApi = false
/**
* Whether metalava is invoked as part of running the checkapi target. When this is true, metalava
* should *cancel* various other flags that are also being passed in, such as updating signature
* files.
*
* This is there to ease integration in the build system: for a given target, the build system will
* pass all the applicable flags (--stubs, --api, --check-compatibility, --generate-documentation, etc),
* and this integration is re-used for the checkapi facility where we *only* want to run compatibility
* checks. This avoids having duplicate metalava invocation logic where potentially newly
* added flags are missing in one of the invocations etc.
*/
var onlyCheckApi = false
/** Whether nullness annotations should be displayed as ?/!/empty instead of with @NonNull/@Nullable. */
var outputKotlinStyleNulls = false // requires v3
/** Whether default values should be included in signature files */
var outputDefaultValues = true
/**
* Whether only the presence of default values should be included in signature files, and not
* the full body of the default value.
*/
var outputConciseDefaultValues = false // requires V4
/** The output format version being used */
var outputFormat: FileFormat = FileFormat.V2
/**
* Whether reading signature files should assume the input is formatted as Kotlin-style nulls
* (e.g. ? means nullable, ! means unknown, empty means not null).
*
* Even when it's false, if the format supports Kotlin-style nulls, we'll still allow them.
*/
var inputKotlinStyleNulls: Boolean = false
/** If true, treat all warnings as errors */
var warningsAreErrors: Boolean = false
/** If true, treat all API lint warnings as errors */
var lintsAreErrors: Boolean = false
/** The list of source roots */
val sourcePath: List<File> = mutableSourcePath
/** The list of dependency jars */
val classpath: List<File> = mutableClassPath
/** All source files to parse */
var sources: List<File> = mutableSources
/**
* Whether to include APIs with annotations (intended for documentation purposes).
* This includes [ARG_SHOW_ANNOTATION], [ARG_SHOW_SINGLE_ANNOTATION] and
* [ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION].
*/
var showAnnotations: AnnotationFilter = mutableShowAnnotations
/**
* Like [showAnnotations], but does not work recursively. Note that
* these annotations are *also* show annotations and will be added to the above list;
* this is a subset.
*/
val showSingleAnnotations: AnnotationFilter = mutableShowSingleAnnotations
/**
* Whether to include unannotated elements if {@link #showAnnotations} is set.
* Note: This only applies to signature files, not stub files.
*/
var showUnannotated = false
/** Whether to validate the API for best practices */
var checkApi = false
val checkApiIgnorePrefix: MutableList<String> = mutableListOf()
/** If non null, an API file to use to hide for controlling what parts of the API are new */
var checkApiBaselineApiFile: File? = null
/** Packages to include (if null, include all) */
var stubPackages: PackageFilter? = null
/** Packages to import (if empty, include all) */
var stubImportPackages: Set<String> = mutableStubImportPackages
/** Packages to exclude/hide */
var hidePackages: List<String> = mutableHidePackages
/** Packages that we should skip generating even if not hidden; typically only used by tests */
var skipEmitPackages: List<String> = mutableSkipEmitPackages
var showAnnotationOverridesVisibility: Boolean = false
/** Annotations to hide */
var hideAnnotations: AnnotationFilter = mutableHideAnnotations
/** Meta-annotations to hide */
var hideMetaAnnotations = mutableHideMetaAnnotations
/**
* Annotations that defines APIs that are implicitly included in the API surface. These APIs
* will be included in included in certain kinds of output such as stubs, but others (e.g.
* API lint and the API signature file) ignore them.
*/
var showForStubPurposesAnnotations: AnnotationFilter = mutableShowForStubPurposesAnnotation
/** Whether the generated API can contain classes that are not present in the source but are present on the
* classpath. Defaults to true for backwards compatibility but is set to false if any API signatures are imported
* as they must provide a complete set of all classes required but not provided by the generated API.
*
* Once all APIs are either self contained or imported all the required references this will be removed and no
* classes will be allowed from the classpath JARs. */
var allowClassesFromClasspath = true
/** Whether to report warnings and other diagnostics along the way */
var quiet = false
/** Whether to report extra diagnostics along the way (note that verbose isn't the same as not quiet) */
var verbose = false
/** If set, a directory to write stub files to. Corresponds to the --stubs/-stubs flag. */
var stubsDir: File? = null
/** If set, a directory to write documentation stub files to. Corresponds to the --stubs/-stubs flag. */
var docStubsDir: File? = null
/** If set, a source file to write the stub index (list of source files) to. Can be passed to
* other tools like javac/javadoc using the special @-syntax. */
var stubsSourceList: File? = null
/** If set, a source file to write the doc stub index (list of source files) to. Can be passed to
* other tools like javac/javadoc using the special @-syntax. */
var docStubsSourceList: File? = null
/** Whether code compiled from Kotlin should be emitted as .kt stubs instead of .java stubs */
var kotlinStubs = false
/** Proguard Keep list file to write */
var proguard: File? = null
/** If set, a file to write an API file to. Corresponds to the --api/-api flag. */
var apiFile: File? = null
/** Like [apiFile], but with JDiff xml format. */
var apiXmlFile: File? = null
/** If set, a file to write the DEX signatures to. Corresponds to [ARG_DEX_API]. */
var dexApiFile: File? = null
/** Path to directory to write SDK values to */
var sdkValueDir: File? = null
/** If set, a file to write extracted annotations to. Corresponds to the --extract-annotations flag. */
var externalAnnotations: File? = null
/** For [ARG_COPY_ANNOTATIONS], the source directory to read stub annotations from */
var privateAnnotationsSource: File? = null
/** For [ARG_COPY_ANNOTATIONS], the target directory to write converted stub annotations from */
var privateAnnotationsTarget: File? = null
/**
* For [ARG_INCLUDE_ANNOTATION_CLASSES], the directory to copy stub annotation source files into the
* stubs folder from
*/
var copyStubAnnotationsFrom: File? = null
/**
* For [ARG_INCLUDE_SOURCE_RETENTION], true if we want to include source-retention annotations
* both in the set of files emitted by [ARG_INCLUDE_ANNOTATION_CLASSES] and into the stubs
* themselves
*/
var includeSourceRetentionAnnotations = false
/** For [ARG_REWRITE_ANNOTATIONS], the jar or bytecode folder to rewrite annotations in */
var rewriteAnnotations: List<File>? = null
/** A manifest file to read to for example look up available permissions */
var manifest: File? = null
/** If set, a file to write a dex API file to. Corresponds to the --removed-dex-api/-removedDexApi flag. */
var removedApiFile: File? = null
/** Whether output should be colorized */
var color = System.getenv("TERM")?.startsWith("xterm") ?: System.getenv("COLORTERM") != null ?: false
/** Whether to generate annotations into the stubs */
var generateAnnotations = false
/** The set of annotation classes that should be passed through unchanged */
var passThroughAnnotations = mutablePassThroughAnnotations
/** The set of annotation classes that should be removed from all outputs */
var excludeAnnotations = mutableExcludeAnnotations
/**
* A signature file to migrate nullness data from
*/
var migrateNullsFrom: File? = null
/** Private backing list for [compatibilityChecks]] */
private val mutableCompatibilityChecks: MutableList<CheckRequest> = mutableListOf()
/** The list of compatibility checks to run */
val compatibilityChecks: List<CheckRequest> = mutableCompatibilityChecks
/** The API to use a base for the otherwise checked API during compat checks. */
var baseApiForCompatCheck: File? = null
/**
* When checking signature files, whether compatible differences in signature
* files are allowed. This is normally not allowed (since it means the next
* engineer adding an incompatible change will suddenly see the cumulative
* differences show up in their diffs when checking in signature files),
* but is useful from the test suite etc. Controlled by
* [ARG_ALLOW_COMPATIBLE_DIFFERENCES].
*/
var allowCompatibleDifferences = false
/** If false, attempt to use the native diff utility on the system */
var noNativeDiff = false
/** Existing external annotation files to merge in */
var mergeQualifierAnnotations: List<File> = mutableMergeQualifierAnnotations
var mergeInclusionAnnotations: List<File> = mutableMergeInclusionAnnotations
/**
* We modify the annotations on these APIs to ask kotlinc to treat it as only a warning
* if a caller of one of these APIs makes an incorrect assumption about its nullability.
*/
var forceConvertToWarningNullabilityAnnotations: PackageFilter? = null
/** An optional <b>jar</b> file to load classes from instead of from source.
* This is similar to the [classpath] attribute except we're explicitly saying
* that this is the complete set of classes and that we <b>should</b> generate
* signatures/stubs from them or use them to diff APIs with (whereas [classpath]
* is only used to resolve types.) */
var apiJar: File? = null
/** Whether to use the experimental KtPsi model on .kt source files instead of existing
* PSI implementation
*/
var enableKotlinPsi = false
/**
* mapping from API level to android.jar files, if computing API levels
*/
var apiLevelJars: Array<File>? = null
/** The api level of the codebase, or -1 if not known/specified */
var currentApiLevel = -1
/**
* The first api level of the codebase; typically 1 but can be
* higher for example for the System API.
*/
var firstApiLevel = 1
/** The codename of the codebase, if it's a preview, or null if not specified */
var currentCodeName: String? = null
/** API level XML file to generate */
var generateApiLevelXml: File? = null
/** Reads API XML file to apply into documentation */
var applyApiLevelsXml: File? = null
/** Level to include for javadoc */
var docLevel = DocLevel.PROTECTED
/** Whether to include the signature file format version header in most signature files */
var includeSignatureFormatVersion: Boolean = true
/** Whether to include the signature file format version header in removed signature files */
val includeSignatureFormatVersionNonRemoved: EmitFileHeader get() =
if (includeSignatureFormatVersion) {
EmitFileHeader.ALWAYS
} else {
EmitFileHeader.NEVER
}
/** Whether to include the signature file format version header in removed signature files */
val includeSignatureFormatVersionRemoved: EmitFileHeader get() =
if (includeSignatureFormatVersion) {
if (deleteEmptyRemovedSignatures) {
EmitFileHeader.IF_NONEMPTY_FILE
} else {
EmitFileHeader.ALWAYS
}
} else {
EmitFileHeader.NEVER
}
/** A baseline to check against */
var baseline: Baseline? = null
/** A baseline to check against, specifically used for "API lint" (i.e. [ARG_API_LINT]) */
var baselineApiLint: Baseline? = null
/**
* A baseline to check against, specifically used for "check-compatibility:*:released"
* (i.e. [ARG_CHECK_COMPATIBILITY_API_RELEASED] and [ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED])
*/
var baselineCompatibilityReleased: Baseline? = null
var allBaselines: List<Baseline>
/** If set, metalava will show this error message when "API lint" (i.e. [ARG_API_LINT]) fails. */
var errorMessageApiLint: String = DefaultLintErrorMessage
/**
* If set, metalava will show this error message when "check-compatibility:*:released" fails.
* (i.e. [ARG_CHECK_COMPATIBILITY_API_RELEASED] and [ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED])
*/
var errorMessageCompatibilityReleased: String? = null
/**
* If set, metalava will show this error message when "check-compatibility:*:current" fails.
* (i.e. [ARG_CHECK_COMPATIBILITY_API_CURRENT] and [ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT])
*/
var errorMessageCompatibilityCurrent: String? = null
/** [Reporter] for "api-lint" */
var reporterApiLint: Reporter
/**
* [Reporter] for "check-compatibility:*:released".
* (i.e. [ARG_CHECK_COMPATIBILITY_API_RELEASED] and [ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED])
*/
var reporterCompatibilityReleased: Reporter
/**
* [Reporter] for "check-compatibility:*:current".
* (i.e. [ARG_CHECK_COMPATIBILITY_API_CURRENT] and [ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT])
*/
var reporterCompatibilityCurrent: Reporter
var allReporters: List<Reporter>
/** If updating baselines, don't fail the build */
var passBaselineUpdates = false
/** If updating baselines and the baseline is empty, delete the file */
var deleteEmptyBaselines = false
/** If generating a removed signature file and it is empty, delete it */
var deleteEmptyRemovedSignatures = false
/** Whether the baseline should only contain errors */
var baselineErrorsOnly = false
/** Writes a list of all errors, even if they were suppressed in baseline or via annotation. */
var reportEvenIfSuppressed: File? = null
var reportEvenIfSuppressedWriter: PrintWriter? = null
/**
* DocReplacements to apply to the documentation.
*/
var docReplacements = mutableListOf<DocReplacement>()
/**
* Whether to omit locations for warnings and errors. This is not a flag exposed to users
* or listed in help; this is intended for the unit test suite, used for example for the
* test which checks compatibility between signature and API files where the paths vary.
*/
var omitLocations = false
/** Directory to write signature files to, if any. */
var androidJarSignatureFiles: File? = null
/**
* The language level to use for Java files, set with [ARG_JAVA_SOURCE]
*/
var javaLanguageLevel: LanguageLevel = LanguageLevel.JDK_1_8
/**
* The language level to use for Java files, set with [ARG_KOTLIN_SOURCE]
*/
var kotlinLanguageLevel: LanguageVersionSettings = LanguageVersionSettingsImpl.DEFAULT
/**
* The JDK to use as a platform, if set with [ARG_JDK_HOME]. This is only set
* when metalava is used for non-Android projects.
*/
var jdkHome: File? = null
/**
* The JDK to use as a platform, if set with [ARG_SDK_HOME]. If this is set
* along with [ARG_COMPILE_SDK_VERSION], metalava will automatically add
* the platform's android.jar file to the classpath if it does not already
* find the android.jar file in the classpath.
*/
var sdkHome: File? = null
/**
* The compileSdkVersion, set by [ARG_COMPILE_SDK_VERSION]. For example,
* for R it would be "29". For R preview, if would be "R".
*/
var compileSdkVersion: String? = null
/** Map from XML API descriptor file to corresponding artifact id name */
val artifactRegistrations = ArtifactTagger()
/** List of signature files to export as JDiff files */
val convertToXmlFiles: List<ConvertFile> = mutableConvertToXmlFiles
enum class TypedefMode {
NONE,
REFERENCE,
INLINE
}
/** How to handle typedef annotations in signature files; corresponds to $ARG_TYPEDEFS_IN_SIGNATURES */
var typedefMode = TypedefMode.NONE
/** Allow implicit root detection (which is the default behavior). See [ARG_NO_IMPLICIT_ROOT] */
var allowImplicitRoot = true
enum class StrictInputFileMode {
PERMISSIVE,
STRICT {
override val shouldFail = true
},
STRICT_WARN,
STRICT_WITH_STACK {
override val shouldFail = true
};
open val shouldFail = false
companion object {
fun fromArgument(arg: String): StrictInputFileMode {
return when (arg) {
ARG_STRICT_INPUT_FILES -> STRICT
ARG_STRICT_INPUT_FILES_WARN -> STRICT_WARN
ARG_STRICT_INPUT_FILES_STACK -> STRICT_WITH_STACK
else -> PERMISSIVE
}
}
}
}
/**
* Whether we should allow metalava to read files that are not explicitly specified in the
* command line. See [ARG_STRICT_INPUT_FILES], [ARG_STRICT_INPUT_FILES_WARN] and
* [ARG_STRICT_INPUT_FILES_STACK].
*/
var strictInputFiles = StrictInputFileMode.PERMISSIVE
var strictInputViolationsFile: File? = null
var strictInputViolationsPrintWriter: PrintWriter? = null
/** File conversion tasks */
data class ConvertFile(
val fromApiFile: File,
val outputFile: File,
val baseApiFile: File? = null,
val strip: Boolean = false,
val outputFormat: FileFormat = FileFormat.JDIFF
)
/** Temporary folder to use instead of the JDK default, if any */
var tempFolder: File? = null
/** When non-0, metalava repeats all the errors at the end of the run, at most this many. */
var repeatErrorsMax = 0
init {
// Pre-check whether --color/--no-color is present and use that to decide how
// to emit the banner even before we emit errors
if (args.contains(ARG_NO_COLOR)) {
color = false
} else if (args.contains(ARG_COLOR) || args.contains("-android")) {
color = true
}
// empty args: only when building initial default Options (options field
// at the top of this file; replaced once the driver runs and passes in
// a real argv. Don't print a banner when initializing the default options.)
if (args.isNotEmpty() && !args.contains(ARG_QUIET) && !args.contains(ARG_NO_BANNER) &&
!args.contains(ARG_VERSION)
) {
if (color) {
stdout.print(colorized(BANNER.trimIndent(), TerminalColor.BLUE))
} else {
stdout.println(BANNER.trimIndent())
}
stdout.println()
stdout.flush()
}
var androidJarPatterns: MutableList<String>? = null
var currentJar: File? = null
var delayedCheckApiFiles = false
var skipGenerateAnnotations = false
reporter = Reporter(null, null)
val baselineBuilder = Baseline.Builder().apply { description = "base" }
val baselineApiLintBuilder = Baseline.Builder().apply { description = "api-lint" }
val baselineCompatibilityReleasedBuilder = Baseline.Builder().apply { description = "compatibility:released" }
fun getBaselineBuilderForArg(flag: String): Baseline.Builder = when (flag) {
ARG_BASELINE, ARG_UPDATE_BASELINE, ARG_MERGE_BASELINE -> baselineBuilder
ARG_BASELINE_API_LINT, ARG_UPDATE_BASELINE_API_LINT -> baselineApiLintBuilder
ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED, ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED
-> baselineCompatibilityReleasedBuilder
else -> error("Internal error: Invalid flag: $flag")
}
var index = 0
while (index < args.size) {
when (val arg = args[index]) {
ARG_HELP, "-h", "-?" -> {
helpAndQuit(color)
}
ARG_QUIET -> {
quiet = true; verbose = false
}
ARG_VERBOSE -> {
verbose = true; quiet = false
}
ARG_VERSION -> {
throw DriverException(stdout = "$PROGRAM_NAME version: ${Version.VERSION}")
}
ARG_ENABLE_KOTLIN_PSI -> enableKotlinPsi = true
// For now we don't distinguish between bootclasspath and classpath
ARG_CLASS_PATH, "-classpath", "-bootclasspath" -> {
val path = getValue(args, ++index)
mutableClassPath.addAll(stringToExistingDirsOrJars(path))
}
ARG_SOURCE_PATH, "--sources", "--sourcepath", "-sourcepath" -> {
val path = getValue(args, ++index)
if (path.isBlank()) {
// Don't compute absolute path; we want to skip this file later on.
// For current directory one should use ".", not "".
mutableSourcePath.add(File(""))
} else {
if (path.endsWith(SdkConstants.DOT_JAVA)) {
throw DriverException(
"$arg should point to a source root directory, not a source file ($path)"
)
}
mutableSourcePath.addAll(stringToExistingDirsOrJars(path, false))
}
}
ARG_SOURCE_FILES -> {
val listString = getValue(args, ++index)
listString.split(",").forEach { path ->
mutableSources.addAll(stringToExistingFiles(path))
}
}
ARG_SUBTRACT_API -> {
if (subtractApi != null) {
throw DriverException(stderr = "Only one $ARG_SUBTRACT_API can be supplied")
}
subtractApi = stringToExistingFile(getValue(args, ++index))
}
// TODO: Remove the legacy --merge-annotations flag once it's no longer used to update P docs
ARG_MERGE_QUALIFIER_ANNOTATIONS, "--merge-zips", "--merge-annotations" -> mutableMergeQualifierAnnotations.addAll(
stringToExistingDirsOrFiles(
getValue(args, ++index)
)
)
ARG_MERGE_INCLUSION_ANNOTATIONS -> mutableMergeInclusionAnnotations.addAll(
stringToExistingDirsOrFiles(
getValue(args, ++index)
)
)
ARG_FORCE_CONVERT_TO_WARNING_NULLABILITY_ANNOTATIONS -> {
val nextArg = getValue(args, ++index)
forceConvertToWarningNullabilityAnnotations = PackageFilter.parse(nextArg)
}
ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS -> {
validateNullabilityFromMergedStubs = true
nullabilityAnnotationsValidator =
nullabilityAnnotationsValidator ?: NullabilityAnnotationsValidator()
}
ARG_VALIDATE_NULLABILITY_FROM_LIST -> {
validateNullabilityFromList = stringToExistingFile(getValue(args, ++index))
nullabilityAnnotationsValidator =
nullabilityAnnotationsValidator ?: NullabilityAnnotationsValidator()
}
ARG_NULLABILITY_WARNINGS_TXT ->
nullabilityWarningsTxt = stringToNewFile(getValue(args, ++index))
ARG_NULLABILITY_ERRORS_NON_FATAL ->
nullabilityErrorsFatal = false
"-sdkvalues", ARG_SDK_VALUES -> sdkValueDir = stringToNewDir(getValue(args, ++index))
ARG_API, "-api" -> apiFile = stringToNewFile(getValue(args, ++index))
ARG_XML_API -> apiXmlFile = stringToNewFile(getValue(args, ++index))
ARG_DEX_API, "-dexApi" -> dexApiFile = stringToNewFile(getValue(args, ++index))
ARG_REMOVED_API, "-removedApi" -> removedApiFile = stringToNewFile(getValue(args, ++index))
ARG_MANIFEST, "-manifest" -> manifest = stringToExistingFile(getValue(args, ++index))
ARG_SHOW_ANNOTATION, "-showAnnotation" -> mutableShowAnnotations.add(getValue(args, ++index))
ARG_SHOW_SINGLE_ANNOTATION -> {
val annotation = getValue(args, ++index)
mutableShowSingleAnnotations.add(annotation)
// These should also be counted as show annotations
mutableShowAnnotations.add(annotation)
}
ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION, "--show-for-stub-purposes-annotations", "-show-for-stub-purposes-annotation" -> {
val annotation = getValue(args, ++index)
mutableShowForStubPurposesAnnotation.add(annotation)
// These should also be counted as show annotations
mutableShowAnnotations.add(annotation)
}
ARG_SHOW_UNANNOTATED, "-showUnannotated" -> showUnannotated = true
"--showAnnotationOverridesVisibility" -> {
unimplemented(arg)
showAnnotationOverridesVisibility = true
}
ARG_HIDE_ANNOTATION, "--hideAnnotations", "-hideAnnotation" ->
mutableHideAnnotations.add(getValue(args, ++index))
ARG_HIDE_META_ANNOTATION, "--hideMetaAnnotations", "-hideMetaAnnotation" ->
mutableHideMetaAnnotations.add(getValue(args, ++index))
ARG_STUBS, "-stubs" -> stubsDir = stringToNewDir(getValue(args, ++index))
ARG_DOC_STUBS -> docStubsDir = stringToNewDir(getValue(args, ++index))
ARG_KOTLIN_STUBS -> kotlinStubs = true
ARG_STUBS_SOURCE_LIST -> stubsSourceList = stringToNewFile(getValue(args, ++index))
ARG_DOC_STUBS_SOURCE_LIST -> docStubsSourceList = stringToNewFile(getValue(args, ++index))
ARG_EXCLUDE_ALL_ANNOTATIONS -> generateAnnotations = false
ARG_EXCLUDE_DOCUMENTATION_FROM_STUBS -> includeDocumentationInStubs = false
ARG_ENHANCE_DOCUMENTATION -> enhanceDocumentation = true
// Note that this only affects stub generation, not signature files.
// For signature files, clear the compatibility mode
// (--annotations-in-signatures)
ARG_INCLUDE_ANNOTATIONS -> generateAnnotations = true
ARG_PASS_THROUGH_ANNOTATION -> {
val annotations = getValue(args, ++index)
annotations.split(",").forEach { path ->
mutablePassThroughAnnotations.add(path)
}
}
ARG_EXCLUDE_ANNOTATION -> {
val annotations = getValue(args, ++index)
annotations.split(",").forEach { path ->
mutableExcludeAnnotations.add(path)
}
}
// Flag used by test suite to avoid including locations in
// the output when diffing against golden files
"--omit-locations" -> omitLocations = true
ARG_PROGUARD, "-proguard" -> proguard = stringToNewFile(getValue(args, ++index))
ARG_HIDE_PACKAGE, "-hidePackage" -> mutableHidePackages.add(getValue(args, ++index))
ARG_STUB_PACKAGES, "-stubpackages" -> {
val packages = getValue(args, ++index)
val filter = stubPackages ?: run {
val newFilter = PackageFilter()
stubPackages = newFilter
newFilter
}
filter.addPackages(packages)
}
ARG_STUB_IMPORT_PACKAGES, "-stubimportpackages" -> {
val packages = getValue(args, ++index)
for (pkg in packages.split(File.pathSeparatorChar)) {
mutableStubImportPackages.add(pkg)
mutableHidePackages.add(pkg)
}
}
"--skip-emit-packages" -> {
val packages = getValue(args, ++index)
mutableSkipEmitPackages += packages.split(File.pathSeparatorChar)
}
ARG_TYPEDEFS_IN_SIGNATURES -> {
val type = getValue(args, ++index)
typedefMode = when (type) {
"ref" -> TypedefMode.REFERENCE
"inline" -> TypedefMode.INLINE
"none" -> TypedefMode.NONE
else -> throw DriverException(
stderr = "$ARG_TYPEDEFS_IN_SIGNATURES must be one of ref, inline, none; was $type"
)
}
}
ARG_IGNORE_CLASSES_ON_CLASSPATH -> {
allowClassesFromClasspath = false
}
ARG_BASELINE, ARG_BASELINE_API_LINT, ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED -> {
val nextArg = getValue(args, ++index)
val builder = getBaselineBuilderForArg(arg)
builder.file = stringToExistingFile(nextArg)
}
ARG_REPORT_EVEN_IF_SUPPRESSED -> {
val relative = getValue(args, ++index)
if (reportEvenIfSuppressed != null) {
throw DriverException("Only one $ARG_REPORT_EVEN_IF_SUPPRESSED is allowed; found both $reportEvenIfSuppressed and $relative")
}
reportEvenIfSuppressed = stringToNewOrExistingFile(relative)
reportEvenIfSuppressedWriter = reportEvenIfSuppressed?.printWriter()
}
ARG_MERGE_BASELINE, ARG_UPDATE_BASELINE, ARG_UPDATE_BASELINE_API_LINT, ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED -> {
val builder = getBaselineBuilderForArg(arg)
builder.merge = (arg == ARG_MERGE_BASELINE)
if (index < args.size - 1) {
val nextArg = args[index + 1]
if (!nextArg.startsWith("-")) {
index++
builder.updateFile = stringToNewOrExistingFile(nextArg)
}
}
}
ARG_ERROR_MESSAGE_API_LINT -> errorMessageApiLint = getValue(args, ++index)
ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED -> errorMessageCompatibilityReleased = getValue(args, ++index)
ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_CURRENT -> errorMessageCompatibilityCurrent = getValue(args, ++index)
ARG_PASS_BASELINE_UPDATES -> passBaselineUpdates = true
ARG_DELETE_EMPTY_BASELINES -> deleteEmptyBaselines = true
ARG_DELETE_EMPTY_REMOVED_SIGNATURES -> deleteEmptyRemovedSignatures = true
ARG_PUBLIC, "-public" -> docLevel = DocLevel.PUBLIC
ARG_PROTECTED, "-protected" -> docLevel = DocLevel.PROTECTED
ARG_PACKAGE, "-package" -> docLevel = DocLevel.PACKAGE
ARG_PRIVATE, "-private" -> docLevel = DocLevel.PRIVATE
ARG_HIDDEN, "-hidden" -> docLevel = DocLevel.HIDDEN
ARG_INPUT_API_JAR -> apiJar = stringToExistingFile(getValue(args, ++index))
ARG_EXTRACT_ANNOTATIONS -> externalAnnotations = stringToNewFile(getValue(args, ++index))
ARG_COPY_ANNOTATIONS -> {
privateAnnotationsSource = stringToExistingDir(getValue(args, ++index))
privateAnnotationsTarget = stringToNewDir(getValue(args, ++index))
}
ARG_REWRITE_ANNOTATIONS -> rewriteAnnotations = stringToExistingDirsOrJars(getValue(args, ++index))
ARG_INCLUDE_ANNOTATION_CLASSES -> copyStubAnnotationsFrom = stringToExistingDir(getValue(args, ++index))
ARG_INCLUDE_SOURCE_RETENTION -> includeSourceRetentionAnnotations = true
"--previous-api" -> {
migrateNullsFrom = stringToExistingFile(getValue(args, ++index))
reporter.report(
Issues.DEPRECATED_OPTION, null as File?,
"--previous-api is deprecated; instead " +
"use $ARG_MIGRATE_NULLNESS $migrateNullsFrom"
)
}
ARG_MIGRATE_NULLNESS -> {
// See if the next argument specifies the nullness API codebase
if (index < args.size - 1) {
val nextArg = args[index + 1]
if (!nextArg.startsWith("-")) {
val file = stringToExistingFile(nextArg)
if (file.isFile) {
index++
migrateNullsFrom = file
}
}
}
}
"--current-api" -> {
val file = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.PUBLIC_API, ReleaseType.DEV))
reporter.report(
Issues.DEPRECATED_OPTION, null as File?,
"--current-api is deprecated; instead " +
"use $ARG_CHECK_COMPATIBILITY_API_CURRENT"
)
}
ARG_CHECK_COMPATIBILITY -> {
// See if the next argument specifies the compatibility check.
// Synonymous with ARG_CHECK_COMPATIBILITY_API_CURRENT, though
// for backwards compatibility with earlier versions and usages
// can also works in conjunction with ARG_CURRENT_API where the
// usage was to use ARG_CURRENT_API to point to the API file and
// then specify ARG_CHECK_COMPATIBILITY (without an argument) to
// indicate that the current api should also be checked for
// compatibility.
if (index < args.size - 1) {
val nextArg = args[index + 1]
if (!nextArg.startsWith("-")) {
val file = stringToExistingFile(nextArg)
if (file.isFile) {
index++
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.PUBLIC_API, ReleaseType.DEV))
}
}
}
}
ARG_CHECK_COMPATIBILITY_API_CURRENT -> {
val file = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.PUBLIC_API, ReleaseType.DEV))
}
ARG_CHECK_COMPATIBILITY_API_RELEASED -> {
val file = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.PUBLIC_API, ReleaseType.RELEASED))
}
ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT -> {
val file = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.REMOVED, ReleaseType.DEV))
}
ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED -> {
val file = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.REMOVED, ReleaseType.RELEASED))
}
ARG_CHECK_COMPATIBILITY_BASE_API -> {
val file = stringToExistingFile(getValue(args, ++index))
baseApiForCompatCheck = file
}
ARG_ALLOW_COMPATIBLE_DIFFERENCES -> allowCompatibleDifferences = true
ARG_NO_NATIVE_DIFF -> noNativeDiff = true
// Compat flag for the old API check command, invoked from build/make/core/definitions.mk:
"--check-api-files" -> {
if (index < args.size - 1 && args[index + 1].startsWith("-")) {
// Work around bug where --check-api-files is invoked with all
// the other metalava args before the 4 files; this will be
// fixed by https://android-review.googlesource.com/c/platform/build/+/874473
delayedCheckApiFiles = true
} else {
val stableApiFile = stringToExistingFile(getValue(args, ++index))
val apiFileToBeTested = stringToExistingFile(getValue(args, ++index))
val stableRemovedApiFile = stringToExistingFile(getValue(args, ++index))
val removedApiFileToBeTested = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(
CheckRequest(
stableApiFile,
ApiType.PUBLIC_API,
ReleaseType.RELEASED,
apiFileToBeTested
)
)
mutableCompatibilityChecks.add(
CheckRequest(
stableRemovedApiFile,
ApiType.REMOVED,
ReleaseType.RELEASED,
removedApiFileToBeTested
)
)
}
}
ARG_ERROR, "-error" -> setIssueSeverity(
getValue(args, ++index),
Severity.ERROR,
arg
)
ARG_WARNING, "-warning" -> setIssueSeverity(
getValue(args, ++index),
Severity.WARNING,
arg
)
ARG_LINT, "-lint" -> setIssueSeverity(getValue(args, ++index), Severity.LINT, arg)
ARG_HIDE, "-hide" -> setIssueSeverity(getValue(args, ++index), Severity.HIDDEN, arg)
ARG_WARNINGS_AS_ERRORS -> warningsAreErrors = true
ARG_LINTS_AS_ERRORS -> lintsAreErrors = true
"-werror" -> {
// Temporarily disabled; this is used in various builds but is pretty much
// never what we want.
// warningsAreErrors = true
}
"-lerror" -> {
// Temporarily disabled; this is used in various builds but is pretty much
// never what we want.
// lintsAreErrors = true
}
ARG_API_LINT -> {
checkApi = true
if (index < args.size - 1) {
val nextArg = args[index + 1]
if (!nextArg.startsWith("-")) {
val file = stringToExistingFile(nextArg)
if (file.isFile) {
index++
checkApiBaselineApiFile = file
}
}
}
}
ARG_API_LINT_IGNORE_PREFIX -> {
checkApiIgnorePrefix.add(getValue(args, ++index))
}
ARG_COLOR -> color = true
ARG_NO_COLOR -> color = false
ARG_NO_BANNER -> {
// Already processed above but don't flag it here as invalid
}
// Extracting API levels
ARG_ANDROID_JAR_PATTERN -> {
val list = androidJarPatterns ?: run {
val list = arrayListOf<String>()
androidJarPatterns = list
list
}
list.add(getValue(args, ++index))
}
ARG_CURRENT_VERSION -> {
currentApiLevel = Integer.parseInt(getValue(args, ++index))
if (currentApiLevel <= 26) {
throw DriverException("Suspicious currentApi=$currentApiLevel, expected at least 27")
}
}
ARG_FIRST_VERSION -> {
firstApiLevel = Integer.parseInt(getValue(args, ++index))
}
ARG_CURRENT_CODENAME -> {
currentCodeName = getValue(args, ++index)
}
ARG_CURRENT_JAR -> {
currentJar = stringToExistingFile(getValue(args, ++index))
}
ARG_GENERATE_API_LEVELS -> {
generateApiLevelXml = stringToNewFile(getValue(args, ++index))
}
ARG_APPLY_API_LEVELS -> {
applyApiLevelsXml = if (args.contains(ARG_GENERATE_API_LEVELS)) {
// If generating the API file at the same time, it doesn't have
// to already exist
stringToNewFile(getValue(args, ++index))
} else {
stringToExistingFile(getValue(args, ++index))
}
}
ARG_UPDATE_API, "--update-api" -> onlyUpdateApi = true
ARG_CHECK_API -> onlyCheckApi = true
ARG_REPLACE_DOCUMENTATION -> {
val packageNames = args[++index].split(":")
val regex = Regex(args[++index])
val replacement = args[++index]
val docReplacement = DocReplacement(packageNames, regex, replacement)
docReplacements.add(docReplacement)
}
ARG_REGISTER_ARTIFACT, "-artifact" -> {
val descriptor = stringToExistingFile(getValue(args, ++index))
val artifactId = getValue(args, ++index)
artifactRegistrations.register(artifactId, descriptor)
}
ARG_CONVERT_TO_JDIFF,
ARG_CONVERT_TO_V1,
ARG_CONVERT_TO_V2,
// doclava compatibility:
"-convert2xml",
"-convert2xmlnostrip" -> {
val strip = arg == "-convert2xml"
val format = when (arg) {
ARG_CONVERT_TO_V1 -> FileFormat.V1
ARG_CONVERT_TO_V2 -> FileFormat.V2
else -> FileFormat.JDIFF
}
val signatureFile = stringToExistingFile(getValue(args, ++index))
val outputFile = stringToNewFile(getValue(args, ++index))
mutableConvertToXmlFiles.add(ConvertFile(signatureFile, outputFile, null, strip, format))
}
ARG_CONVERT_NEW_TO_JDIFF,
ARG_CONVERT_NEW_TO_V1,
ARG_CONVERT_NEW_TO_V2,
// doclava compatibility:
"-new_api",
"-new_api_no_strip" -> {
val format = when (arg) {
ARG_CONVERT_NEW_TO_V1 -> FileFormat.V1
ARG_CONVERT_NEW_TO_V2 -> FileFormat.V2
else -> FileFormat.JDIFF
}
val strip = arg == "-new_api"
val baseFile = stringToExistingFile(getValue(args, ++index))
val signatureFile = stringToExistingFile(getValue(args, ++index))
val jDiffFile = stringToNewFile(getValue(args, ++index))
mutableConvertToXmlFiles.add(ConvertFile(signatureFile, jDiffFile, baseFile, strip, format))
}
"--write-android-jar-signatures" -> {
val root = stringToExistingDir(getValue(args, ++index))
if (!File(root, "prebuilts/sdk").isDirectory) {
throw DriverException("$androidJarSignatureFiles does not point to an Android source tree")
}
androidJarSignatureFiles = root
}
"-encoding" -> {
val value = getValue(args, ++index)
if (value.uppercase(Locale.getDefault()) != "UTF-8") {
throw DriverException("$value: Only UTF-8 encoding is supported")
}
}
ARG_JAVA_SOURCE, "-source" -> {
val value = getValue(args, ++index)
val level = LanguageLevel.parse(value)
when {
level == null -> throw DriverException("$value is not a valid or supported Java language level")
level.isLessThan(LanguageLevel.JDK_1_7) -> throw DriverException("$arg must be at least 1.7")
else -> javaLanguageLevel = level
}
}
ARG_KOTLIN_SOURCE -> {
val value = getValue(args, ++index)
val languageLevel =
LanguageVersion.fromVersionString(value)
?: throw DriverException("$value is not a valid or supported Kotlin language level")
val apiVersion = ApiVersion.createByLanguageVersion(languageLevel)
val settings = LanguageVersionSettingsImpl(languageLevel, apiVersion)
kotlinLanguageLevel = settings
}
ARG_JDK_HOME -> {
jdkHome = stringToExistingDir(getValue(args, ++index))
}
ARG_SDK_HOME -> {
sdkHome = stringToExistingDir(getValue(args, ++index))
}
ARG_COMPILE_SDK_VERSION -> {
compileSdkVersion = getValue(args, ++index)
}
ARG_NO_IMPLICIT_ROOT -> {
allowImplicitRoot = false
}
ARG_STRICT_INPUT_FILES, ARG_STRICT_INPUT_FILES_WARN, ARG_STRICT_INPUT_FILES_STACK -> {
if (strictInputViolationsFile != null) {
throw DriverException("$ARG_STRICT_INPUT_FILES, $ARG_STRICT_INPUT_FILES_WARN and $ARG_STRICT_INPUT_FILES_STACK may be specified only once")
}
strictInputFiles = StrictInputFileMode.fromArgument(arg)
val file = stringToNewOrExistingFile(getValue(args, ++index))
strictInputViolationsFile = file
strictInputViolationsPrintWriter = file.printWriter()
}
ARG_STRICT_INPUT_FILES_EXEMPT -> {
val listString = getValue(args, ++index)
listString.split(File.pathSeparatorChar).forEach { path ->
// Throw away the result; just let the function add the files to the
// allowed list.
stringToExistingFilesOrDirs(path)
}
}
ARG_REPEAT_ERRORS_MAX -> {
repeatErrorsMax = Integer.parseInt(getValue(args, ++index))
}
"--temp-folder" -> {
tempFolder = stringToNewOrExistingDir(getValue(args, ++index))
}
// Option only meant for tests (not documented); doesn't work in all cases (to do that we'd
// need JNA to call libc)
"--pwd" -> {
val pwd = stringToExistingDir(getValue(args, ++index)).absoluteFile
System.setProperty("user.dir", pwd.path)
}
"--noop", "--no-op" -> {
}
// Doclava1 flag: Already the behavior in metalava
"-keepstubcomments" -> {
}
// Unimplemented doclava1 flags (no arguments)
"-quiet",
"-yamlV2" -> {
unimplemented(arg)
}
"-android" -> { // partially implemented: Pick up the color hint, but there may be other implications
color = true
unimplemented(arg)
}
"-stubsourceonly" -> {
/* noop */
}
// Unimplemented doclava1 flags (1 argument)
"-d" -> {
unimplemented(arg)
index++
}
// Unimplemented doclava1 flags (2 arguments)
"-since" -> {
unimplemented(arg)
index += 2
}
// doclava1 doc-related flags: only supported here to make this command a drop-in
// replacement
"-referenceonly",
"-devsite",
"-ignoreJdLinks",
"-nodefaultassets",
"-parsecomments",
"-offlinemode",
"-gcmref",
"-metadataDebug",
"-includePreview",
"-staticonly",
"-navtreeonly",
"-atLinksNavtree" -> {
javadoc(arg)
}
// doclava1 flags with 1 argument
"-doclet",
"-docletpath",
"-templatedir",
"-htmldir",
"-knowntags",
"-resourcesdir",
"-resourcesoutdir",
"-yaml",
"-apidocsdir",
"-toroot",
"-samplegroup",
"-samplesdir",
"-dac_libraryroot",
"-dac_dataname",
"-title",
"-proofread",
"-todo",
"-overview" -> {
javadoc(arg)
index++
}
// doclava1 flags with two arguments
"-federate",
"-federationapi",
"-htmldir2" -> {
javadoc(arg)
index += 2
}
// doclava1 flags with three arguments
"-samplecode" -> {
javadoc(arg)
index += 3
}
// doclava1 flag with variable number of arguments; skip everything until next arg
"-hdf" -> {
javadoc(arg)
index++
while (index < args.size) {
if (args[index].startsWith("-")) {
break
}
index++
}
index--
}
else -> {
if (arg.startsWith("-J-") || arg.startsWith("-XD")) {
// -J: mechanism to pass extra flags to javadoc, e.g.
// -J-XX:-OmitStackTraceInFastThrow
// -XD: mechanism to set properties, e.g.
// -XDignore.symbol.file
javadoc(arg)
} else if (arg.startsWith(ARG_OUTPUT_KOTLIN_NULLS)) {
outputKotlinStyleNulls = if (arg == ARG_OUTPUT_KOTLIN_NULLS) {
true
} else {
yesNo(arg.substring(ARG_OUTPUT_KOTLIN_NULLS.length + 1))
}
} else if (arg.startsWith(ARG_INPUT_KOTLIN_NULLS)) {
inputKotlinStyleNulls = if (arg == ARG_INPUT_KOTLIN_NULLS) {
true
} else {
yesNo(arg.substring(ARG_INPUT_KOTLIN_NULLS.length + 1))
}
} else if (arg.startsWith(ARG_OUTPUT_DEFAULT_VALUES)) {
outputDefaultValues = if (arg == ARG_OUTPUT_DEFAULT_VALUES) {
true
} else {
yesNo(arg.substring(ARG_OUTPUT_DEFAULT_VALUES.length + 1))
}
} else if (arg.startsWith(ARG_INCLUDE_SIG_VERSION)) {
includeSignatureFormatVersion = if (arg == ARG_INCLUDE_SIG_VERSION)
true
else yesNo(arg.substring(ARG_INCLUDE_SIG_VERSION.length + 1))
} else if (arg.startsWith(ARG_FORMAT)) {
outputFormat = when (arg) {
"$ARG_FORMAT=v1" -> {
FileFormat.V1
}
"$ARG_FORMAT=v2", "$ARG_FORMAT=recommended" -> {
FileFormat.V2
}
"$ARG_FORMAT=v3" -> {
FileFormat.V3
}
"$ARG_FORMAT=v4", "$ARG_FORMAT=latest" -> {
FileFormat.V4
}
else -> throw DriverException(stderr = "Unexpected signature format; expected v1, v2, v3 or v4")
}
outputFormat.configureOptions(this)
} else if (arg.startsWith("-")) {
// Some other argument: display usage info and exit
val usage = getUsage(includeHeader = false, colorize = color)
throw DriverException(stderr = "Invalid argument $arg\n\n$usage")
} else {
if (delayedCheckApiFiles) {
delayedCheckApiFiles = false
val stableApiFile = stringToExistingFile(arg)
val apiFileToBeTested = stringToExistingFile(getValue(args, ++index))
val stableRemovedApiFile = stringToExistingFile(getValue(args, ++index))
val removedApiFileToBeTested = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(
CheckRequest(
stableApiFile,
ApiType.PUBLIC_API,
ReleaseType.RELEASED,
apiFileToBeTested
)
)
mutableCompatibilityChecks.add(
CheckRequest(
stableRemovedApiFile,
ApiType.REMOVED,
ReleaseType.RELEASED,
removedApiFileToBeTested
)
)
} else {
// All args that don't start with "-" are taken to be filenames
mutableSources.addAll(stringToExistingFiles(arg))
// Temporary workaround for
// aosp/I73ff403bfc3d9dfec71789a3e90f9f4ea95eabe3
if (arg.endsWith("hwbinder-stubs-docs-stubs.srcjar.rsp")) {
skipGenerateAnnotations = true
}
}
}
}
}
++index
}
if (generateApiLevelXml != null) {
// <String> is redundant here but while IDE (with newer type inference engine
// understands that) the current 1.3.x compiler does not
@Suppress("RemoveExplicitTypeArguments")
val patterns = androidJarPatterns ?: run {
mutableListOf<String>()
}
// Fallbacks
patterns.add("prebuilts/tools/common/api-versions/android-%/android.jar")
patterns.add("prebuilts/sdk/%/public/android.jar")
apiLevelJars = findAndroidJars(
patterns,
firstApiLevel,
currentApiLevel,
currentCodeName,
currentJar
)
}
// outputKotlinStyleNulls implies at least format=v3
if (outputKotlinStyleNulls) {
if (outputFormat < FileFormat.V3) {
outputFormat = FileFormat.V3
}
outputFormat.configureOptions(this)
}
// If the caller has not explicitly requested that unannotated classes and
// members should be shown in the output then only show them if no annotations were provided.
if (!showUnannotated && showAnnotations.isEmpty()) {
showUnannotated = true
}
if (skipGenerateAnnotations) {
generateAnnotations = false
}
if (onlyUpdateApi) {
if (onlyCheckApi) {
throw DriverException(stderr = "Cannot supply both $ARG_UPDATE_API and $ARG_CHECK_API at the same time")
}
// We're running in update API mode: cancel other "action" flags; only signature file generation
// flags count
apiLevelJars = null
generateApiLevelXml = null
applyApiLevelsXml = null
androidJarSignatureFiles = null
stubsDir = null
docStubsDir = null
stubsSourceList = null
docStubsSourceList = null
sdkValueDir = null
externalAnnotations = null
proguard = null
mutableCompatibilityChecks.clear()
mutableAnnotationCoverageOf.clear()
artifactRegistrations.clear()
mutableConvertToXmlFiles.clear()
nullabilityAnnotationsValidator = null
nullabilityWarningsTxt = null
validateNullabilityFromMergedStubs = false
validateNullabilityFromMergedStubs = false
validateNullabilityFromList = null
} else if (onlyCheckApi) {
apiLevelJars = null
generateApiLevelXml = null
applyApiLevelsXml = null
androidJarSignatureFiles = null
stubsDir = null
docStubsDir = null
stubsSourceList = null
docStubsSourceList = null
sdkValueDir = null
externalAnnotations = null
proguard = null
mutableAnnotationCoverageOf.clear()
artifactRegistrations.clear()
mutableConvertToXmlFiles.clear()
nullabilityAnnotationsValidator = null
nullabilityWarningsTxt = null
validateNullabilityFromMergedStubs = false
validateNullabilityFromMergedStubs = false
validateNullabilityFromList = null
apiFile = null
apiXmlFile = null
dexApiFile = null
removedApiFile = null
}
// Fix up [Baseline] files and [Reporter]s.
val baselineHeaderComment = if (isBuildingAndroid())
"// See tools/metalava/API-LINT.md for how to update this file.\n\n"
else
""
baselineBuilder.headerComment = baselineHeaderComment
baselineApiLintBuilder.headerComment = baselineHeaderComment
baselineCompatibilityReleasedBuilder.headerComment = baselineHeaderComment
if (baselineBuilder.file == null) {
// If default baseline is a file, use it.
val defaultBaselineFile = getDefaultBaselineFile()
if (defaultBaselineFile != null && defaultBaselineFile.isFile) {
baselineBuilder.file = defaultBaselineFile
}
}
baseline = baselineBuilder.build()
baselineApiLint = baselineApiLintBuilder.build()
baselineCompatibilityReleased = baselineCompatibilityReleasedBuilder.build()
reporterApiLint = Reporter(
baselineApiLint ?: baseline,
errorMessageApiLint
)
reporterCompatibilityReleased = Reporter(
baselineCompatibilityReleased ?: baseline,
errorMessageCompatibilityReleased
)
reporterCompatibilityCurrent = Reporter(
// Note, the compat-check:current shouldn't take a baseline file, so we don't have
// a task specific baseline file, but we still respect the global baseline file.
baseline,
errorMessageCompatibilityCurrent
)
// Build "all baselines" and "all reporters"
// Baselines are nullable, so selectively add to the list.
allBaselines = listOfNotNull(baseline, baselineApiLint, baselineCompatibilityReleased)
// Reporters are non-null.
allReporters = listOf(
reporter,
reporterApiLint,
reporterCompatibilityReleased,
reporterCompatibilityCurrent
)
updateClassPath()
checkFlagConsistency()
}
/** Update the classpath to insert android.jar or JDK classpath elements if necessary */
private fun updateClassPath() {
val sdkHome = sdkHome
val jdkHome = jdkHome
if (sdkHome != null &&
compileSdkVersion != null &&
classpath.none { it.name == FN_FRAMEWORK_LIBRARY }
) {
val jar = File(sdkHome, "platforms/android-$compileSdkVersion")
if (jar.isFile) {
mutableClassPath.add(jar)
} else {
throw DriverException(
stderr = "Could not find android.jar for API level " +
"$compileSdkVersion in SDK $sdkHome: $jar does not exist"
)
}
if (jdkHome != null) {
throw DriverException(stderr = "Do not specify both $ARG_SDK_HOME and $ARG_JDK_HOME")
}
} else if (jdkHome != null) {
val isJre = !isJdkFolder(jdkHome)
@Suppress("DEPRECATION")
val roots = JavaSdkUtil.getJdkClassesRoots(jdkHome, isJre)
mutableClassPath.addAll(roots)
}
}
fun isJdkModular(homePath: File): Boolean {
return File(homePath, "jmods").isDirectory
}
/**
* Produce a default file name for the baseline. It's normally "baseline.txt", but can
* be prefixed by show annotations; e.g. @TestApi -> test-baseline.txt, @SystemApi -> system-baseline.txt,
* etc.
*
* Note because the default baseline file is not explicitly set in the command line,
* this file would trigger a --strict-input-files violation. To avoid that, always explicitly
* pass a baseline file.
*/
private fun getDefaultBaselineFile(): File? {
if (sourcePath.isNotEmpty() && sourcePath[0].path.isNotBlank()) {
fun annotationToPrefix(qualifiedName: String): String {
val name = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
return name.lowercase(Locale.US).removeSuffix("api") + "-"
}
val sb = StringBuilder()
showAnnotations.getIncludedAnnotationNames().forEach { sb.append(annotationToPrefix(it)) }
sb.append(DEFAULT_BASELINE_NAME)
var base = sourcePath[0]
// Convention: in AOSP, signature files are often in sourcepath/api: let's place baseline
// files there too
val api = File(base, "api")
if (api.isDirectory) {
base = api
}
return File(base, sb.toString())
} else {
return null
}
}
/**
* Find an android stub jar that matches the given criteria.
*
* Note because the default baseline file is not explicitly set in the command line,
* this file would trigger a --strict-input-files violation. To avoid that, use
* --strict-input-files-exempt to exempt the jar directory.
*/
private fun findAndroidJars(
androidJarPatterns: List<String>,
minApi: Int,
currentApiLevel: Int,
currentCodeName: String?,
currentJar: File?
): Array<File> {
@Suppress("NAME_SHADOWING")
val currentApiLevel = if (currentCodeName != null && "REL" != currentCodeName) {
currentApiLevel + 1
} else {
currentApiLevel
}
val apiLevelFiles = mutableListOf<File>()
// api level 0: placeholder, should not be processed.
// (This is here because we want the array index to match
// the API level)
val element = File("not an api: the starting API index is $minApi")
for (i in 0 until minApi) {
apiLevelFiles.add(element)
}
// Get all the android.jar. They are in platforms-#
var apiLevel = minApi - 1
while (true) {
apiLevel++
try {
var jar: File? = null
if (apiLevel == currentApiLevel) {
jar = currentJar
}
if (jar == null) {
jar = getAndroidJarFile(apiLevel, androidJarPatterns)
}
if (jar == null || !jar.isFile) {
if (verbose) {
stdout.println("Last API level found: ${apiLevel - 1}")
}
if (apiLevel < 28) {
// Clearly something is wrong with the patterns; this should result in a build error
val argList = mutableListOf<String>()
args.forEachIndexed { index, arg ->
if (arg == ARG_ANDROID_JAR_PATTERN) {
argList.add(args[index + 1])
}
}
throw DriverException(
stderr = "Could not find android.jar for API level $apiLevel; the " +
"$ARG_ANDROID_JAR_PATTERN set might be invalid: ${argList.joinToString()}"
)
}
break
}
if (verbose) {
stdout.println("Found API $apiLevel at ${jar.path}")
}
apiLevelFiles.add(jar)
} catch (e: IOException) {
e.printStackTrace()
}
}
return apiLevelFiles.toTypedArray()
}
private fun getAndroidJarFile(apiLevel: Int, patterns: List<String>): File? {
// Note this method doesn't register the result to [FileReadSandbox]
return patterns
.map { fileForPathInner(it.replace("%", apiLevel.toString())) }
.firstOrNull { it.isFile }
}
private fun yesNo(answer: String): Boolean {
return when (answer) {
"yes", "true", "enabled", "on" -> true
"no", "false", "disabled", "off" -> false
else -> throw DriverException(stderr = "Unexpected $answer; expected yes or no")
}
}
/** Makes sure that the flag combinations make sense */
private fun checkFlagConsistency() {
if (apiJar != null && sources.isNotEmpty()) {
throw DriverException(stderr = "Specify either $ARG_SOURCE_FILES or $ARG_INPUT_API_JAR, not both")
}
}
private fun javadoc(arg: String) {
if (!alreadyWarned.add(arg)) {
return
}
if (!options.quiet) {
reporter.report(
Severity.WARNING, null as String?, "Ignoring javadoc-related doclava1 flag $arg",
color = color
)
}
}
private fun unimplemented(arg: String) {
if (!alreadyWarned.add(arg)) {
return
}
if (!options.quiet) {
val message = "Ignoring unimplemented doclava1 flag $arg" +
when (arg) {
"-encoding" -> " (UTF-8 assumed)"
"-source" -> " (1.8 assumed)"
else -> ""
}
reporter.report(Severity.WARNING, null as String?, message, color = color)
}
}
private fun helpAndQuit(colorize: Boolean = color) {
throw DriverException(stdout = getUsage(colorize = colorize))
}
private fun getValue(args: Array<String>, index: Int): String {
if (index >= args.size) {
throw DriverException("Missing argument for ${args[index - 1]}")
}
return args[index]
}
private fun stringToExistingDir(value: String): File {
val file = fileForPathInner(value)
if (!file.isDirectory) {
throw DriverException("$file is not a directory")
}
return FileReadSandbox.allowAccess(file)
}
@Suppress("unused")
private fun stringToExistingDirs(value: String): List<File> {
val files = mutableListOf<File>()
for (path in value.split(File.pathSeparatorChar)) {
val file = fileForPathInner(path)
if (!file.isDirectory) {
throw DriverException("$file is not a directory")
}
files.add(file)
}
return FileReadSandbox.allowAccess(files)
}
private fun stringToExistingDirsOrJars(value: String, exempt: Boolean = true): List<File> {
val files = mutableListOf<File>()
for (path in value.split(File.pathSeparatorChar)) {
val file = fileForPathInner(path)
if (!file.isDirectory && !(file.path.endsWith(SdkConstants.DOT_JAR) && file.isFile)) {
throw DriverException("$file is not a jar or directory")
}
files.add(file)
}
if (exempt) {
return FileReadSandbox.allowAccess(files)
}
return files
}
private fun stringToExistingDirsOrFiles(value: String): List<File> {
val files = mutableListOf<File>()
for (path in value.split(File.pathSeparatorChar)) {
val file = fileForPathInner(path)
if (!file.exists()) {
throw DriverException("$file does not exist")
}
files.add(file)
}
return FileReadSandbox.allowAccess(files)
}
private fun stringToExistingFile(value: String): File {
val file = fileForPathInner(value)
if (!file.isFile) {
throw DriverException("$file is not a file")
}
return FileReadSandbox.allowAccess(file)
}
@Suppress("unused")
private fun stringToExistingFileOrDir(value: String): File {
val file = fileForPathInner(value)
if (!file.exists()) {
throw DriverException("$file is not a file or directory")
}
return FileReadSandbox.allowAccess(file)
}
private fun stringToExistingFiles(value: String): List<File> {
return stringToExistingFilesOrDirsInternal(value, false)
}
private fun stringToExistingFilesOrDirs(value: String): List<File> {
return stringToExistingFilesOrDirsInternal(value, true)
}
private fun stringToExistingFilesOrDirsInternal(value: String, allowDirs: Boolean): List<File> {
val files = mutableListOf<File>()
value.split(File.pathSeparatorChar)
.map { fileForPathInner(it) }
.forEach { file ->
if (file.path.startsWith("@")) {
// File list; files to be read are stored inside. SHOULD have been one per line
// but sadly often uses spaces for separation too (so we split by whitespace,
// which means you can't point to files in paths with spaces)
val listFile = File(file.path.substring(1))
if (!allowDirs && !listFile.isFile) {
throw DriverException("$listFile is not a file")
}
val contents = Files.asCharSource(listFile, UTF_8).read()
val pathList = Splitter.on(CharMatcher.whitespace()).trimResults().omitEmptyStrings().split(
contents
)
pathList.asSequence().map { File(it) }.forEach {
if (!allowDirs && !it.isFile) {
throw DriverException("$it is not a file")
}
files.add(it)
}
} else {
if (!allowDirs && !file.isFile) {
throw DriverException("$file is not a file")
}
files.add(file)
}
}
return FileReadSandbox.allowAccess(files)
}
private fun stringToNewFile(value: String): File {
val output = fileForPathInner(value)
if (output.exists()) {
if (output.isDirectory) {
throw DriverException("$output is a directory")
}
val deleted = output.delete()
if (!deleted) {
throw DriverException("Could not delete previous version of $output")
}
} else if (output.parentFile != null && !output.parentFile.exists()) {
val ok = output.parentFile.mkdirs()
if (!ok) {
throw DriverException("Could not create ${output.parentFile}")
}
}
return FileReadSandbox.allowAccess(output)
}
private fun stringToNewOrExistingDir(value: String): File {
val dir = fileForPathInner(value)
if (!dir.isDirectory) {
val ok = dir.mkdirs()
if (!ok) {
throw DriverException("Could not create $dir")
}
}
return FileReadSandbox.allowAccess(dir)
}
private fun stringToNewOrExistingFile(value: String): File {
val file = fileForPathInner(value)
if (!file.exists()) {
val parentFile = file.parentFile
if (parentFile != null && !parentFile.isDirectory) {
val ok = parentFile.mkdirs()
if (!ok) {
throw DriverException("Could not create $parentFile")
}
}
}
return FileReadSandbox.allowAccess(file)
}
private fun stringToNewDir(value: String): File {
val output = fileForPathInner(value)
val ok =
if (output.exists()) {
if (output.isDirectory) {
output.deleteRecursively()
}
if (output.exists()) {
true
} else {
output.mkdir()
}
} else {
output.mkdirs()
}
if (!ok) {
throw DriverException("Could not create $output")
}
return FileReadSandbox.allowAccess(output)
}
/**
* Converts a path to a [File] that represents the absolute path, with the following special
* behavior:
* - "~" will be expanded into the home directory path.
* - If the given path starts with "@", it'll be converted into "@" + [file's absolute path]
*
* Note, unlike the other "stringToXxx" methods, this method won't register the given path
* to [FileReadSandbox].
*/
private fun fileForPathInner(path: String): File {
// java.io.File doesn't automatically handle ~/ -> home directory expansion.
// This isn't necessary when metalava is run via the command line driver
// (since shells will perform this expansion) but when metalava is run
// directly, not from a shell.
if (path.startsWith("~/")) {
val home = System.getProperty("user.home") ?: return File(path)
return File(home + path.substring(1))
} else if (path.startsWith("@")) {
return File("@" + File(path.substring(1)).absolutePath)
}
return File(path).absoluteFile
}
private fun getUsage(includeHeader: Boolean = true, colorize: Boolean = color): String {
val usage = StringWriter()
val printWriter = PrintWriter(usage)
usage(printWriter, includeHeader, colorize)
return usage.toString()
}
private fun usage(out: PrintWriter, includeHeader: Boolean = true, colorize: Boolean = color) {
if (includeHeader) {
out.println(wrap(HELP_PROLOGUE, MAX_LINE_WIDTH, ""))
}
if (colorize) {
out.println("Usage: ${colorized(PROGRAM_NAME, TerminalColor.BLUE)} <flags>")
} else {
out.println("Usage: $PROGRAM_NAME <flags>")
}
val args = arrayOf(
"", "\nGeneral:",
ARG_HELP, "This message.",
ARG_VERSION, "Show the version of $PROGRAM_NAME.",
ARG_QUIET, "Only include vital output",
ARG_VERBOSE, "Include extra diagnostic output",
ARG_COLOR, "Attempt to colorize the output (defaults to true if \$TERM is xterm)",
ARG_NO_COLOR, "Do not attempt to colorize the output",
ARG_UPDATE_API,
"Cancel any other \"action\" flags other than generating signature files. This is here " +
"to make it easier customize build system tasks, particularly for the \"make update-api\" task.",
ARG_CHECK_API,
"Cancel any other \"action\" flags other than checking signature files. This is here " +
"to make it easier customize build system tasks, particularly for the \"make checkapi\" task.",
"$ARG_REPEAT_ERRORS_MAX <N>", "When specified, repeat at most N errors before finishing.",
"", "\nAPI sources:",
"$ARG_SOURCE_FILES <files>",
"A comma separated list of source files to be parsed. Can also be " +
"@ followed by a path to a text file containing paths to the full set of files to parse.",
"$ARG_SOURCE_PATH <paths>",
"One or more directories (separated by `${File.pathSeparator}`) " +
"containing source files (within a package hierarchy). If $ARG_STRICT_INPUT_FILES, " +
"$ARG_STRICT_INPUT_FILES_WARN, or $ARG_STRICT_INPUT_FILES_STACK are used, files accessed under " +
"$ARG_SOURCE_PATH that are not explicitly specified in $ARG_SOURCE_FILES are reported as " +
"violations.",
"$ARG_CLASS_PATH <paths>",
"One or more directories or jars (separated by " +
"`${File.pathSeparator}`) containing classes that should be on the classpath when parsing the " +
"source files",
"$ARG_MERGE_QUALIFIER_ANNOTATIONS <file>",
"An external annotations file to merge and overlay " +
"the sources, or a directory of such files. Should be used for annotations intended for " +
"inclusion in the API to be written out, e.g. nullability. Formats supported are: IntelliJ's " +
"external annotations database format, .jar or .zip files containing those, Android signature " +
"files, and Java stub files.",
"$ARG_MERGE_INCLUSION_ANNOTATIONS <file>",
"An external annotations file to merge and overlay " +
"the sources, or a directory of such files. Should be used for annotations which determine " +
"inclusion in the API to be written out, i.e. show and hide. The only format supported is " +
"Java stub files.",
ARG_VALIDATE_NULLABILITY_FROM_MERGED_STUBS,
"Triggers validation of nullability annotations " +
"for any class where $ARG_MERGE_QUALIFIER_ANNOTATIONS includes a Java stub file.",
ARG_VALIDATE_NULLABILITY_FROM_LIST,
"Triggers validation of nullability annotations " +
"for any class listed in the named file (one top-level class per line, # prefix for comment line).",
"$ARG_NULLABILITY_WARNINGS_TXT <file>",
"Specifies where to write warnings encountered during " +
"validation of nullability annotations. (Does not trigger validation by itself.)",
ARG_NULLABILITY_ERRORS_NON_FATAL,
"Specifies that errors encountered during validation of " +
"nullability annotations should not be treated as errors. They will be written out to the " +
"file specified in $ARG_NULLABILITY_WARNINGS_TXT instead.",
"$ARG_INPUT_API_JAR <file>", "A .jar file to read APIs from directly",
"$ARG_MANIFEST <file>", "A manifest file, used to for check permissions to cross check APIs",
"$ARG_REPLACE_DOCUMENTATION <p> <r> <t>",
"Amongst nonempty documentation of items from Java " +
"packages <p> and their subpackages, replaces any matches of regular expression <r> " +
"with replacement text <t>. <p> is given as a nonempty list of Java package names separated " +
"by ':' (e.g. \"java:android.util\"); <t> may contain backreferences (\$1, \$2 etc.) to " +
"matching groups from <r>.",
"$ARG_HIDE_PACKAGE <package>",
"Remove the given packages from the API even if they have not been " +
"marked with @hide",
"$ARG_SHOW_ANNOTATION <annotation class>",
"Unhide any hidden elements that are also annotated " +
"with the given annotation",
"$ARG_SHOW_SINGLE_ANNOTATION <annotation>",
"Like $ARG_SHOW_ANNOTATION, but does not apply " +
"to members; these must also be explicitly annotated",
"$ARG_SHOW_FOR_STUB_PURPOSES_ANNOTATION <annotation class>",
"Like $ARG_SHOW_ANNOTATION, but elements annotated " +
"with it are assumed to be \"implicitly\" included in the API surface, and they'll be included " +
"in certain kinds of output such as stubs, but not in others, such as the signature file and API lint",
"$ARG_HIDE_ANNOTATION <annotation class>",
"Treat any elements annotated with the given annotation " +
"as hidden",
"$ARG_HIDE_META_ANNOTATION <meta-annotation class>",
"Treat as hidden any elements annotated with an " +
"annotation which is itself annotated with the given meta-annotation",
ARG_SHOW_UNANNOTATED, "Include un-annotated public APIs in the signature file as well",
"$ARG_JAVA_SOURCE <level>", "Sets the source level for Java source files; default is 1.8.",
"$ARG_KOTLIN_SOURCE <level>", "Sets the source level for Kotlin source files; default is ${LanguageVersionSettingsImpl.DEFAULT.languageVersion}.",
"$ARG_SDK_HOME <dir>", "If set, locate the `android.jar` file from the given Android SDK",
"$ARG_COMPILE_SDK_VERSION <api>", "Use the given API level",
"$ARG_JDK_HOME <dir>", "If set, add the Java APIs from the given JDK to the classpath",
"$ARG_STUB_PACKAGES <package-list>",
"List of packages (separated by ${File.pathSeparator}) which will " +
"be used to filter out irrelevant code. If specified, only code in these packages will be " +
"included in signature files, stubs, etc. (This is not limited to just the stubs; the name " +
"is historical.) You can also use \".*\" at the end to match subpackages, so `foo.*` will " +
"match both `foo` and `foo.bar`.",
"$ARG_SUBTRACT_API <api file>",
"Subtracts the API in the given signature or jar file from the " +
"current API being emitted via $ARG_API, $ARG_STUBS, $ARG_DOC_STUBS, etc. " +
"Note that the subtraction only applies to classes; it does not subtract members.",
"$ARG_TYPEDEFS_IN_SIGNATURES <ref|inline>",
"Whether to include typedef annotations in signature " +
"files. `$ARG_TYPEDEFS_IN_SIGNATURES ref` will include just a reference to the typedef class, " +
"which is not itself part of the API and is not included as a class, and " +
"`$ARG_TYPEDEFS_IN_SIGNATURES inline` will include the constants themselves into each usage " +
"site. You can also supply `$ARG_TYPEDEFS_IN_SIGNATURES none` to explicitly turn it off, if the " +
"default ever changes.",
ARG_IGNORE_CLASSES_ON_CLASSPATH,
"Prevents references to classes on the classpath from being added to " +
"the generated stub files.",
"", "\nDocumentation:",
ARG_PUBLIC, "Only include elements that are public",
ARG_PROTECTED, "Only include elements that are public or protected",
ARG_PACKAGE, "Only include elements that are public, protected or package protected",
ARG_PRIVATE, "Include all elements except those that are marked hidden",
ARG_HIDDEN, "Include all elements, including hidden",
"", "\nExtracting Signature Files:",
// TODO: Document --show-annotation!
"$ARG_API <file>", "Generate a signature descriptor file",
"$ARG_DEX_API <file>", "Generate a DEX signature descriptor file listing the APIs",
"$ARG_REMOVED_API <file>", "Generate a signature descriptor file for APIs that have been removed",
"$ARG_FORMAT=<v1,v2,v3,...>", "Sets the output signature file format to be the given version.",
"$ARG_OUTPUT_KOTLIN_NULLS[=yes|no]",
"Controls whether nullness annotations should be formatted as " +
"in Kotlin (with \"?\" for nullable types, \"\" for non nullable types, and \"!\" for unknown. " +
"The default is yes.",
"$ARG_OUTPUT_DEFAULT_VALUES[=yes|no]",
"Controls whether default values should be included in " +
"signature files. The default is yes.",
"$ARG_INCLUDE_SIG_VERSION[=yes|no]",
"Whether the signature files should include a comment listing " +
"the format version of the signature file.",
"$ARG_PROGUARD <file>", "Write a ProGuard keep file for the API",
"$ARG_SDK_VALUES <dir>", "Write SDK values files to the given directory",
ARG_ENABLE_KOTLIN_PSI,
"[EXPERIMENTAL] If set, use Kotlin PSI for Kotlin instead of UAST",
"", "\nGenerating Stubs:",
"$ARG_STUBS <dir>", "Generate stub source files for the API",
"$ARG_DOC_STUBS <dir>",
"Generate documentation stub source files for the API. Documentation stub " +
"files are similar to regular stub files, but there are some differences. For example, in " +
"the stub files, we'll use special annotations like @RecentlyNonNull instead of @NonNull to " +
"indicate that an element is recently marked as non null, whereas in the documentation stubs we'll " +
"just list this as @NonNull. Another difference is that @doconly elements are included in " +
"documentation stubs, but not regular stubs, etc.",
ARG_KOTLIN_STUBS,
"[CURRENTLY EXPERIMENTAL] If specified, stubs generated from Kotlin source code will " +
"be written in Kotlin rather than the Java programming language.",
ARG_INCLUDE_ANNOTATIONS, "Include annotations such as @Nullable in the stub files.",
ARG_EXCLUDE_ALL_ANNOTATIONS, "Exclude annotations such as @Nullable from the stub files; the default.",
"$ARG_PASS_THROUGH_ANNOTATION <annotation classes>",
"A comma separated list of fully qualified names of " +
"annotation classes that must be passed through unchanged.",
"$ARG_EXCLUDE_ANNOTATION <annotation classes>",
"A comma separated list of fully qualified names of " +
"annotation classes that must be stripped from metalava's outputs.",
ARG_ENHANCE_DOCUMENTATION,
"Enhance documentation in various ways, for example auto-generating documentation based on source " +
"annotations present in the code. This is implied by --doc-stubs.",
ARG_EXCLUDE_DOCUMENTATION_FROM_STUBS,
"Exclude element documentation (javadoc and kdoc) " +
"from the generated stubs. (Copyright notices are not affected by this, they are always included. " +
"Documentation stubs (--doc-stubs) are not affected.)",
"$ARG_STUBS_SOURCE_LIST <file>",
"Write the list of generated stub files into the given source " +
"list file. If generating documentation stubs and you haven't also specified " +
"$ARG_DOC_STUBS_SOURCE_LIST, this list will refer to the documentation stubs; " +
"otherwise it's the non-documentation stubs.",
"$ARG_DOC_STUBS_SOURCE_LIST <file>",
"Write the list of generated doc stub files into the given source " +
"list file",
"$ARG_REGISTER_ARTIFACT <api-file> <id>",
"Registers the given id for the packages found in " +
"the given signature file. $PROGRAM_NAME will inject an @artifactId <id> tag into every top " +
"level stub class in that API.",
"", "\nDiffs and Checks:",
"$ARG_INPUT_KOTLIN_NULLS[=yes|no]",
"Whether the signature file being read should be " +
"interpreted as having encoded its types using Kotlin style types: a suffix of \"?\" for nullable " +
"types, no suffix for non nullable types, and \"!\" for unknown. The default is no.",
"$ARG_CHECK_COMPATIBILITY:type:state <file>",
"Check compatibility. Type is one of 'api' " +
"and 'removed', which checks either the public api or the removed api. State is one of " +
"'current' and 'released', to check either the currently in development API or the last publicly " +
"released API, respectively. Different compatibility checks apply in the two scenarios. " +
"For example, to check the code base against the current public API, use " +
"$ARG_CHECK_COMPATIBILITY:api:current.",
"$ARG_CHECK_COMPATIBILITY_BASE_API <file>",
"When performing a compat check, use the provided signature " +
"file as a base api, which is treated as part of the API being checked. This allows us to compute the " +
"full API surface from a partial API surface (e.g. the current @SystemApi txt file), which allows us to " +
"recognize when an API is moved from the partial API to the base API and avoid incorrectly flagging this " +
"as an API removal.",
"$ARG_API_LINT [api file]",
"Check API for Android API best practices. If a signature file is " +
"provided, only the APIs that are new since the API will be checked.",
"$ARG_API_LINT_IGNORE_PREFIX [prefix]",
"A list of package prefixes to ignore API issues in " +
"when running with $ARG_API_LINT.",
"$ARG_MIGRATE_NULLNESS <api file>",
"Compare nullness information with the previous stable API " +
"and mark newly annotated APIs as under migration.",
ARG_WARNINGS_AS_ERRORS, "Promote all warnings to errors",
ARG_LINTS_AS_ERRORS, "Promote all API lint warnings to errors",
"$ARG_ERROR <id>", "Report issues of the given id as errors",
"$ARG_WARNING <id>", "Report issues of the given id as warnings",
"$ARG_LINT <id>", "Report issues of the given id as having lint-severity",
"$ARG_HIDE <id>", "Hide/skip issues of the given id",
"$ARG_REPORT_EVEN_IF_SUPPRESSED <file>", "Write all issues into the given file, even if suppressed (via annotation or baseline) but not if hidden (by '$ARG_HIDE')",
"$ARG_BASELINE <file>",
"Filter out any errors already reported in the given baseline file, or " +
"create if it does not already exist",
"$ARG_UPDATE_BASELINE [file]",
"Rewrite the existing baseline file with the current set of warnings. " +
"If some warnings have been fixed, this will delete them from the baseline files. If a file " +
"is provided, the updated baseline is written to the given file; otherwise the original source " +
"baseline file is updated.",
"$ARG_BASELINE_API_LINT <file> $ARG_UPDATE_BASELINE_API_LINT [file]",
"Same as $ARG_BASELINE and " +
"$ARG_UPDATE_BASELINE respectively, but used specifically for API lint issues performed by " +
"$ARG_API_LINT.",
"$ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED <file> $ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED [file]",
"Same as $ARG_BASELINE and " +
"$ARG_UPDATE_BASELINE respectively, but used specifically for API compatibility issues performed by " +
"$ARG_CHECK_COMPATIBILITY_API_RELEASED and $ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED.",
"$ARG_MERGE_BASELINE [file]",
"Like $ARG_UPDATE_BASELINE, but instead of always replacing entries " +
"in the baseline, it will merge the existing baseline with the new baseline. This is useful " +
"if $PROGRAM_NAME runs multiple times on the same source tree with different flags at different " +
"times, such as occasionally with $ARG_API_LINT.",
ARG_PASS_BASELINE_UPDATES,
"Normally, encountering error will fail the build, even when updating " +
"baselines. This flag allows you to tell $PROGRAM_NAME to continue without errors, such that " +
"all the baselines in the source tree can be updated in one go.",
ARG_DELETE_EMPTY_BASELINES,
"Whether to delete baseline files if they are updated and there is nothing " +
"to include.",
"$ARG_ERROR_MESSAGE_API_LINT <message>", "If set, $PROGRAM_NAME shows it when errors are detected in $ARG_API_LINT.",
"$ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED <message>",
"If set, $PROGRAM_NAME shows it " +
"when errors are detected in $ARG_CHECK_COMPATIBILITY_API_RELEASED and $ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED.",
"$ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_CURRENT <message>",
"If set, $PROGRAM_NAME shows it " +
"when errors are detected in $ARG_CHECK_COMPATIBILITY_API_CURRENT and $ARG_CHECK_COMPATIBILITY_REMOVED_CURRENT.",
"", "\nJDiff:",
"$ARG_XML_API <file>", "Like $ARG_API, but emits the API in the JDiff XML format instead",
"$ARG_CONVERT_TO_JDIFF <sig> <xml>",
"Reads in the given signature file, and writes it out " +
"in the JDiff XML format. Can be specified multiple times.",
"$ARG_CONVERT_NEW_TO_JDIFF <old> <new> <xml>",
"Reads in the given old and new api files, " +
"computes the difference, and writes out only the new parts of the API in the JDiff XML format.",
"$ARG_CONVERT_TO_V1 <sig> <sig>",
"Reads in the given signature file and writes it out as a " +
"signature file in the original v1/doclava format.",
"$ARG_CONVERT_TO_V2 <sig> <sig>",
"Reads in the given signature file and writes it out as a " +
"signature file in the new signature format, v2.",
"$ARG_CONVERT_NEW_TO_V2 <old> <new> <sig>",
"Reads in the given old and new api files, " +
"computes the difference, and writes out only the new parts of the API in the v2 format.",
"", "\nExtracting Annotations:",
"$ARG_EXTRACT_ANNOTATIONS <zipfile>",
"Extracts source annotations from the source files and writes " +
"them into the given zip file",
"$ARG_INCLUDE_ANNOTATION_CLASSES <dir>",
"Copies the given stub annotation source files into the " +
"generated stub sources; <dir> is typically $PROGRAM_NAME/stub-annotations/src/main/java/.",
"$ARG_REWRITE_ANNOTATIONS <dir/jar>",
"For a bytecode folder or output jar, rewrites the " +
"androidx annotations to be package private",
"$ARG_FORCE_CONVERT_TO_WARNING_NULLABILITY_ANNOTATIONS <package1:-package2:...>",
"On every API declared " +
"in a class referenced by the given filter, makes nullability issues appear to callers as warnings " +
"rather than errors by replacing @Nullable/@NonNull in these APIs with " +
"@RecentlyNullable/@RecentlyNonNull",
"$ARG_COPY_ANNOTATIONS <source> <dest>",
"For a source folder full of annotation " +
"sources, generates corresponding package private versions of the same annotations.",
ARG_INCLUDE_SOURCE_RETENTION,
"If true, include source-retention annotations in the stub files. Does " +
"not apply to signature files. Source retention annotations are extracted into the external " +
"annotations files instead.",
"", "\nInjecting API Levels:",
"$ARG_APPLY_API_LEVELS <api-versions.xml>",
"Reads an XML file containing API level descriptions " +
"and merges the information into the documentation",
"", "\nExtracting API Levels:",
"$ARG_GENERATE_API_LEVELS <xmlfile>",
"Reads android.jar SDK files and generates an XML file recording " +
"the API level for each class, method and field",
"$ARG_ANDROID_JAR_PATTERN <pattern>",
"Patterns to use to locate Android JAR files. The default " +
"is \$ANDROID_HOME/platforms/android-%/android.jar.",
ARG_FIRST_VERSION, "Sets the first API level to generate an API database from; usually 1",
ARG_CURRENT_VERSION, "Sets the current API level of the current source code",
ARG_CURRENT_CODENAME, "Sets the code name for the current source code",
ARG_CURRENT_JAR, "Points to the current API jar, if any",
"", "\nSandboxing:",
ARG_NO_IMPLICIT_ROOT,
"Disable implicit root directory detection. " +
"Otherwise, $PROGRAM_NAME adds in source roots implied by the source files",
"$ARG_STRICT_INPUT_FILES <file>",
"Do not read files that are not explicitly specified in the command line. " +
"All violations are written to the given file. Reads on directories are always allowed, but " +
"$PROGRAM_NAME still tracks reads on directories that are not specified in the command line, " +
"and write them to the file.",
"$ARG_STRICT_INPUT_FILES_WARN <file>",
"Warn when files not explicitly specified on the command line are " +
"read. All violations are written to the given file. Reads on directories not specified in the command " +
"line are allowed but also logged.",
"$ARG_STRICT_INPUT_FILES_STACK <file>", "Same as $ARG_STRICT_INPUT_FILES but also print stacktraces.",
"$ARG_STRICT_INPUT_FILES_EXEMPT <files or dirs>",
"Used with $ARG_STRICT_INPUT_FILES. Explicitly allow " +
"access to files and/or directories (separated by `${File.pathSeparator}). Can also be " +
"@ followed by a path to a text file containing paths to the full set of files and/or directories.",
"", "\nEnvironment Variables:",
ENV_VAR_METALAVA_DUMP_ARGV,
"Set to true to have metalava emit all the arguments it was invoked with. " +
"Helpful when debugging or reproducing under a debugger what the build system is doing.",
ENV_VAR_METALAVA_PREPEND_ARGS,
"One or more arguments (concatenated by space) to insert into the " +
"command line, before the documentation flags.",
ENV_VAR_METALAVA_APPEND_ARGS,
"One or more arguments (concatenated by space) to append to the " +
"end of the command line, after the generate documentation flags."
)
val sb = StringBuilder(INDENT_WIDTH)
for (indent in 0 until INDENT_WIDTH) {
sb.append(' ')
}
val indent = sb.toString()
val formatString = "%1$-" + INDENT_WIDTH + "s%2\$s"
var i = 0
while (i < args.size) {
val arg = args[i]
val description = "\n" + args[i + 1]
if (arg.isEmpty()) {
if (colorize) {
out.println(colorized(description, TerminalColor.YELLOW))
} else {
out.println(description)
}
} else {
val output =
if (colorize) {
val colorArg = bold(arg)
val invisibleChars = colorArg.length - arg.length
// +invisibleChars: the extra chars in the above are counted but don't contribute to width
// so allow more space
val colorFormatString = "%1$-" + (INDENT_WIDTH + invisibleChars) + "s%2\$s"
wrap(
String.format(colorFormatString, colorArg, description),
MAX_LINE_WIDTH + invisibleChars, MAX_LINE_WIDTH, indent
)
} else {
wrap(
String.format(formatString, arg, description),
MAX_LINE_WIDTH, indent
)
}
// Remove trailing whitespace
val lines = output.lines()
lines.forEachIndexed { index, line ->
out.print(line.trimEnd())
if (index < lines.size - 1) {
out.println()
}
}
}
i += 2
}
}
companion object {
private fun setIssueSeverity(
id: String,
severity: Severity,
arg: String
) {
if (id.contains(",")) { // Handle being passed in multiple comma separated id's
id.split(",").forEach {
setIssueSeverity(it.trim(), severity, arg)
}
return
}
val issue = Issues.findIssueById(id)
?: Issues.findIssueByIdIgnoringCase(id)?.also {
reporter.report(
Issues.DEPRECATED_OPTION, null as File?,
"Case-insensitive issue matching is deprecated, use " +
"$arg ${it.name} instead of $arg $id"
)
} ?: throw DriverException("Unknown issue id: $arg $id")
defaultConfiguration.setSeverity(issue, severity)
}
}
}