• 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 @file:JvmName("Driver")
17 
18 package com.android.tools.metalava
19 
20 import com.android.SdkConstants.DOT_JAR
21 import com.android.SdkConstants.DOT_JAVA
22 import com.android.SdkConstants.DOT_KT
23 import com.android.SdkConstants.DOT_TXT
24 import com.android.ide.common.process.CachedProcessOutputHandler
25 import com.android.ide.common.process.DefaultProcessExecutor
26 import com.android.ide.common.process.ProcessInfoBuilder
27 import com.android.ide.common.process.ProcessOutput
28 import com.android.ide.common.process.ProcessOutputHandler
29 import com.android.tools.lint.KotlinLintAnalyzerFacade
30 import com.android.tools.lint.LintCoreApplicationEnvironment
31 import com.android.tools.lint.LintCoreProjectEnvironment
32 import com.android.tools.lint.annotations.Extractor
33 import com.android.tools.lint.checks.infrastructure.ClassName
34 import com.android.tools.lint.detector.api.assertionsEnabled
35 import com.android.tools.metalava.CompatibilityCheck.CheckRequest
36 import com.android.tools.metalava.apilevels.ApiGenerator
37 import com.android.tools.metalava.doclava1.ApiPredicate
38 import com.android.tools.metalava.doclava1.Errors
39 import com.android.tools.metalava.doclava1.FilterPredicate
40 import com.android.tools.metalava.doclava1.TextCodebase
41 import com.android.tools.metalava.model.ClassItem
42 import com.android.tools.metalava.model.Codebase
43 import com.android.tools.metalava.model.Item
44 import com.android.tools.metalava.model.PackageDocs
45 import com.android.tools.metalava.model.psi.PsiBasedCodebase
46 import com.android.tools.metalava.model.psi.packageHtmlToJavadoc
47 import com.android.tools.metalava.model.visitors.ApiVisitor
48 import com.android.utils.StdLogger
49 import com.android.utils.StdLogger.Level.ERROR
50 import com.google.common.base.Stopwatch
51 import com.google.common.collect.Lists
52 import com.google.common.io.Files
53 import com.intellij.core.CoreApplicationEnvironment
54 import com.intellij.openapi.diagnostic.DefaultLogger
55 import com.intellij.openapi.extensions.Extensions
56 import com.intellij.openapi.roots.LanguageLevelProjectExtension
57 import com.intellij.openapi.util.Disposer
58 import com.intellij.pom.java.LanguageLevel
59 import com.intellij.psi.javadoc.CustomJavadocTagProvider
60 import com.intellij.psi.javadoc.JavadocTagInfo
61 import com.intellij.util.execution.ParametersListUtil
62 import java.io.File
63 import java.io.IOException
64 import java.io.OutputStream
65 import java.io.OutputStreamWriter
66 import java.io.PrintWriter
67 import java.util.concurrent.TimeUnit
68 import java.util.concurrent.TimeUnit.SECONDS
69 import java.util.function.Predicate
70 import kotlin.text.Charsets.UTF_8
71 
72 const val PROGRAM_NAME = "metalava"
73 const val HELP_PROLOGUE = "$PROGRAM_NAME extracts metadata from source code to generate artifacts such as the " +
74     "signature files, the SDK stub files, external annotations etc."
75 
76 @Suppress("PropertyName") // Can't mark const because trimIndent() :-(
77 val BANNER: String = """
78                 _        _
79  _ __ ___   ___| |_ __ _| | __ ___   ____ _
80 | '_ ` _ \ / _ \ __/ _` | |/ _` \ \ / / _` |
81 | | | | | |  __/ || (_| | | (_| |\ V / (_| |
82 |_| |_| |_|\___|\__\__,_|_|\__,_| \_/ \__,_|
83 """.trimIndent()
84 
85 fun main(args: Array<String>) {
86     run(args, setExitCode = true)
87 }
88 
89 /**
90  * The metadata driver is a command line interface to extracting various metadata
91  * from a source tree (or existing signature files etc). Run with --help to see
92  * more details.
93  */
runnull94 fun run(
95     args: Array<String>,
96     stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)),
97     stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)),
98     setExitCode: Boolean = false
99 ): Boolean {
100 
101     if (System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) != null && !isUnderTest()
102     ) {
103         stdout.println("---Running $PROGRAM_NAME----")
104         stdout.println("pwd=${File("").absolutePath}")
105         args.forEach { arg ->
106             stdout.println("\"$arg\",")
107         }
108         stdout.println("----------------------------")
109     }
110 
111     var exitValue: Boolean
112     var exitCode = 0
113 
114     try {
115         val modifiedArgs =
116             if (args.isEmpty()) {
117                 arrayOf("--help")
118             } else {
119                 val index = args.indexOf(ARG_GENERATE_DOCUMENTATION)
120                 val prepend = envVarToArgs(ENV_VAR_METALAVA_PREPEND_ARGS)
121                 val append = envVarToArgs(ENV_VAR_METALAVA_APPEND_ARGS)
122                 if (prepend.isEmpty() && append.isEmpty()) {
123                     args
124                 } else {
125                     val newArgs =
126                         if (index != -1) {
127                             args.sliceArray(0 until index) + prepend + args.sliceArray(index until args.size) + append
128                         } else {
129                             prepend + args + append
130                         }
131                     if (System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) != null) {
132                         stdout.println("---Modified $PROGRAM_NAME arguments from environment variables ----")
133                         stdout.println("$ENV_VAR_METALAVA_PREPEND_ARGS: ${prepend.joinToString { "\"$it\"" }}")
134                         stdout.println("$ENV_VAR_METALAVA_APPEND_ARGS: ${append.joinToString { "\"$it\"" }}")
135                         newArgs.forEach { arg ->
136                             stdout.println("\"$arg\",")
137                         }
138                         stdout.println("----------------------------")
139                         stdout.flush()
140                     }
141                     newArgs
142                 }
143             }
144 
145         compatibility = Compatibility(compat = Options.useCompatMode(args))
146         options = Options(modifiedArgs, stdout, stderr)
147         processFlags()
148 
149         if (reporter.hasErrors() && !options.passBaselineUpdates) {
150             exitCode = -1
151         }
152         exitValue = true
153     } catch (e: DriverException) {
154         stdout.flush()
155         stderr.flush()
156         if (e.stderr.isNotBlank()) {
157             stderr.println("\n${e.stderr}")
158         }
159         if (e.stdout.isNotBlank()) {
160             stdout.println("\n${e.stdout}")
161         }
162         exitCode = e.exitCode
163         exitValue = false
164     } finally {
165         Disposer.dispose(LintCoreApplicationEnvironment.get().parentDisposable)
166     }
167 
168     if (options.updateBaseline) {
169         if (options.verbose) {
170             options.baseline?.dumpStats(options.stdout)
171         }
172         if (!options.quiet) {
173             stdout.println("$PROGRAM_NAME wrote updated baseline to ${options.baseline?.updateFile}")
174         }
175     }
176     options.baseline?.close()
177 
178     stdout.flush()
179     stderr.flush()
180 
181     if (setExitCode) {
182         exit(exitCode)
183     }
184 
185     return exitValue
186 }
187 
188 /**
189  * Given an environment variable name pointing to a shell argument string,
190  * returns the parsed argument strings (or empty array if not set)
191  */
envVarToArgsnull192 private fun envVarToArgs(varName: String): Array<String> {
193     val value = System.getenv(varName) ?: return emptyArray()
194     return ParametersListUtil.parse(value).toTypedArray()
195 }
196 
exitnull197 private fun exit(exitCode: Int = 0) {
198     if (options.verbose) {
199         options.stdout.println("$PROGRAM_NAME exiting with exit code $exitCode")
200     }
201     options.stdout.flush()
202     options.stderr.flush()
203     System.exit(exitCode)
204 }
205 
processFlagsnull206 private fun processFlags() {
207     val stopwatch = Stopwatch.createStarted()
208 
209     processNonCodebaseFlags()
210 
211     val codebase =
212         if (options.sources.size == 1 && options.sources[0].path.endsWith(DOT_TXT)) {
213             SignatureFileLoader.load(
214                 file = options.sources[0],
215                 kotlinStyleNulls = options.inputKotlinStyleNulls
216             )
217         } else if (options.apiJar != null) {
218             loadFromJarFile(options.apiJar!!)
219         } else if (options.sources.size == 1 && options.sources[0].path.endsWith(DOT_JAR)) {
220             loadFromJarFile(options.sources[0])
221         } else if (options.sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
222             loadFromSources()
223         } else {
224             return
225         }
226     options.manifest?.let { codebase.manifest = it }
227 
228     if (options.verbose) {
229         options.stdout.println("\n$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(TimeUnit.SECONDS)} seconds")
230     }
231 
232     options.subtractApi?.let {
233         subtractApi(codebase, it)
234     }
235 
236     val androidApiLevelXml = options.generateApiLevelXml
237     val apiLevelJars = options.apiLevelJars
238     if (androidApiLevelXml != null && apiLevelJars != null) {
239         progress("\nGenerating API levels XML descriptor file, ${androidApiLevelXml.name}: ")
240         ApiGenerator.generate(apiLevelJars, androidApiLevelXml, codebase)
241     }
242 
243     if ((options.stubsDir != null || options.docStubsDir != null) && codebase.supportsDocumentation()) {
244         progress("\nEnhancing docs: ")
245         val docAnalyzer = DocAnalyzer(codebase)
246         docAnalyzer.enhance()
247 
248         val applyApiLevelsXml = options.applyApiLevelsXml
249         if (applyApiLevelsXml != null) {
250             progress("\nApplying API levels")
251             docAnalyzer.applyApiLevels(applyApiLevelsXml)
252         }
253     }
254 
255     // Generate the documentation stubs *before* we migrate nullness information.
256     options.docStubsDir?.let {
257         createStubFiles(
258             it, codebase, docStubs = true,
259             writeStubList = options.docStubsSourceList != null
260         )
261     }
262 
263     // Based on the input flags, generates various output files such
264     // as signature files and/or stubs files
265     options.apiFile?.let { apiFile ->
266         val apiType = ApiType.PUBLIC_API
267         val apiEmit = apiType.getEmitFilter()
268         val apiReference = apiType.getReferenceFilter()
269 
270         createReportFile(codebase, apiFile, "API") { printWriter ->
271             SignatureWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
272         }
273     }
274 
275     options.dexApiFile?.let { apiFile ->
276         val apiFilter = FilterPredicate(ApiPredicate())
277         val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
278         val apiReference = ApiPredicate(ignoreShown = true)
279         val dexApiEmit = memberIsNotCloned.and(apiFilter)
280 
281         createReportFile(
282             codebase, apiFile, "DEX API"
283         ) { printWriter -> DexApiWriter(printWriter, dexApiEmit, apiReference) }
284     }
285 
286     options.apiXmlFile?.let { apiFile ->
287         val apiType = ApiType.PUBLIC_API
288         val apiEmit = apiType.getEmitFilter()
289         val apiReference = apiType.getReferenceFilter()
290 
291         createReportFile(codebase, apiFile, "XML API") { printWriter ->
292             JDiffXmlWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
293         }
294     }
295 
296     options.dexApiMappingFile?.let { apiFile ->
297         val apiType = ApiType.ALL
298         val apiEmit = apiType.getEmitFilter()
299         val apiReference = apiType.getReferenceFilter()
300 
301         createReportFile(
302             codebase, apiFile, "DEX API Mapping"
303         ) { printWriter ->
304             DexApiWriter(
305                 printWriter, apiEmit, apiReference,
306                 membersOnly = true,
307                 includePositions = true
308             )
309         }
310     }
311 
312     options.removedApiFile?.let { apiFile ->
313         val unfiltered = codebase.original ?: codebase
314 
315         val apiType = ApiType.REMOVED
316         val removedEmit = apiType.getEmitFilter()
317         val removedReference = apiType.getReferenceFilter()
318 
319         createReportFile(unfiltered, apiFile, "removed API") { printWriter ->
320             SignatureWriter(printWriter, removedEmit, removedReference, codebase.original != null)
321         }
322     }
323 
324     options.removedDexApiFile?.let { apiFile ->
325         val unfiltered = codebase.original ?: codebase
326 
327         val removedFilter = FilterPredicate(ApiPredicate(matchRemoved = true))
328         val removedReference = ApiPredicate(ignoreShown = true, ignoreRemoved = true)
329         val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
330         val removedDexEmit = memberIsNotCloned.and(removedFilter)
331 
332         createReportFile(
333             unfiltered, apiFile, "removed DEX API"
334         ) { printWriter -> DexApiWriter(printWriter, removedDexEmit, removedReference) }
335     }
336 
337     options.privateApiFile?.let { apiFile ->
338         val apiType = ApiType.PRIVATE
339         val privateEmit = apiType.getEmitFilter()
340         val privateReference = apiType.getReferenceFilter()
341 
342         createReportFile(codebase, apiFile, "private API") { printWriter ->
343             SignatureWriter(printWriter, privateEmit, privateReference, codebase.original != null)
344         }
345     }
346 
347     options.privateDexApiFile?.let { apiFile ->
348         val apiFilter = FilterPredicate(ApiPredicate())
349         val privateEmit = apiFilter.negate()
350         val privateReference = Predicate<Item> { true }
351 
352         createReportFile(
353             codebase, apiFile, "private DEX API"
354         ) { printWriter ->
355             DexApiWriter(
356                 printWriter, privateEmit, privateReference, inlineInheritedFields = false
357             )
358         }
359     }
360 
361     options.proguard?.let { proguard ->
362         val apiEmit = FilterPredicate(ApiPredicate())
363         val apiReference = ApiPredicate(ignoreShown = true)
364         createReportFile(
365             codebase, proguard, "Proguard file"
366         ) { printWriter -> ProguardWriter(printWriter, apiEmit, apiReference) }
367     }
368 
369     options.sdkValueDir?.let { dir ->
370         dir.mkdirs()
371         SdkFileWriter(codebase, dir).generate()
372     }
373 
374     for (check in options.compatibilityChecks) {
375         checkCompatibility(codebase, check)
376     }
377 
378     val previousApiFile = options.migrateNullsFrom
379     if (previousApiFile != null) {
380         val previous =
381             if (previousApiFile.path.endsWith(DOT_JAR)) {
382                 loadFromJarFile(previousApiFile)
383             } else {
384                 SignatureFileLoader.load(
385                     file = previousApiFile,
386                     kotlinStyleNulls = options.inputKotlinStyleNulls
387                 )
388             }
389 
390         // If configured, checks for newly added nullness information compared
391         // to the previous stable API and marks the newly annotated elements
392         // as migrated (which will cause the Kotlin compiler to treat problems
393         // as warnings instead of errors
394 
395         migrateNulls(codebase, previous)
396 
397         previous.dispose()
398     }
399 
400     // Now that we've migrated nullness information we can proceed to write non-doc stubs, if any.
401 
402     options.stubsDir?.let {
403         createStubFiles(
404             it, codebase, docStubs = false,
405             writeStubList = options.stubsSourceList != null
406         )
407 
408         val stubAnnotations = options.copyStubAnnotationsFrom
409         if (stubAnnotations != null) {
410             // Support pointing to both stub-annotations and stub-annotations/src/main/java
411             val src = File(stubAnnotations, "src${File.separator}main${File.separator}java")
412             val source = if (src.isDirectory) src else stubAnnotations
413             source.listFiles()?.forEach { file ->
414                 RewriteAnnotations().copyAnnotations(codebase, file, File(it, file.name))
415             }
416         }
417     }
418 
419     if (options.docStubsDir == null && options.stubsDir == null) {
420         val writeStubsFile: (File) -> Unit = { file ->
421             val root = File("").absoluteFile
422             val sources = options.sources
423             val rootPath = root.path
424             val contents = sources.joinToString(" ") {
425                 val path = it.path
426                 if (path.startsWith(rootPath)) {
427                     path.substring(rootPath.length)
428                 } else {
429                     path
430                 }
431             }
432             file.writeText(contents)
433         }
434         options.stubsSourceList?.let(writeStubsFile)
435         options.docStubsSourceList?.let(writeStubsFile)
436     }
437     options.externalAnnotations?.let { extractAnnotations(codebase, it) }
438     progress("\n")
439 
440     // Coverage stats?
441     if (options.dumpAnnotationStatistics) {
442         progress("\nMeasuring annotation statistics: ")
443         AnnotationStatistics(codebase).count()
444     }
445     if (options.annotationCoverageOf.isNotEmpty()) {
446         progress("\nMeasuring annotation coverage: ")
447         AnnotationStatistics(codebase).measureCoverageOf(options.annotationCoverageOf)
448     }
449 
450     if (options.verbose) {
451         val packageCount = codebase.size()
452         options.stdout.println("\n$PROGRAM_NAME finished handling $packageCount packages in $stopwatch")
453         options.stdout.flush()
454     }
455 
456     invokeDocumentationTool()
457 }
458 
subtractApinull459 fun subtractApi(codebase: Codebase, subtractApiFile: File) {
460     val path = subtractApiFile.path
461     val oldCodebase =
462         when {
463             path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile)
464             path.endsWith(DOT_JAR) -> loadFromJarFile(subtractApiFile)
465             else -> throw DriverException("Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}")
466         }
467 
468     CodebaseComparator().compare(object : ComparisonVisitor() {
469         override fun compare(old: ClassItem, new: ClassItem) {
470             new.included = false
471             new.emit = false
472         }
473     }, oldCodebase, codebase, ApiType.ALL.getReferenceFilter())
474 }
475 
processNonCodebaseFlagsnull476 fun processNonCodebaseFlags() {
477     // --copy-annotations?
478     val privateAnnotationsSource = options.privateAnnotationsSource
479     val privateAnnotationsTarget = options.privateAnnotationsTarget
480     if (privateAnnotationsSource != null && privateAnnotationsTarget != null) {
481         val rewrite = RewriteAnnotations()
482         // Support pointing to both stub-annotations and stub-annotations/src/main/java
483         val src = File(privateAnnotationsSource, "src${File.separator}main${File.separator}java")
484         val source = if (src.isDirectory) src else privateAnnotationsSource
485         source.listFiles()?.forEach { file ->
486             rewrite.modifyAnnotationSources(null, file, File(privateAnnotationsTarget, file.name))
487         }
488     }
489 
490     // --rewrite-annotations?
491     options.rewriteAnnotations?.let { RewriteAnnotations().rewriteAnnotations(it) }
492 
493     // Convert android.jar files?
494     options.androidJarSignatureFiles?.let { root ->
495         // Generate API signature files for all the historical JAR files
496         ConvertJarsToSignatureFiles().convertJars(root)
497     }
498 
499     for (convert in options.convertToXmlFiles) {
500         val signatureApi = SignatureFileLoader.load(
501             file = convert.fromApiFile,
502             kotlinStyleNulls = options.inputKotlinStyleNulls
503         )
504 
505         val apiType = ApiType.ALL
506         val apiEmit = apiType.getEmitFilter()
507         val strip = convert.strip
508         val apiReference = if (strip) apiType.getEmitFilter() else apiType.getReferenceFilter()
509         val baseFile = convert.baseApiFile
510 
511         val outputApi =
512             if (baseFile != null) {
513                 // Convert base on a diff
514                 val baseApi = SignatureFileLoader.load(
515                     file = baseFile,
516                     kotlinStyleNulls = options.inputKotlinStyleNulls
517                 )
518 
519                 val includeFields =
520                     if (convert.outputFormat == FileFormat.V2) true else compatibility.includeFieldsInApiDiff
521                 TextCodebase.computeDelta(baseFile, baseApi, signatureApi, includeFields)
522             } else {
523                 signatureApi
524             }
525 
526         if (outputApi.isEmpty() && baseFile != null && compatibility.compat) {
527             // doclava compatibility: emits error warning instead of emitting empty <api/> element
528             options.stdout.println("No API change detected, not generating diff")
529         } else {
530             val output = convert.outputFile
531             if (convert.outputFormat == FileFormat.JDIFF) {
532                 // See JDiff's XMLToAPI#nameAPI
533                 val apiName = convert.outputFile.nameWithoutExtension.replace(' ', '_')
534                 createReportFile(outputApi, output, "JDiff File") { printWriter ->
535                     JDiffXmlWriter(printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip, apiName)
536                 }
537             } else {
538                 val prevOptions = options
539                 val prevCompatibility = compatibility
540                 try {
541                     when (convert.outputFormat) {
542                         FileFormat.V1 -> {
543                             compatibility = Compatibility(true)
544                             options = Options(emptyArray(), options.stdout, options.stderr)
545                             FileFormat.V1.configureOptions(options, compatibility)
546                         }
547                         FileFormat.V2 -> {
548                             compatibility = Compatibility(false)
549                             options = Options(emptyArray(), options.stdout, options.stderr)
550                             FileFormat.V2.configureOptions(options, compatibility)
551                         }
552                         else -> error("Unsupported format ${convert.outputFormat}")
553                     }
554 
555                     createReportFile(outputApi, output, "Diff API File") { printWriter ->
556                         SignatureWriter(
557                             printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip
558                         )
559                     }
560                 } finally {
561                     options = prevOptions
562                     compatibility = prevCompatibility
563                 }
564             }
565         }
566     }
567 }
568 
569 /**
570  * Checks compatibility of the given codebase with the codebase described in the
571  * signature file.
572  */
checkCompatibilitynull573 fun checkCompatibility(
574     codebase: Codebase,
575     check: CheckRequest
576 ) {
577     val signatureFile = check.file
578 
579     val current =
580         if (signatureFile.path.endsWith(DOT_JAR)) {
581             loadFromJarFile(signatureFile)
582         } else {
583             SignatureFileLoader.load(
584                 file = signatureFile,
585                 kotlinStyleNulls = options.inputKotlinStyleNulls
586             )
587         }
588 
589     if (current is TextCodebase && current.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
590         throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${current.format} without analyzing current codebase with $ARG_FORMAT=${current.format}")
591     }
592 
593     var base: Codebase? = null
594     val releaseType = check.releaseType
595     val apiType = check.apiType
596 
597     // If diffing with a system-api or test-api (or other signature-based codebase
598     // generated from --show-annotations), the API is partial: it's only listing
599     // the API that is *different* from the base API. This really confuses the
600     // codebase comparison when diffing with a complete codebase, since it looks like
601     // many classes and members have been added and removed. Therefore, the comparison
602     // is simpler if we just make the comparison with the same generated signature
603     // file. If we've only emitted one for the new API, use it directly, if not, generate
604     // it first
605     val new =
606         if (check.codebase != null) {
607             SignatureFileLoader.load(
608                 file = check.codebase,
609                 kotlinStyleNulls = options.inputKotlinStyleNulls
610             )
611         } else if (options.showAnnotations.isNotEmpty() || apiType != ApiType.PUBLIC_API) {
612             val apiFile = apiType.getSignatureFile(codebase, "compat-check-signatures-$apiType")
613 
614             // Fast path: if the signature files are identical, we're already good!
615             if (apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
616                 return
617             }
618 
619             base = codebase
620 
621             SignatureFileLoader.load(
622                 file = apiFile,
623                 kotlinStyleNulls = options.inputKotlinStyleNulls
624             )
625         } else {
626             // Fast path: if we've already generated a signature file and it's identical, we're good!
627             val apiFile = options.apiFile
628             if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
629                 return
630             }
631 
632             codebase
633         }
634 
635     // If configured, compares the new API with the previous API and reports
636     // any incompatibilities.
637     CompatibilityCheck.checkCompatibility(new, current, releaseType, apiType, base)
638 
639     // Make sure the text files are identical too? (only applies for *current.txt;
640     // last-released is expected to differ)
641     if (releaseType == ReleaseType.DEV && !options.allowCompatibleDifferences) {
642         val apiFile = if (new.location.isFile)
643             new.location
644         else
645             apiType.getSignatureFile(codebase, "compat-diff-signatures-$apiType")
646 
647         fun getCanonicalSignatures(file: File): String {
648             // Get rid of trailing newlines and Windows line endings
649             val text = file.readText(UTF_8)
650             return text.replace("\r\n", "\n").trim()
651         }
652         val currentTxt = getCanonicalSignatures(signatureFile)
653         val newTxt = getCanonicalSignatures(apiFile)
654         if (newTxt != currentTxt) {
655             val diff = getNativeDiff(signatureFile, apiFile) ?: getDiff(currentTxt, newTxt, 1)
656             val updateApi = if (isBuildingAndroid())
657                 "Run make update-api to update.\n"
658             else
659                 ""
660             val message =
661                 """
662                     Aborting: Your changes have resulted in differences in the signature file
663                     for the ${apiType.displayName} API.
664 
665                     The changes may be compatible, but the signature file needs to be updated.
666                     $updateApi
667                     Diffs:
668                 """.trimIndent() + "\n" + diff
669 
670             throw DriverException(exitCode = -1, stderr = message)
671         }
672     }
673 }
674 
createTempFilenull675 fun createTempFile(namePrefix: String, nameSuffix: String): File {
676     val tempFolder = options.tempFolder
677     return if (tempFolder != null) {
678         val preferred = File(tempFolder, namePrefix + nameSuffix)
679         if (!preferred.exists()) {
680             return preferred
681         }
682         File.createTempFile(namePrefix, nameSuffix, tempFolder)
683     } else {
684         File.createTempFile(namePrefix, nameSuffix)
685     }
686 }
687 
invokeDocumentationToolnull688 fun invokeDocumentationTool() {
689     if (options.noDocs) {
690         return
691     }
692 
693     val args = options.invokeDocumentationToolArguments
694     if (args.isNotEmpty()) {
695         if (!options.quiet) {
696             options.stdout.println(
697                 "Invoking external documentation tool ${args[0]} with arguments\n\"${
698                 args.slice(1 until args.size).joinToString(separator = "\",\n\"") { it }}\""
699             )
700             options.stdout.flush()
701         }
702 
703         val builder = ProcessInfoBuilder()
704 
705         builder.setExecutable(File(args[0]))
706         builder.addArgs(args.slice(1 until args.size))
707 
708         val processOutputHandler =
709             if (options.quiet) {
710                 CachedProcessOutputHandler()
711             } else {
712                 object : ProcessOutputHandler {
713                     override fun handleOutput(processOutput: ProcessOutput?) {
714                     }
715 
716                     override fun createOutput(): ProcessOutput {
717                         val out = PrintWriterOutputStream(options.stdout)
718                         val err = PrintWriterOutputStream(options.stderr)
719                         return object : ProcessOutput {
720                             override fun getStandardOutput(): OutputStream {
721                                 return out
722                             }
723 
724                             override fun getErrorOutput(): OutputStream {
725                                 return err
726                             }
727 
728                             override fun close() {
729                                 out.flush()
730                                 err.flush()
731                             }
732                         }
733                     }
734                 }
735             }
736 
737         val result = DefaultProcessExecutor(StdLogger(ERROR))
738             .execute(builder.createProcess(), processOutputHandler)
739 
740         val exitCode = result.exitValue
741         if (!options.quiet) {
742             options.stdout.println("${args[0]} finished with exitCode $exitCode")
743             options.stdout.flush()
744         }
745         if (exitCode != 0) {
746             val stdout = if (processOutputHandler is CachedProcessOutputHandler)
747                 processOutputHandler.processOutput.standardOutputAsString
748             else ""
749             val stderr = if (processOutputHandler is CachedProcessOutputHandler)
750                 processOutputHandler.processOutput.errorOutputAsString
751             else ""
752             throw DriverException(
753                 stdout = "Invoking documentation tool ${args[0]} failed with exit code $exitCode\n$stdout",
754                 stderr = stderr,
755                 exitCode = exitCode
756             )
757         }
758     }
759 }
760 
761 class PrintWriterOutputStream(private val writer: PrintWriter) : OutputStream() {
762 
writenull763     override fun write(b: ByteArray) {
764         writer.write(String(b, UTF_8))
765     }
766 
writenull767     override fun write(b: Int) {
768         write(byteArrayOf(b.toByte()), 0, 1)
769     }
770 
writenull771     override fun write(b: ByteArray, off: Int, len: Int) {
772         writer.write(String(b, off, len, UTF_8))
773     }
774 
flushnull775     override fun flush() {
776         writer.flush()
777     }
778 
closenull779     override fun close() {
780         writer.close()
781     }
782 }
783 
migrateNullsnull784 private fun migrateNulls(codebase: Codebase, previous: Codebase) {
785     previous.compareWith(NullnessMigration(), codebase, ApiPredicate())
786 }
787 
loadFromSourcesnull788 private fun loadFromSources(): Codebase {
789     progress("\nProcessing sources: ")
790 
791     val sources = if (options.sources.isEmpty()) {
792         if (options.verbose) {
793             options.stdout.println("No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})")
794         }
795         gatherSources(options.sourcePath)
796     } else {
797         options.sources
798     }
799 
800     progress("\nReading Codebase: ")
801     val codebase = parseSources(sources, "Codebase loaded from source folders")
802 
803     progress("\nAnalyzing API: ")
804 
805     val analyzer = ApiAnalyzer(codebase)
806     analyzer.mergeExternalInclusionAnnotations()
807     analyzer.computeApi()
808     analyzer.mergeExternalQualifierAnnotations()
809     options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
810     options.nullabilityAnnotationsValidator?.report()
811     analyzer.handleStripping()
812 
813     if (options.checkKotlinInterop) {
814         KotlinInteropChecks().check(codebase)
815     }
816 
817     // General API checks for Android APIs
818     AndroidApiChecks().check(codebase)
819 
820     if (options.checkApi) {
821         val localTimer = Stopwatch.createStarted()
822         // See if we should provide a previous codebase to provide a delta from?
823         val previousApiFile = options.checkApiBaselineApiFile
824         val previous =
825             when {
826                 previousApiFile == null -> null
827                 previousApiFile.path.endsWith(DOT_JAR) -> loadFromJarFile(previousApiFile)
828                 else -> SignatureFileLoader.load(
829                     file = previousApiFile,
830                     kotlinStyleNulls = options.inputKotlinStyleNulls
831                 )
832             }
833         ApiLint.check(codebase, previous)
834         progress("\n$PROGRAM_NAME ran api-lint in ${localTimer.elapsed(SECONDS)} seconds")
835     }
836 
837     val filterEmit = ApiPredicate(ignoreShown = true, ignoreRemoved = false)
838     val apiEmit = ApiPredicate(ignoreShown = true)
839     val apiReference = ApiPredicate(ignoreShown = true)
840 
841     // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary
842     progress("\nInsert missing stubs methods: ")
843     analyzer.generateInheritedStubs(apiEmit, apiReference)
844 
845     // Compute default constructors (and add missing package private constructors
846     // to make stubs compilable if necessary)
847     if (options.stubsDir != null || options.docStubsDir != null) {
848         progress("\nInsert missing constructors: ")
849         analyzer.addConstructors(filterEmit)
850     }
851 
852     progress("\nPerforming misc API checks: ")
853     analyzer.performChecks()
854 
855     return codebase
856 }
857 
858 /**
859  * Returns a codebase initialized from the given Java or Kotlin source files, with the given
860  * description. The codebase will use a project environment initialized according to the current
861  * [options].
862  */
parseSourcesnull863 internal fun parseSources(
864     sources: List<File>,
865     description: String,
866     sourcePath: List<File> = options.sourcePath,
867     classpath: List<File> = options.classpath,
868     javaLanguageLevel: LanguageLevel = options.javaLanguageLevel,
869     manifest: File? = options.manifest,
870     currentApiLevel: Int = options.currentApiLevel + if (options.currentCodeName != null) 1 else 0
871 ): PsiBasedCodebase {
872     val projectEnvironment = createProjectEnvironment()
873     val project = projectEnvironment.project
874 
875     // Push language level to PSI handler
876     project.getComponent(LanguageLevelProjectExtension::class.java)?.languageLevel = javaLanguageLevel
877 
878     val joined = mutableListOf<File>()
879     joined.addAll(sourcePath.mapNotNull { if (it.path.isNotBlank()) it.absoluteFile else null })
880     joined.addAll(classpath.map { it.absoluteFile })
881     // Add in source roots implied by the source files
882     val sourceRoots = mutableListOf<File>()
883     extractRoots(sources, sourceRoots)
884     joined.addAll(sourceRoots)
885 
886     // Create project environment with those paths
887     projectEnvironment.registerPaths(joined)
888 
889     val kotlinFiles = sources.filter { it.path.endsWith(DOT_KT) }
890     val trace = KotlinLintAnalyzerFacade().analyze(kotlinFiles, joined, project)
891 
892     val rootDir = sourceRoots.firstOrNull() ?: sourcePath.firstOrNull() ?: File("").canonicalFile
893 
894     val units = Extractor.createUnitsForFiles(project, sources)
895     val packageDocs = gatherHiddenPackagesFromJavaDocs(sourcePath)
896 
897     val codebase = PsiBasedCodebase(rootDir, description)
898     codebase.initialize(project, units, packageDocs)
899     codebase.manifest = manifest
900     codebase.apiLevel = currentApiLevel
901     codebase.bindingContext = trace.bindingContext
902     return codebase
903 }
904 
loadFromJarFilenull905 fun loadFromJarFile(apiJar: File, manifest: File? = null, preFiltered: Boolean = false): Codebase {
906     val projectEnvironment = createProjectEnvironment()
907 
908     progress("\nProcessing jar file: ")
909 
910     // Create project environment with those paths
911     val project = projectEnvironment.project
912     projectEnvironment.registerPaths(listOf(apiJar))
913 
914     val kotlinFiles = emptyList<File>()
915     val trace = KotlinLintAnalyzerFacade().analyze(kotlinFiles, listOf(apiJar), project)
916 
917     val codebase = PsiBasedCodebase(apiJar, "Codebase loaded from $apiJar")
918     codebase.initialize(project, apiJar, preFiltered)
919     if (manifest != null) {
920         codebase.manifest = options.manifest
921     }
922     val apiEmit = ApiPredicate(ignoreShown = true)
923     val apiReference = ApiPredicate(ignoreShown = true)
924     val analyzer = ApiAnalyzer(codebase)
925     analyzer.mergeExternalInclusionAnnotations()
926     analyzer.computeApi()
927     analyzer.mergeExternalQualifierAnnotations()
928     options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
929     options.nullabilityAnnotationsValidator?.report()
930     analyzer.generateInheritedStubs(apiEmit, apiReference)
931     codebase.bindingContext = trace.bindingContext
932     return codebase
933 }
934 
createProjectEnvironmentnull935 private fun createProjectEnvironment(): LintCoreProjectEnvironment {
936     ensurePsiFileCapacity()
937     val appEnv = LintCoreApplicationEnvironment.get()
938     val parentDisposable = appEnv.parentDisposable
939 
940     if (!assertionsEnabled() &&
941         System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) == null &&
942         !isUnderTest()
943     ) {
944         DefaultLogger.disableStderrDumping(parentDisposable)
945     }
946 
947     val environment = LintCoreProjectEnvironment.create(parentDisposable, appEnv)
948 
949     // Missing service needed in metalava but not in lint: javadoc handling
950     environment.project.registerService(
951         com.intellij.psi.javadoc.JavadocManager::class.java,
952         com.intellij.psi.impl.source.javadoc.JavadocManagerImpl::class.java
953     )
954     environment.registerProjectExtensionPoint(JavadocTagInfo.EP_NAME,
955         com.intellij.psi.javadoc.JavadocTagInfo::class.java)
956     CoreApplicationEnvironment.registerExtensionPoint(
957         Extensions.getRootArea(), CustomJavadocTagProvider.EP_NAME, CustomJavadocTagProvider::class.java
958     )
959 
960     return environment
961 }
962 
ensurePsiFileCapacitynull963 private fun ensurePsiFileCapacity() {
964     val fileSize = System.getProperty("idea.max.intellisense.filesize")
965     if (fileSize == null) {
966         // Ensure we can handle large compilation units like android.R
967         System.setProperty("idea.max.intellisense.filesize", "100000")
968     }
969 }
970 
extractAnnotationsnull971 private fun extractAnnotations(codebase: Codebase, file: File) {
972     val localTimer = Stopwatch.createStarted()
973 
974     options.externalAnnotations?.let { outputFile ->
975         @Suppress("UNCHECKED_CAST")
976         ExtractAnnotations(
977             codebase,
978             outputFile
979         ).extractAnnotations()
980         if (options.verbose) {
981             options.stdout.print("\n$PROGRAM_NAME extracted annotations into $file in $localTimer")
982             options.stdout.flush()
983         }
984     }
985 }
986 
createStubFilesnull987 private fun createStubFiles(stubDir: File, codebase: Codebase, docStubs: Boolean, writeStubList: Boolean) {
988     // Generating stubs from a sig-file-based codebase is problematic
989     assert(codebase.supportsDocumentation())
990 
991     // Temporary bug workaround for org.chromium.arc
992     if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
993         codebase.findClass("org.chromium.mojo.bindings.Callbacks")?.hidden = true
994     }
995 
996     if (docStubs) {
997         progress("\nGenerating documentation stub files: ")
998     } else {
999         progress("\nGenerating stub files: ")
1000     }
1001 
1002     val localTimer = Stopwatch.createStarted()
1003     val prevCompatibility = compatibility
1004     if (compatibility.compat) {
1005         compatibility = Compatibility(false)
1006         // But preserve the setting for whether we want to erase throws signatures (to ensure the API
1007         // stays compatible)
1008         compatibility.useErasureInThrows = prevCompatibility.useErasureInThrows
1009     }
1010 
1011     val stubWriter =
1012         StubWriter(
1013             codebase = codebase,
1014             stubsDir = stubDir,
1015             generateAnnotations = options.generateAnnotations,
1016             preFiltered = codebase.preFiltered,
1017             docStubs = docStubs
1018         )
1019     codebase.accept(stubWriter)
1020 
1021     if (docStubs) {
1022         // Overview docs? These are generally in the empty package.
1023         codebase.findPackage("")?.let { empty ->
1024             val overview = codebase.getPackageDocs()?.getOverviewDocumentation(empty)
1025             if (overview != null && overview.isNotBlank()) {
1026                 stubWriter.writeDocOverview(empty, overview)
1027             }
1028         }
1029     }
1030 
1031     if (writeStubList) {
1032         // Optionally also write out a list of source files that were generated; used
1033         // for example to point javadoc to the stubs output to generate documentation
1034         val file = if (docStubs) {
1035             options.docStubsSourceList ?: options.stubsSourceList
1036         } else {
1037             options.stubsSourceList
1038         }
1039         file?.let {
1040             val root = File("").absoluteFile
1041             stubWriter.writeSourceList(it, root)
1042         }
1043     }
1044 
1045     compatibility = prevCompatibility
1046 
1047     progress(
1048         "\n$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
1049         localTimer.elapsed(SECONDS)} seconds"
1050     )
1051 }
1052 
progressnull1053 fun progress(message: String) {
1054     if (options.verbose) {
1055         options.stdout.print(message)
1056         options.stdout.flush()
1057     }
1058 }
1059 
createReportFilenull1060 fun createReportFile(
1061     codebase: Codebase,
1062     apiFile: File,
1063     description: String?,
1064     createVisitor: (PrintWriter) -> ApiVisitor
1065 ) {
1066     if (description != null) {
1067         progress("\nWriting $description file: ")
1068     }
1069     val localTimer = Stopwatch.createStarted()
1070     try {
1071         val writer = PrintWriter(Files.asCharSink(apiFile, UTF_8).openBufferedStream())
1072         writer.use { printWriter ->
1073             val apiWriter = createVisitor(printWriter)
1074             codebase.accept(apiWriter)
1075         }
1076     } catch (e: IOException) {
1077         reporter.report(Errors.IO_ERROR, apiFile, "Cannot open file for write.")
1078     }
1079     if (description != null && options.verbose) {
1080         options.stdout.print("\n$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds")
1081     }
1082 }
1083 
1084 /** Used for verbose output to show progress bar */
1085 private var tick = 0
1086 
1087 /** Needed for tests to ensure we don't get unpredictable behavior of "." in output */
resetTickernull1088 fun resetTicker() {
1089     tick = 0
1090 }
1091 
1092 /** Print progress */
ticknull1093 fun tick() {
1094     tick++
1095     if (tick % 100 == 0) {
1096         if (!options.verbose) {
1097             return
1098         }
1099         options.stdout.print(".")
1100         options.stdout.flush()
1101     }
1102 }
1103 
skippableDirectorynull1104 private fun skippableDirectory(file: File): Boolean = file.path.endsWith(".git") && file.name == ".git"
1105 
1106 private fun addSourceFiles(list: MutableList<File>, file: File) {
1107     if (file.isDirectory) {
1108         if (skippableDirectory(file)) {
1109             return
1110         }
1111         if (java.nio.file.Files.isSymbolicLink(file.toPath())) {
1112             reporter.report(
1113                 Errors.IGNORING_SYMLINK, file,
1114                 "Ignoring symlink during source file discovery directory traversal"
1115             )
1116             return
1117         }
1118         val files = file.listFiles()
1119         if (files != null) {
1120             for (child in files) {
1121                 addSourceFiles(list, child)
1122             }
1123         }
1124     } else {
1125         if (file.isFile && (file.path.endsWith(DOT_JAVA) || file.path.endsWith(DOT_KT))) {
1126             list.add(file)
1127         }
1128     }
1129 }
1130 
gatherSourcesnull1131 fun gatherSources(sourcePath: List<File>): List<File> {
1132     val sources = Lists.newArrayList<File>()
1133     for (file in sourcePath) {
1134         if (file.path.isBlank()) {
1135             // --source-path "" means don't search source path; use "." for pwd
1136             continue
1137         }
1138         addSourceFiles(sources, file.absoluteFile)
1139     }
1140     return sources
1141 }
1142 
addHiddenPackagesnull1143 private fun addHiddenPackages(
1144     packageToDoc: MutableMap<String, String>,
1145     packageToOverview: MutableMap<String, String>,
1146     hiddenPackages: MutableSet<String>,
1147     file: File,
1148     pkg: String
1149 ) {
1150     if (file.isDirectory) {
1151         if (skippableDirectory(file)) {
1152             return
1153         }
1154         // Ignore symbolic links during traversal
1155         if (java.nio.file.Files.isSymbolicLink(file.toPath())) {
1156             reporter.report(
1157                 Errors.IGNORING_SYMLINK, file,
1158                 "Ignoring symlink during package.html discovery directory traversal"
1159             )
1160             return
1161         }
1162         val files = file.listFiles()
1163         if (files != null) {
1164             for (child in files) {
1165                 var subPkg =
1166                     if (child.isDirectory)
1167                         if (pkg.isEmpty())
1168                             child.name
1169                         else pkg + "." + child.name
1170                     else pkg
1171 
1172                 if (subPkg.endsWith("src.main.java")) {
1173                     // It looks like the source path was incorrectly configured; make corrections here
1174                     // to ensure that we map the package.html files to the real packages.
1175                     subPkg = ""
1176                 }
1177 
1178                 addHiddenPackages(packageToDoc, packageToOverview, hiddenPackages, child, subPkg)
1179             }
1180         }
1181     } else if (file.isFile) {
1182         var javadoc = false
1183         val map = when {
1184             file.name == "package.html" -> {
1185                 javadoc = true; packageToDoc
1186             }
1187             file.name == "overview.html" -> {
1188                 packageToOverview
1189             }
1190             else -> return
1191         }
1192         var contents = Files.asCharSource(file, UTF_8).read()
1193         if (javadoc) {
1194             contents = packageHtmlToJavadoc(contents)
1195         }
1196 
1197         var realPkg = pkg
1198         // Sanity check the package; it's computed from the directory name
1199         // relative to the source path, but if the real source path isn't
1200         // passed in (and is instead some directory containing the source path)
1201         // then we compute the wrong package here. Instead, look for an adjacent
1202         // java class and pick the package from it
1203         for (sibling in file.parentFile.listFiles()) {
1204             if (sibling.path.endsWith(DOT_JAVA)) {
1205                 val javaPkg = ClassName(sibling.readText()).packageName
1206                 if (javaPkg != null) {
1207                     realPkg = pkg
1208                     break
1209                 }
1210             }
1211         }
1212 
1213         map[realPkg] = contents
1214         if (contents.contains("@hide")) {
1215             hiddenPackages.add(realPkg)
1216         }
1217     }
1218 }
1219 
gatherHiddenPackagesFromJavaDocsnull1220 private fun gatherHiddenPackagesFromJavaDocs(sourcePath: List<File>): PackageDocs {
1221     val packageComments = HashMap<String, String>(100)
1222     val overviewHtml = HashMap<String, String>(10)
1223     val hiddenPackages = HashSet<String>(100)
1224     for (file in sourcePath) {
1225         if (file.path.isBlank()) {
1226             // Ignoring empty paths, which means "no source path search". Use "." for current directory.
1227             continue
1228         }
1229         addHiddenPackages(packageComments, overviewHtml, hiddenPackages, file, "")
1230     }
1231     return PackageDocs(packageComments, overviewHtml, hiddenPackages)
1232 }
1233 
extractRootsnull1234 fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> {
1235     // Cache for each directory since computing root for a source file is
1236     // expensive
1237     val dirToRootCache = mutableMapOf<String, File>()
1238     for (file in sources) {
1239         val parent = file.parentFile ?: continue
1240         val found = dirToRootCache[parent.path]
1241         if (found != null) {
1242             continue
1243         }
1244 
1245         val root = findRoot(file) ?: continue
1246         dirToRootCache[parent.path] = root
1247 
1248         if (!sourceRoots.contains(root)) {
1249             sourceRoots.add(root)
1250         }
1251     }
1252 
1253     return sourceRoots
1254 }
1255 
1256 /**
1257  * If given a full path to a Java or Kotlin source file, produces the path to
1258  * the source root if possible.
1259  */
findRootnull1260 private fun findRoot(file: File): File? {
1261     val path = file.path
1262     if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) {
1263         val pkg = findPackage(file) ?: return null
1264         val parent = file.parentFile ?: return null
1265         val endIndex = parent.path.length - pkg.length
1266         val before = path[endIndex - 1]
1267         if (before == '/' || before == '\\') {
1268             return File(path.substring(0, endIndex))
1269         } else {
1270             reporter.report(
1271                 Errors.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name. " +
1272                     "This usually means that a source file was where the directory does not seem to match the package " +
1273                     "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
1274             )
1275         }
1276     }
1277 
1278     return null
1279 }
1280 
1281 /** Finds the package of the given Java/Kotlin source file, if possible */
findPackagenull1282 fun findPackage(file: File): String? {
1283     val source = Files.asCharSource(file, UTF_8).read()
1284     return findPackage(source)
1285 }
1286 
1287 /** Finds the package of the given Java/Kotlin source code, if possible */
findPackagenull1288 fun findPackage(source: String): String? {
1289     return ClassName(source).packageName
1290 }
1291 
1292 /** Whether metalava is running unit tests */
isUnderTestnull1293 fun isUnderTest() = java.lang.Boolean.getBoolean(ENV_VAR_METALAVA_TESTS_RUNNING)
1294 
1295 /** Whether metalava is being invoked as part of an Android platform build */
1296 fun isBuildingAndroid() = System.getenv("ANDROID_BUILD_TOP") != null && !isUnderTest()
1297