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