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