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