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