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