• 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.tools.lint.UastEnvironment
25 import com.android.tools.lint.annotations.Extractor
26 import com.android.tools.lint.checks.infrastructure.ClassName
27 import com.android.tools.lint.detector.api.assertionsEnabled
28 import com.android.tools.metalava.CompatibilityCheck.CheckRequest
29 import com.android.tools.metalava.apilevels.ApiGenerator
30 import com.android.tools.metalava.model.ClassItem
31 import com.android.tools.metalava.model.Codebase
32 import com.android.tools.metalava.model.Item
33 import com.android.tools.metalava.model.PackageDocs
34 import com.android.tools.metalava.model.psi.PsiBasedCodebase
35 import com.android.tools.metalava.model.psi.packageHtmlToJavadoc
36 import com.android.tools.metalava.model.text.TextCodebase
37 import com.android.tools.metalava.model.visitors.ApiVisitor
38 import com.android.tools.metalava.stub.StubWriter
39 import com.google.common.base.Stopwatch
40 import com.google.common.collect.Lists
41 import com.google.common.io.Files
42 import com.intellij.core.CoreApplicationEnvironment
43 import com.intellij.openapi.diagnostic.DefaultLogger
44 import com.intellij.pom.java.LanguageLevel
45 import com.intellij.psi.javadoc.CustomJavadocTagProvider
46 import com.intellij.psi.javadoc.JavadocTagInfo
47 import org.jetbrains.kotlin.config.CommonConfigurationKeys.MODULE_NAME
48 import org.jetbrains.kotlin.config.JVMConfigurationKeys
49 import org.jetbrains.kotlin.config.LanguageVersionSettings
50 import java.io.File
51 import java.io.IOException
52 import java.io.OutputStreamWriter
53 import java.io.PrintWriter
54 import java.io.StringWriter
55 import java.util.concurrent.TimeUnit.SECONDS
56 import java.util.function.Predicate
57 import kotlin.system.exitProcess
58 import kotlin.text.Charsets.UTF_8
59 
60 const val PROGRAM_NAME = "metalava"
61 const val HELP_PROLOGUE = "$PROGRAM_NAME extracts metadata from source code to generate artifacts such as the " +
62     "signature files, the SDK stub files, external annotations etc."
63 const val PACKAGE_HTML = "package.html"
64 const val OVERVIEW_HTML = "overview.html"
65 
66 @Suppress("PropertyName") // Can't mark const because trimIndent() :-(
67 val BANNER: String = """
68                 _        _
69  _ __ ___   ___| |_ __ _| | __ ___   ____ _
70 | '_ ` _ \ / _ \ __/ _` | |/ _` \ \ / / _` |
71 | | | | | |  __/ || (_| | | (_| |\ V / (_| |
72 |_| |_| |_|\___|\__\__,_|_|\__,_| \_/ \__,_|
73 """.trimIndent()
74 
75 fun main(args: Array<String>) {
76     run(args, setExitCode = true)
77 }
78 
79 internal var hasFileReadViolations = false
80 
81 /**
82  * The metadata driver is a command line interface to extracting various metadata
83  * from a source tree (or existing signature files etc). Run with --help to see
84  * more details.
85  */
runnull86 fun run(
87     originalArgs: Array<String>,
88     stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)),
89     stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)),
90     setExitCode: Boolean = false
91 ): Boolean {
92     var exitCode = 0
93 
94     try {
95         val modifiedArgs = preprocessArgv(originalArgs)
96 
97         progress("$PROGRAM_NAME started\n")
98 
99         // Dump the arguments, and maybe generate a rerun-script.
100         maybeDumpArgv(stdout, originalArgs, modifiedArgs)
101 
102         // Actual work begins here.
103         options = Options(modifiedArgs, stdout, stderr)
104 
105         maybeActivateSandbox()
106 
107         processFlags()
108 
109         if (options.allReporters.any { it.hasErrors() } && !options.passBaselineUpdates) {
110             // Repeat the errors at the end to make it easy to find the actual problems.
111             if (options.repeatErrorsMax > 0) {
112                 repeatErrors(stderr, options.allReporters, options.repeatErrorsMax)
113             }
114             exitCode = -1
115         }
116         if (hasFileReadViolations) {
117             if (options.strictInputFiles.shouldFail) {
118                 stderr.print("Error: ")
119                 exitCode = -1
120             } else {
121                 stderr.print("Warning: ")
122             }
123             stderr.println("$PROGRAM_NAME detected access to files that are not explicitly specified. See ${options.strictInputViolationsFile} for details.")
124         }
125     } catch (e: DriverException) {
126         stdout.flush()
127         stderr.flush()
128 
129         val prefix = if (e.exitCode != 0) { "Aborting: " } else { "" }
130 
131         if (e.stderr.isNotBlank()) {
132             stderr.println("\n${prefix}${e.stderr}")
133         }
134         if (e.stdout.isNotBlank()) {
135             stdout.println("\n${prefix}${e.stdout}")
136         }
137         exitCode = e.exitCode
138     } finally {
139         disposeUastEnvironment()
140     }
141 
142     // Update and close all baseline files.
143     options.allBaselines.forEach { baseline ->
144         if (options.verbose) {
145             baseline.dumpStats(options.stdout)
146         }
147         if (baseline.close()) {
148             if (!options.quiet) {
149                 stdout.println("$PROGRAM_NAME wrote updated baseline to ${baseline.updateFile}")
150             }
151         }
152     }
153 
154     options.reportEvenIfSuppressedWriter?.close()
155     options.strictInputViolationsPrintWriter?.close()
156 
157     // Show failure messages, if any.
158     options.allReporters.forEach {
159         it.writeErrorMessage(stderr)
160     }
161 
162     stdout.flush()
163     stderr.flush()
164 
165     if (setExitCode) {
166         exit(exitCode)
167     }
168 
169     return exitCode == 0
170 }
171 
exitnull172 private fun exit(exitCode: Int = 0) {
173     if (options.verbose) {
174         progress("$PROGRAM_NAME exiting with exit code $exitCode\n")
175     }
176     options.stdout.flush()
177     options.stderr.flush()
178     exitProcess(exitCode)
179 }
180 
maybeActivateSandboxnull181 private fun maybeActivateSandbox() {
182     // Set up a sandbox to detect access to files that are not explicitly specified.
183     if (options.strictInputFiles == Options.StrictInputFileMode.PERMISSIVE) {
184         return
185     }
186 
187     val writer = options.strictInputViolationsPrintWriter!!
188 
189     // Writes all violations to [Options.strictInputFiles].
190     // If Options.StrictInputFile.Mode is STRICT, then all violations on reads are logged, and the
191     // tool exits with a negative error code if there are any file read violations. Directory read
192     // violations are logged, but are considered to be a "warning" and doesn't affect the exit code.
193     // If STRICT_WARN, all violations on reads are logged similar to STRICT, but the exit code is
194     // unaffected.
195     // If STRICT_WITH_STACK, similar to STRICT, but also logs the stack trace to
196     // Options.strictInputFiles.
197     // See [FileReadSandbox] for the details.
198     FileReadSandbox.activate(object : FileReadSandbox.Listener {
199         var seen = mutableSetOf<String>()
200         override fun onViolation(absolutePath: String, isDirectory: Boolean) {
201             if (!seen.contains(absolutePath)) {
202                 val suffix = if (isDirectory) "/" else ""
203                 writer.println("$absolutePath$suffix")
204                 if (options.strictInputFiles == Options.StrictInputFileMode.STRICT_WITH_STACK) {
205                     Throwable().printStackTrace(writer)
206                 }
207                 seen.add(absolutePath)
208                 if (!isDirectory) {
209                     hasFileReadViolations = true
210                 }
211             }
212         }
213     })
214 }
215 
repeatErrorsnull216 private fun repeatErrors(writer: PrintWriter, reporters: List<Reporter>, max: Int) {
217     writer.println("Error: $PROGRAM_NAME detected the following problems:")
218     val totalErrors = reporters.sumOf { it.errorCount }
219     var remainingCap = max
220     var totalShown = 0
221     reporters.forEach {
222         val numShown = it.printErrors(writer, remainingCap)
223         remainingCap -= numShown
224         totalShown += numShown
225     }
226     if (totalShown < totalErrors) {
227         writer.println("${totalErrors - totalShown} more error(s) omitted. Search the log for 'error:' to find all of them.")
228     }
229 }
230 
processFlagsnull231 private fun processFlags() {
232     val stopwatch = Stopwatch.createStarted()
233 
234     processNonCodebaseFlags()
235 
236     val sources = options.sources
237     val codebase =
238         if (sources.isNotEmpty() && sources[0].path.endsWith(DOT_TXT)) {
239             // Make sure all the source files have .txt extensions.
240             sources.firstOrNull { !it.path.endsWith(DOT_TXT) }?. let {
241                 throw DriverException("Inconsistent input file types: The first file is of $DOT_TXT, but detected different extension in ${it.path}")
242             }
243             SignatureFileLoader.loadFiles(sources, options.inputKotlinStyleNulls)
244         } else if (options.apiJar != null) {
245             loadFromJarFile(options.apiJar!!)
246         } else if (sources.size == 1 && sources[0].path.endsWith(DOT_JAR)) {
247             loadFromJarFile(sources[0])
248         } else if (sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
249             loadFromSources()
250         } else {
251             return
252         }
253     options.manifest?.let { codebase.manifest = it }
254 
255     if (options.verbose) {
256         progress("$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(SECONDS)} seconds\n")
257     }
258 
259     options.subtractApi?.let {
260         progress("Subtracting API: ")
261         subtractApi(codebase, it)
262     }
263 
264     val androidApiLevelXml = options.generateApiLevelXml
265     val apiLevelJars = options.apiLevelJars
266     if (androidApiLevelXml != null && apiLevelJars != null) {
267         assert(options.currentApiLevel != -1)
268 
269         progress("Generating API levels XML descriptor file, ${androidApiLevelXml.name}: ")
270         ApiGenerator.generate(
271             apiLevelJars, options.firstApiLevel, options.currentApiLevel, options.isDeveloperPreviewBuild(),
272             androidApiLevelXml, codebase, options.sdkJarRoot, options.sdkInfoFile, options.removeMissingClassesInApiLevels
273         )
274     }
275 
276     if (options.docStubsDir != null || options.enhanceDocumentation) {
277         if (!codebase.supportsDocumentation()) {
278             error("Codebase does not support documentation, so it cannot be enhanced.")
279         }
280         progress("Enhancing docs: ")
281         val docAnalyzer = DocAnalyzer(codebase)
282         docAnalyzer.enhance()
283         val applyApiLevelsXml = options.applyApiLevelsXml
284         if (applyApiLevelsXml != null) {
285             progress("Applying API levels")
286             docAnalyzer.applyApiLevels(applyApiLevelsXml)
287         }
288     }
289 
290     // Generate the documentation stubs *before* we migrate nullness information.
291     options.docStubsDir?.let {
292         createStubFiles(
293             it, codebase, docStubs = true,
294             writeStubList = options.docStubsSourceList != null
295         )
296     }
297 
298     // Based on the input flags, generates various output files such
299     // as signature files and/or stubs files
300     options.apiFile?.let { apiFile ->
301         val apiType = ApiType.PUBLIC_API
302         val apiEmit = apiType.getEmitFilter()
303         val apiReference = apiType.getReferenceFilter()
304 
305         createReportFile(codebase, apiFile, "API") { printWriter ->
306             SignatureWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
307         }
308     }
309 
310     options.apiXmlFile?.let { apiFile ->
311         val apiType = ApiType.PUBLIC_API
312         val apiEmit = apiType.getEmitFilter()
313         val apiReference = apiType.getReferenceFilter()
314 
315         createReportFile(codebase, apiFile, "XML API") { printWriter ->
316             JDiffXmlWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
317         }
318     }
319 
320     options.removedApiFile?.let { apiFile ->
321         val unfiltered = codebase.original ?: codebase
322 
323         val apiType = ApiType.REMOVED
324         val removedEmit = apiType.getEmitFilter()
325         val removedReference = apiType.getReferenceFilter()
326 
327         createReportFile(unfiltered, apiFile, "removed API", options.deleteEmptyRemovedSignatures) { printWriter ->
328             SignatureWriter(printWriter, removedEmit, removedReference, codebase.original != null, options.includeSignatureFormatVersionRemoved)
329         }
330     }
331 
332     options.dexApiFile?.let { apiFile ->
333         val apiFilter = FilterPredicate(ApiPredicate())
334         val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
335         val apiReference = ApiPredicate(ignoreShown = true)
336         val dexApiEmit = memberIsNotCloned.and(apiFilter)
337 
338         createReportFile(
339             codebase, apiFile, "DEX API"
340         ) { printWriter -> DexApiWriter(printWriter, dexApiEmit, apiReference) }
341     }
342 
343     options.proguard?.let { proguard ->
344         val apiEmit = FilterPredicate(ApiPredicate())
345         val apiReference = ApiPredicate(ignoreShown = true)
346         createReportFile(
347             codebase, proguard, "Proguard file"
348         ) { printWriter -> ProguardWriter(printWriter, apiEmit, apiReference) }
349     }
350 
351     options.sdkValueDir?.let { dir ->
352         dir.mkdirs()
353         SdkFileWriter(codebase, dir).generate()
354     }
355 
356     for (check in options.compatibilityChecks) {
357         checkCompatibility(codebase, check)
358     }
359 
360     val previousApiFile = options.migrateNullsFrom
361     if (previousApiFile != null) {
362         val previous =
363             if (previousApiFile.path.endsWith(DOT_JAR)) {
364                 loadFromJarFile(previousApiFile)
365             } else {
366                 SignatureFileLoader.load(
367                     file = previousApiFile,
368                     kotlinStyleNulls = options.inputKotlinStyleNulls
369                 )
370             }
371 
372         // If configured, checks for newly added nullness information compared
373         // to the previous stable API and marks the newly annotated elements
374         // as migrated (which will cause the Kotlin compiler to treat problems
375         // as warnings instead of errors
376 
377         migrateNulls(codebase, previous)
378 
379         previous.dispose()
380     }
381 
382     convertToWarningNullabilityAnnotations(codebase, options.forceConvertToWarningNullabilityAnnotations)
383 
384     // Now that we've migrated nullness information we can proceed to write non-doc stubs, if any.
385 
386     options.stubsDir?.let {
387         createStubFiles(
388             it, codebase, docStubs = false,
389             writeStubList = options.stubsSourceList != null
390         )
391     }
392 
393     if (options.docStubsDir == null && options.stubsDir == null) {
394         val writeStubsFile: (File) -> Unit = { file ->
395             val root = File("").absoluteFile
396             val rootPath = root.path
397             val contents = sources.joinToString(" ") {
398                 val path = it.path
399                 if (path.startsWith(rootPath)) {
400                     path.substring(rootPath.length)
401                 } else {
402                     path
403                 }
404             }
405             file.writeText(contents)
406         }
407         options.stubsSourceList?.let(writeStubsFile)
408         options.docStubsSourceList?.let(writeStubsFile)
409     }
410     options.externalAnnotations?.let { extractAnnotations(codebase, it) }
411 
412     if (options.verbose) {
413         val packageCount = codebase.size()
414         progress("$PROGRAM_NAME finished handling $packageCount packages in ${stopwatch.elapsed(SECONDS)} seconds\n")
415     }
416 }
417 
subtractApinull418 fun subtractApi(codebase: Codebase, subtractApiFile: File) {
419     val path = subtractApiFile.path
420     val oldCodebase =
421         when {
422             path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile)
423             path.endsWith(DOT_JAR) -> loadFromJarFile(subtractApiFile)
424             else -> throw DriverException("Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}")
425         }
426 
427     CodebaseComparator().compare(
428         object : ComparisonVisitor() {
429             override fun compare(old: ClassItem, new: ClassItem) {
430                 new.emit = false
431             }
432         },
433         oldCodebase, codebase, ApiType.ALL.getReferenceFilter()
434     )
435 }
436 
processNonCodebaseFlagsnull437 fun processNonCodebaseFlags() {
438     // --copy-annotations?
439     val privateAnnotationsSource = options.privateAnnotationsSource
440     val privateAnnotationsTarget = options.privateAnnotationsTarget
441     if (privateAnnotationsSource != null && privateAnnotationsTarget != null) {
442         val rewrite = RewriteAnnotations()
443         // Support pointing to both stub-annotations and stub-annotations/src/main/java
444         val src = File(privateAnnotationsSource, "src${File.separator}main${File.separator}java")
445         val source = if (src.isDirectory) src else privateAnnotationsSource
446         source.listFiles()?.forEach { file ->
447             rewrite.modifyAnnotationSources(null, file, File(privateAnnotationsTarget, file.name))
448         }
449     }
450 
451     // Convert android.jar files?
452     options.androidJarSignatureFiles?.let { root ->
453         // Generate API signature files for all the historical JAR files
454         ConvertJarsToSignatureFiles().convertJars(root)
455     }
456 
457     for (convert in options.convertToXmlFiles) {
458         val signatureApi = SignatureFileLoader.load(
459             file = convert.fromApiFile,
460             kotlinStyleNulls = options.inputKotlinStyleNulls
461         )
462 
463         val apiType = ApiType.ALL
464         val apiEmit = apiType.getEmitFilter()
465         val strip = convert.strip
466         val apiReference = if (strip) apiType.getEmitFilter() else apiType.getReferenceFilter()
467         val baseFile = convert.baseApiFile
468 
469         val outputApi =
470             if (baseFile != null) {
471                 // Convert base on a diff
472                 val baseApi = SignatureFileLoader.load(
473                     file = baseFile,
474                     kotlinStyleNulls = options.inputKotlinStyleNulls
475                 )
476                 TextCodebase.computeDelta(baseFile, baseApi, signatureApi)
477             } else {
478                 signatureApi
479             }
480 
481         // See JDiff's XMLToAPI#nameAPI
482         val apiName = convert.outputFile.nameWithoutExtension.replace(' ', '_')
483         createReportFile(outputApi, convert.outputFile, "JDiff File") { printWriter ->
484             JDiffXmlWriter(printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip, apiName)
485         }
486     }
487 }
488 
489 /**
490  * Checks compatibility of the given codebase with the codebase described in the
491  * signature file.
492  */
checkCompatibilitynull493 fun checkCompatibility(
494     newCodebase: Codebase,
495     check: CheckRequest
496 ) {
497     progress("Checking API compatibility ($check): ")
498     val signatureFile = check.file
499 
500     val oldCodebase =
501         if (signatureFile.path.endsWith(DOT_JAR)) {
502             loadFromJarFile(signatureFile)
503         } else {
504             SignatureFileLoader.load(
505                 file = signatureFile,
506                 kotlinStyleNulls = options.inputKotlinStyleNulls
507             )
508         }
509 
510     if (oldCodebase is TextCodebase && oldCodebase.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
511         throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${oldCodebase.format} without analyzing current codebase with $ARG_FORMAT=${oldCodebase.format}")
512     }
513 
514     var baseApi: Codebase? = null
515 
516     val apiType = check.apiType
517 
518     if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
519         // Fast path: if we've already generated a signature file, and it's identical, we're good!
520         val apiFile = options.apiFile
521         if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
522             return
523         }
524         val baseApiFile = options.baseApiForCompatCheck
525         if (baseApiFile != null) {
526             baseApi = SignatureFileLoader.load(
527                 file = baseApiFile,
528                 kotlinStyleNulls = options.inputKotlinStyleNulls
529             )
530         }
531     } else if (options.baseApiForCompatCheck != null) {
532         // This option does not make sense with showAnnotation, as the "base" in that case
533         // is the non-annotated APIs.
534         throw DriverException(
535             ARG_CHECK_COMPATIBILITY_BASE_API +
536                 " is not compatible with --showAnnotation."
537         )
538     }
539 
540     // If configured, compares the new API with the previous API and reports
541     // any incompatibilities.
542     CompatibilityCheck.checkCompatibility(newCodebase, oldCodebase, apiType, baseApi)
543 }
544 
createTempFilenull545 fun createTempFile(namePrefix: String, nameSuffix: String): File {
546     val tempFolder = options.tempFolder
547     return if (tempFolder != null) {
548         val preferred = File(tempFolder, namePrefix + nameSuffix)
549         if (!preferred.exists()) {
550             return preferred
551         }
552         File.createTempFile(namePrefix, nameSuffix, tempFolder)
553     } else {
554         File.createTempFile(namePrefix, nameSuffix)
555     }
556 }
557 
migrateNullsnull558 private fun migrateNulls(codebase: Codebase, previous: Codebase) {
559     previous.compareWith(NullnessMigration(), codebase)
560 }
561 
convertToWarningNullabilityAnnotationsnull562 private fun convertToWarningNullabilityAnnotations(codebase: Codebase, filter: PackageFilter?) {
563     if (filter != null) {
564         // Our caller has asked for these APIs to not trigger nullness errors (only warnings) if
565         // their callers make incorrect nullness assumptions (for example, calling a function on a
566         // reference of nullable type). The way to communicate this to kotlinc is to mark these
567         // APIs as RecentlyNullable/RecentlyNonNull
568         codebase.accept(MarkPackagesAsRecent(filter))
569     }
570 }
571 
loadFromSourcesnull572 private fun loadFromSources(): Codebase {
573     progress("Processing sources: ")
574 
575     val sources = options.sources.ifEmpty {
576         if (options.verbose) {
577             options.stdout.println("No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})")
578         }
579         gatherSources(options.sourcePath)
580     }
581 
582     progress("Reading Codebase: ")
583     val codebase = parseSources(sources, "Codebase loaded from source folders")
584 
585     progress("Analyzing API: ")
586 
587     val analyzer = ApiAnalyzer(codebase)
588     analyzer.mergeExternalInclusionAnnotations()
589     analyzer.computeApi()
590 
591     val filterEmit = ApiPredicate(ignoreShown = true, ignoreRemoved = false)
592     val apiEmit = ApiPredicate(ignoreShown = true)
593     val apiReference = ApiPredicate(ignoreShown = true)
594 
595     // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary. Do
596     // this before merging annotations or performing checks on the API to ensure that these methods
597     // can have annotations added and are checked properly.
598     progress("Insert missing stubs methods: ")
599     analyzer.generateInheritedStubs(apiEmit, apiReference)
600 
601     analyzer.mergeExternalQualifierAnnotations()
602     options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
603     options.nullabilityAnnotationsValidator?.report()
604     analyzer.handleStripping()
605 
606     // General API checks for Android APIs
607     AndroidApiChecks().check(codebase)
608 
609     if (options.checkApi) {
610         progress("API Lint: ")
611         val localTimer = Stopwatch.createStarted()
612         // See if we should provide a previous codebase to provide a delta from?
613         val previousApiFile = options.checkApiBaselineApiFile
614         val previous =
615             when {
616                 previousApiFile == null -> null
617                 previousApiFile.path.endsWith(DOT_JAR) -> loadFromJarFile(previousApiFile)
618                 else -> SignatureFileLoader.load(
619                     file = previousApiFile,
620                     kotlinStyleNulls = options.inputKotlinStyleNulls
621                 )
622             }
623         val apiLintReporter = options.reporterApiLint
624         ApiLint.check(codebase, previous, apiLintReporter)
625         progress("$PROGRAM_NAME ran api-lint in ${localTimer.elapsed(SECONDS)} seconds with ${apiLintReporter.getBaselineDescription()}")
626     }
627 
628     // Compute default constructors (and add missing package private constructors
629     // to make stubs compilable if necessary). Do this after all the checks as
630     // these are not part of the API.
631     if (options.stubsDir != null || options.docStubsDir != null) {
632         progress("Insert missing constructors: ")
633         analyzer.addConstructors(filterEmit)
634     }
635 
636     progress("Performing misc API checks: ")
637     analyzer.performChecks()
638 
639     return codebase
640 }
641 
642 /**
643  * Returns a codebase initialized from the given Java or Kotlin source files, with the given
644  * description. The codebase will use a project environment initialized according to the current
645  * [options].
646  */
parseSourcesnull647 internal fun parseSources(
648     sources: List<File>,
649     description: String,
650     sourcePath: List<File> = options.sourcePath,
651     classpath: List<File> = options.classpath,
652     javaLanguageLevel: LanguageLevel = options.javaLanguageLevel,
653     kotlinLanguageLevel: LanguageVersionSettings = options.kotlinLanguageLevel,
654     manifest: File? = options.manifest
655 ): PsiBasedCodebase {
656     val sourceRoots = mutableListOf<File>()
657     sourcePath.filterTo(sourceRoots) { it.path.isNotBlank() }
658     // Add in source roots implied by the source files
659     if (options.allowImplicitRoot) {
660         extractRoots(sources, sourceRoots)
661     }
662 
663     val config = UastEnvironment.Configuration.create()
664     config.javaLanguageLevel = javaLanguageLevel
665     config.kotlinLanguageLevel = kotlinLanguageLevel
666     config.addSourceRoots(sourceRoots.map { it.absoluteFile })
667     config.addClasspathRoots(classpath.map { it.absoluteFile })
668     options.jdkHome?.let {
669         if (options.isJdkModular(it)) {
670             config.kotlinCompilerConfig.put(JVMConfigurationKeys.JDK_HOME, it)
671             config.kotlinCompilerConfig.put(JVMConfigurationKeys.NO_JDK, false)
672         }
673     }
674 
675     val environment = createProjectEnvironment(config)
676 
677     val kotlinFiles = sources.filter { it.path.endsWith(DOT_KT) }
678     environment.analyzeFiles(kotlinFiles)
679 
680     val rootDir = sourceRoots.firstOrNull() ?: sourcePath.firstOrNull() ?: File("").canonicalFile
681 
682     val units = Extractor.createUnitsForFiles(environment.ideaProject, sources)
683     val packageDocs = gatherPackageJavadoc(sources, sourceRoots)
684 
685     val codebase = PsiBasedCodebase(rootDir, description)
686     codebase.initialize(environment, units, packageDocs)
687     codebase.manifest = manifest
688     return codebase
689 }
690 
loadFromJarFilenull691 fun loadFromJarFile(apiJar: File, manifest: File? = null, preFiltered: Boolean = false): Codebase {
692     progress("Processing jar file: ")
693 
694     val config = UastEnvironment.Configuration.create()
695     config.addClasspathRoots(listOf(apiJar))
696 
697     val environment = createProjectEnvironment(config)
698     environment.analyzeFiles(emptyList()) // Initializes PSI machinery.
699 
700     val codebase = PsiBasedCodebase(apiJar, "Codebase loaded from $apiJar")
701     codebase.initialize(environment, apiJar, preFiltered)
702     if (manifest != null) {
703         codebase.manifest = options.manifest
704     }
705     val apiEmit = ApiPredicate(ignoreShown = true)
706     val apiReference = ApiPredicate(ignoreShown = true)
707     val analyzer = ApiAnalyzer(codebase)
708     analyzer.mergeExternalInclusionAnnotations()
709     analyzer.computeApi()
710     analyzer.mergeExternalQualifierAnnotations()
711     options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
712     options.nullabilityAnnotationsValidator?.report()
713     analyzer.generateInheritedStubs(apiEmit, apiReference)
714     return codebase
715 }
716 
717 internal const val METALAVA_SYNTHETIC_SUFFIX = "metalava_module"
718 
createProjectEnvironmentnull719 private fun createProjectEnvironment(config: UastEnvironment.Configuration): UastEnvironment {
720     ensurePsiFileCapacity()
721 
722     // Note: the Kotlin module name affects the naming of certain synthetic methods.
723     config.kotlinCompilerConfig.put(MODULE_NAME, METALAVA_SYNTHETIC_SUFFIX)
724 
725     val environment = UastEnvironment.create(config)
726     uastEnvironments.add(environment)
727 
728     if (!assertionsEnabled() &&
729         System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) == null &&
730         !isUnderTest()
731     ) {
732         DefaultLogger.disableStderrDumping(environment.ideaProject)
733     }
734 
735     // Missing service needed in metalava but not in lint: javadoc handling
736     environment.ideaProject.registerService(
737         com.intellij.psi.javadoc.JavadocManager::class.java,
738         com.intellij.psi.impl.source.javadoc.JavadocManagerImpl::class.java
739     )
740     CoreApplicationEnvironment.registerExtensionPoint(
741         environment.ideaProject.extensionArea, JavadocTagInfo.EP_NAME, JavadocTagInfo::class.java
742     )
743     CoreApplicationEnvironment.registerApplicationExtensionPoint(
744         CustomJavadocTagProvider.EP_NAME, CustomJavadocTagProvider::class.java
745     )
746 
747     return environment
748 }
749 
750 private val uastEnvironments = mutableListOf<UastEnvironment>()
751 
disposeUastEnvironmentnull752 private fun disposeUastEnvironment() {
753     // Codebase.dispose() is not consistently called, so we dispose the environments here too.
754     for (env in uastEnvironments) {
755         if (!env.ideaProject.isDisposed) {
756             env.dispose()
757         }
758     }
759     uastEnvironments.clear()
760     UastEnvironment.disposeApplicationEnvironment()
761 }
762 
ensurePsiFileCapacitynull763 private fun ensurePsiFileCapacity() {
764     val fileSize = System.getProperty("idea.max.intellisense.filesize")
765     if (fileSize == null) {
766         // Ensure we can handle large compilation units like android.R
767         System.setProperty("idea.max.intellisense.filesize", "100000")
768     }
769 }
770 
extractAnnotationsnull771 private fun extractAnnotations(codebase: Codebase, file: File) {
772     val localTimer = Stopwatch.createStarted()
773 
774     options.externalAnnotations?.let { outputFile ->
775         @Suppress("UNCHECKED_CAST")
776         ExtractAnnotations(
777             codebase,
778             outputFile
779         ).extractAnnotations()
780         if (options.verbose) {
781             progress("$PROGRAM_NAME extracted annotations into $file in ${localTimer.elapsed(SECONDS)} seconds\n")
782         }
783     }
784 }
785 
createStubFilesnull786 private fun createStubFiles(stubDir: File, codebase: Codebase, docStubs: Boolean, writeStubList: Boolean) {
787     if (codebase is TextCodebase) {
788         if (options.verbose) {
789             options.stdout.println(
790                 "Generating stubs from text based codebase is an experimental feature. " +
791                     "It is not guaranteed that stubs generated from text based codebase are " +
792                     "class level equivalent to the stubs generated from source files. "
793             )
794         }
795     }
796 
797     // Temporary bug workaround for org.chromium.arc
798     if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
799         codebase.findClass("org.chromium.mojo.bindings.Callbacks")?.hidden = true
800     }
801 
802     if (docStubs) {
803         progress("Generating documentation stub files: ")
804     } else {
805         progress("Generating stub files: ")
806     }
807 
808     val localTimer = Stopwatch.createStarted()
809 
810     val stubWriter =
811         StubWriter(
812             codebase = codebase,
813             stubsDir = stubDir,
814             generateAnnotations = options.generateAnnotations,
815             preFiltered = codebase.preFiltered,
816             docStubs = docStubs
817         )
818     codebase.accept(stubWriter)
819 
820     if (docStubs) {
821         // Overview docs? These are generally in the empty package.
822         codebase.findPackage("")?.let { empty ->
823             val overview = codebase.getPackageDocs()?.getOverviewDocumentation(empty)
824             if (overview != null && overview.isNotBlank()) {
825                 stubWriter.writeDocOverview(empty, overview)
826             }
827         }
828     }
829 
830     if (writeStubList) {
831         // Optionally also write out a list of source files that were generated; used
832         // for example to point javadoc to the stubs output to generate documentation
833         val file = if (docStubs) {
834             options.docStubsSourceList ?: options.stubsSourceList
835         } else {
836             options.stubsSourceList
837         }
838         file?.let {
839             val root = File("").absoluteFile
840             stubWriter.writeSourceList(it, root)
841         }
842     }
843 
844     progress(
845         "$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
846         localTimer.elapsed(SECONDS)} seconds\n"
847     )
848 }
849 
createReportFilenull850 fun createReportFile(
851     codebase: Codebase,
852     apiFile: File,
853     description: String?,
854     deleteEmptyFiles: Boolean = false,
855     createVisitor: (PrintWriter) -> ApiVisitor
856 ) {
857     if (description != null) {
858         progress("Writing $description file: ")
859     }
860     val localTimer = Stopwatch.createStarted()
861     try {
862         val stringWriter = StringWriter()
863         val writer = PrintWriter(stringWriter)
864         writer.use { printWriter ->
865             val apiWriter = createVisitor(printWriter)
866             codebase.accept(apiWriter)
867         }
868         val text = stringWriter.toString()
869         if (text.isNotEmpty() || !deleteEmptyFiles) {
870             apiFile.writeText(text)
871         }
872     } catch (e: IOException) {
873         reporter.report(Issues.IO_ERROR, apiFile, "Cannot open file for write.")
874     }
875     if (description != null && options.verbose) {
876         progress("$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds\n")
877     }
878 }
879 
skippableDirectorynull880 private fun skippableDirectory(file: File): Boolean = file.path.endsWith(".git") && file.name == ".git"
881 
882 private fun addSourceFiles(list: MutableList<File>, file: File) {
883     if (file.isDirectory) {
884         if (skippableDirectory(file)) {
885             return
886         }
887         if (java.nio.file.Files.isSymbolicLink(file.toPath())) {
888             reporter.report(
889                 Issues.IGNORING_SYMLINK, file,
890                 "Ignoring symlink during source file discovery directory traversal"
891             )
892             return
893         }
894         val files = file.listFiles()
895         if (files != null) {
896             for (child in files) {
897                 addSourceFiles(list, child)
898             }
899         }
900     } else if (file.isFile) {
901         when {
902             file.name.endsWith(DOT_JAVA) ||
903                 file.name.endsWith(DOT_KT) ||
904                 file.name.equals(PACKAGE_HTML) ||
905                 file.name.equals(OVERVIEW_HTML) -> list.add(file)
906         }
907     }
908 }
909 
gatherSourcesnull910 fun gatherSources(sourcePath: List<File>): List<File> {
911     val sources = Lists.newArrayList<File>()
912     for (file in sourcePath) {
913         if (file.path.isBlank()) {
914             // --source-path "" means don't search source path; use "." for pwd
915             continue
916         }
917         addSourceFiles(sources, file.absoluteFile)
918     }
919     return sources.sortedWith(compareBy { it.name })
920 }
921 
gatherPackageJavadocnull922 private fun gatherPackageJavadoc(sources: List<File>, sourceRoots: List<File>): PackageDocs {
923     val packageComments = HashMap<String, String>(100)
924     val overviewHtml = HashMap<String, String>(10)
925     val hiddenPackages = HashSet<String>(100)
926     val sortedSourceRoots = sourceRoots.sortedBy { -it.name.length }
927     for (file in sources) {
928         var javadoc = false
929         val map = when (file.name) {
930             PACKAGE_HTML -> {
931                 javadoc = true; packageComments
932             }
933             OVERVIEW_HTML -> {
934                 overviewHtml
935             }
936             else -> continue
937         }
938         var contents = Files.asCharSource(file, UTF_8).read()
939         if (javadoc) {
940             contents = packageHtmlToJavadoc(contents)
941         }
942 
943         // Figure out the package: if there is a java file in the same directory, get the package
944         // name from the java file. Otherwise, guess from the directory path + source roots.
945         // NOTE: This causes metalava to read files other than the ones explicitly passed to it.
946         var pkg = file.parentFile?.listFiles()
947             ?.filter { it.name.endsWith(DOT_JAVA) }
948             ?.asSequence()?.mapNotNull { findPackage(it) }
949             ?.firstOrNull()
950         if (pkg == null) {
951             // Strip the longest prefix source root.
952             val prefix = sortedSourceRoots.firstOrNull { file.startsWith(it) }?.path ?: ""
953             pkg = file.parentFile.path.substring(prefix.length).trim('/').replace("/", ".")
954         }
955         map[pkg] = contents
956         if (contents.contains("@hide")) {
957             hiddenPackages.add(pkg)
958         }
959     }
960 
961     return PackageDocs(packageComments, overviewHtml, hiddenPackages)
962 }
963 
extractRootsnull964 fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> {
965     // Cache for each directory since computing root for a source file is
966     // expensive
967     val dirToRootCache = mutableMapOf<String, File>()
968     for (file in sources) {
969         val parent = file.parentFile ?: continue
970         val found = dirToRootCache[parent.path]
971         if (found != null) {
972             continue
973         }
974 
975         val root = findRoot(file) ?: continue
976         dirToRootCache[parent.path] = root
977 
978         if (!sourceRoots.contains(root)) {
979             sourceRoots.add(root)
980         }
981     }
982 
983     return sourceRoots
984 }
985 
986 /**
987  * If given a full path to a Java or Kotlin source file, produces the path to
988  * the source root if possible.
989  */
findRootnull990 private fun findRoot(file: File): File? {
991     val path = file.path
992     if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) {
993         val pkg = findPackage(file) ?: return null
994         val parent = file.parentFile ?: return null
995         val endIndex = parent.path.length - pkg.length
996         val before = path[endIndex - 1]
997         if (before == '/' || before == '\\') {
998             return File(path.substring(0, endIndex))
999         } else {
1000             reporter.report(
1001                 Issues.IO_ERROR, file,
1002                 "$PROGRAM_NAME was unable to determine the package name. " +
1003                     "This usually means that a source file was where the directory does not seem to match the package " +
1004                     "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
1005             )
1006         }
1007     }
1008 
1009     return null
1010 }
1011 
1012 /** Finds the package of the given Java/Kotlin source file, if possible */
findPackagenull1013 fun findPackage(file: File): String? {
1014     val source = Files.asCharSource(file, UTF_8).read()
1015     return findPackage(source)
1016 }
1017 
1018 /** Finds the package of the given Java/Kotlin source code, if possible */
findPackagenull1019 fun findPackage(source: String): String? {
1020     return ClassName(source).packageName
1021 }
1022 
1023 /** Whether metalava is running unit tests */
isUnderTestnull1024 fun isUnderTest() = java.lang.Boolean.getBoolean(ENV_VAR_METALAVA_TESTS_RUNNING)
1025 
1026 /** Whether metalava is being invoked as part of an Android platform build */
1027 fun isBuildingAndroid() = System.getenv("ANDROID_BUILD_TOP") != null && !isUnderTest()
1028