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.KotlinLintAnalyzerFacade
30 import com.android.tools.lint.LintCoreApplicationEnvironment
31 import com.android.tools.lint.LintCoreProjectEnvironment
32 import com.android.tools.lint.annotations.Extractor
33 import com.android.tools.lint.checks.infrastructure.ClassName
34 import com.android.tools.lint.detector.api.assertionsEnabled
35 import com.android.tools.metalava.CompatibilityCheck.CheckRequest
36 import com.android.tools.metalava.apilevels.ApiGenerator
37 import com.android.tools.metalava.doclava1.ApiPredicate
38 import com.android.tools.metalava.doclava1.Errors
39 import com.android.tools.metalava.doclava1.FilterPredicate
40 import com.android.tools.metalava.doclava1.TextCodebase
41 import com.android.tools.metalava.model.ClassItem
42 import com.android.tools.metalava.model.Codebase
43 import com.android.tools.metalava.model.Item
44 import com.android.tools.metalava.model.PackageDocs
45 import com.android.tools.metalava.model.psi.PsiBasedCodebase
46 import com.android.tools.metalava.model.psi.packageHtmlToJavadoc
47 import com.android.tools.metalava.model.visitors.ApiVisitor
48 import com.android.utils.StdLogger
49 import com.android.utils.StdLogger.Level.ERROR
50 import com.google.common.base.Stopwatch
51 import com.google.common.collect.Lists
52 import com.google.common.io.Files
53 import com.intellij.core.CoreApplicationEnvironment
54 import com.intellij.openapi.diagnostic.DefaultLogger
55 import com.intellij.openapi.extensions.Extensions
56 import com.intellij.openapi.roots.LanguageLevelProjectExtension
57 import com.intellij.openapi.util.Disposer
58 import com.intellij.pom.java.LanguageLevel
59 import com.intellij.psi.javadoc.CustomJavadocTagProvider
60 import com.intellij.psi.javadoc.JavadocTagInfo
61 import com.intellij.util.execution.ParametersListUtil
62 import java.io.File
63 import java.io.IOException
64 import java.io.OutputStream
65 import java.io.OutputStreamWriter
66 import java.io.PrintWriter
67 import java.util.concurrent.TimeUnit
68 import java.util.concurrent.TimeUnit.SECONDS
69 import java.util.function.Predicate
70 import kotlin.text.Charsets.UTF_8
71
72 const val PROGRAM_NAME = "metalava"
73 const val HELP_PROLOGUE = "$PROGRAM_NAME extracts metadata from source code to generate artifacts such as the " +
74 "signature files, the SDK stub files, external annotations etc."
75
76 @Suppress("PropertyName") // Can't mark const because trimIndent() :-(
77 val BANNER: String = """
78 _ _
79 _ __ ___ ___| |_ __ _| | __ ___ ____ _
80 | '_ ` _ \ / _ \ __/ _` | |/ _` \ \ / / _` |
81 | | | | | | __/ || (_| | | (_| |\ V / (_| |
82 |_| |_| |_|\___|\__\__,_|_|\__,_| \_/ \__,_|
83 """.trimIndent()
84
85 fun main(args: Array<String>) {
86 run(args, setExitCode = true)
87 }
88
89 /**
90 * The metadata driver is a command line interface to extracting various metadata
91 * from a source tree (or existing signature files etc). Run with --help to see
92 * more details.
93 */
runnull94 fun run(
95 args: Array<String>,
96 stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)),
97 stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)),
98 setExitCode: Boolean = false
99 ): Boolean {
100
101 if (System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) != null && !isUnderTest()
102 ) {
103 stdout.println("---Running $PROGRAM_NAME----")
104 stdout.println("pwd=${File("").absolutePath}")
105 args.forEach { arg ->
106 stdout.println("\"$arg\",")
107 }
108 stdout.println("----------------------------")
109 }
110
111 var exitValue: Boolean
112 var exitCode = 0
113
114 try {
115 val modifiedArgs =
116 if (args.isEmpty()) {
117 arrayOf("--help")
118 } else {
119 val index = args.indexOf(ARG_GENERATE_DOCUMENTATION)
120 val prepend = envVarToArgs(ENV_VAR_METALAVA_PREPEND_ARGS)
121 val append = envVarToArgs(ENV_VAR_METALAVA_APPEND_ARGS)
122 if (prepend.isEmpty() && append.isEmpty()) {
123 args
124 } else {
125 val newArgs =
126 if (index != -1) {
127 args.sliceArray(0 until index) + prepend + args.sliceArray(index until args.size) + append
128 } else {
129 prepend + args + append
130 }
131 if (System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) != null) {
132 stdout.println("---Modified $PROGRAM_NAME arguments from environment variables ----")
133 stdout.println("$ENV_VAR_METALAVA_PREPEND_ARGS: ${prepend.joinToString { "\"$it\"" }}")
134 stdout.println("$ENV_VAR_METALAVA_APPEND_ARGS: ${append.joinToString { "\"$it\"" }}")
135 newArgs.forEach { arg ->
136 stdout.println("\"$arg\",")
137 }
138 stdout.println("----------------------------")
139 stdout.flush()
140 }
141 newArgs
142 }
143 }
144
145 compatibility = Compatibility(compat = Options.useCompatMode(args))
146 options = Options(modifiedArgs, stdout, stderr)
147 processFlags()
148
149 if (reporter.hasErrors() && !options.passBaselineUpdates) {
150 exitCode = -1
151 }
152 exitValue = true
153 } catch (e: DriverException) {
154 stdout.flush()
155 stderr.flush()
156 if (e.stderr.isNotBlank()) {
157 stderr.println("\n${e.stderr}")
158 }
159 if (e.stdout.isNotBlank()) {
160 stdout.println("\n${e.stdout}")
161 }
162 exitCode = e.exitCode
163 exitValue = false
164 } finally {
165 Disposer.dispose(LintCoreApplicationEnvironment.get().parentDisposable)
166 }
167
168 if (options.updateBaseline) {
169 if (options.verbose) {
170 options.baseline?.dumpStats(options.stdout)
171 }
172 if (!options.quiet) {
173 stdout.println("$PROGRAM_NAME wrote updated baseline to ${options.baseline?.updateFile}")
174 }
175 }
176 options.baseline?.close()
177
178 stdout.flush()
179 stderr.flush()
180
181 if (setExitCode) {
182 exit(exitCode)
183 }
184
185 return exitValue
186 }
187
188 /**
189 * Given an environment variable name pointing to a shell argument string,
190 * returns the parsed argument strings (or empty array if not set)
191 */
envVarToArgsnull192 private fun envVarToArgs(varName: String): Array<String> {
193 val value = System.getenv(varName) ?: return emptyArray()
194 return ParametersListUtil.parse(value).toTypedArray()
195 }
196
exitnull197 private fun exit(exitCode: Int = 0) {
198 if (options.verbose) {
199 options.stdout.println("$PROGRAM_NAME exiting with exit code $exitCode")
200 }
201 options.stdout.flush()
202 options.stderr.flush()
203 System.exit(exitCode)
204 }
205
processFlagsnull206 private fun processFlags() {
207 val stopwatch = Stopwatch.createStarted()
208
209 processNonCodebaseFlags()
210
211 val codebase =
212 if (options.sources.size == 1 && options.sources[0].path.endsWith(DOT_TXT)) {
213 SignatureFileLoader.load(
214 file = options.sources[0],
215 kotlinStyleNulls = options.inputKotlinStyleNulls
216 )
217 } else if (options.apiJar != null) {
218 loadFromJarFile(options.apiJar!!)
219 } else if (options.sources.size == 1 && options.sources[0].path.endsWith(DOT_JAR)) {
220 loadFromJarFile(options.sources[0])
221 } else if (options.sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
222 loadFromSources()
223 } else {
224 return
225 }
226 options.manifest?.let { codebase.manifest = it }
227
228 if (options.verbose) {
229 options.stdout.println("\n$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(TimeUnit.SECONDS)} seconds")
230 }
231
232 options.subtractApi?.let {
233 subtractApi(codebase, it)
234 }
235
236 val androidApiLevelXml = options.generateApiLevelXml
237 val apiLevelJars = options.apiLevelJars
238 if (androidApiLevelXml != null && apiLevelJars != null) {
239 progress("\nGenerating API levels XML descriptor file, ${androidApiLevelXml.name}: ")
240 ApiGenerator.generate(apiLevelJars, androidApiLevelXml, codebase)
241 }
242
243 if ((options.stubsDir != null || options.docStubsDir != null) && codebase.supportsDocumentation()) {
244 progress("\nEnhancing docs: ")
245 val docAnalyzer = DocAnalyzer(codebase)
246 docAnalyzer.enhance()
247
248 val applyApiLevelsXml = options.applyApiLevelsXml
249 if (applyApiLevelsXml != null) {
250 progress("\nApplying API levels")
251 docAnalyzer.applyApiLevels(applyApiLevelsXml)
252 }
253 }
254
255 // Generate the documentation stubs *before* we migrate nullness information.
256 options.docStubsDir?.let {
257 createStubFiles(
258 it, codebase, docStubs = true,
259 writeStubList = options.docStubsSourceList != null
260 )
261 }
262
263 // Based on the input flags, generates various output files such
264 // as signature files and/or stubs files
265 options.apiFile?.let { apiFile ->
266 val apiType = ApiType.PUBLIC_API
267 val apiEmit = apiType.getEmitFilter()
268 val apiReference = apiType.getReferenceFilter()
269
270 createReportFile(codebase, apiFile, "API") { printWriter ->
271 SignatureWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
272 }
273 }
274
275 options.dexApiFile?.let { apiFile ->
276 val apiFilter = FilterPredicate(ApiPredicate())
277 val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
278 val apiReference = ApiPredicate(ignoreShown = true)
279 val dexApiEmit = memberIsNotCloned.and(apiFilter)
280
281 createReportFile(
282 codebase, apiFile, "DEX API"
283 ) { printWriter -> DexApiWriter(printWriter, dexApiEmit, apiReference) }
284 }
285
286 options.apiXmlFile?.let { apiFile ->
287 val apiType = ApiType.PUBLIC_API
288 val apiEmit = apiType.getEmitFilter()
289 val apiReference = apiType.getReferenceFilter()
290
291 createReportFile(codebase, apiFile, "XML API") { printWriter ->
292 JDiffXmlWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
293 }
294 }
295
296 options.dexApiMappingFile?.let { apiFile ->
297 val apiType = ApiType.ALL
298 val apiEmit = apiType.getEmitFilter()
299 val apiReference = apiType.getReferenceFilter()
300
301 createReportFile(
302 codebase, apiFile, "DEX API Mapping"
303 ) { printWriter ->
304 DexApiWriter(
305 printWriter, apiEmit, apiReference,
306 membersOnly = true,
307 includePositions = true
308 )
309 }
310 }
311
312 options.removedApiFile?.let { apiFile ->
313 val unfiltered = codebase.original ?: codebase
314
315 val apiType = ApiType.REMOVED
316 val removedEmit = apiType.getEmitFilter()
317 val removedReference = apiType.getReferenceFilter()
318
319 createReportFile(unfiltered, apiFile, "removed API") { printWriter ->
320 SignatureWriter(printWriter, removedEmit, removedReference, codebase.original != null)
321 }
322 }
323
324 options.removedDexApiFile?.let { apiFile ->
325 val unfiltered = codebase.original ?: codebase
326
327 val removedFilter = FilterPredicate(ApiPredicate(matchRemoved = true))
328 val removedReference = ApiPredicate(ignoreShown = true, ignoreRemoved = true)
329 val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
330 val removedDexEmit = memberIsNotCloned.and(removedFilter)
331
332 createReportFile(
333 unfiltered, apiFile, "removed DEX API"
334 ) { printWriter -> DexApiWriter(printWriter, removedDexEmit, removedReference) }
335 }
336
337 options.privateApiFile?.let { apiFile ->
338 val apiType = ApiType.PRIVATE
339 val privateEmit = apiType.getEmitFilter()
340 val privateReference = apiType.getReferenceFilter()
341
342 createReportFile(codebase, apiFile, "private API") { printWriter ->
343 SignatureWriter(printWriter, privateEmit, privateReference, codebase.original != null)
344 }
345 }
346
347 options.privateDexApiFile?.let { apiFile ->
348 val apiFilter = FilterPredicate(ApiPredicate())
349 val privateEmit = apiFilter.negate()
350 val privateReference = Predicate<Item> { true }
351
352 createReportFile(
353 codebase, apiFile, "private DEX API"
354 ) { printWriter ->
355 DexApiWriter(
356 printWriter, privateEmit, privateReference, inlineInheritedFields = false
357 )
358 }
359 }
360
361 options.proguard?.let { proguard ->
362 val apiEmit = FilterPredicate(ApiPredicate())
363 val apiReference = ApiPredicate(ignoreShown = true)
364 createReportFile(
365 codebase, proguard, "Proguard file"
366 ) { printWriter -> ProguardWriter(printWriter, apiEmit, apiReference) }
367 }
368
369 options.sdkValueDir?.let { dir ->
370 dir.mkdirs()
371 SdkFileWriter(codebase, dir).generate()
372 }
373
374 for (check in options.compatibilityChecks) {
375 checkCompatibility(codebase, check)
376 }
377
378 val previousApiFile = options.migrateNullsFrom
379 if (previousApiFile != null) {
380 val previous =
381 if (previousApiFile.path.endsWith(DOT_JAR)) {
382 loadFromJarFile(previousApiFile)
383 } else {
384 SignatureFileLoader.load(
385 file = previousApiFile,
386 kotlinStyleNulls = options.inputKotlinStyleNulls
387 )
388 }
389
390 // If configured, checks for newly added nullness information compared
391 // to the previous stable API and marks the newly annotated elements
392 // as migrated (which will cause the Kotlin compiler to treat problems
393 // as warnings instead of errors
394
395 migrateNulls(codebase, previous)
396
397 previous.dispose()
398 }
399
400 // Now that we've migrated nullness information we can proceed to write non-doc stubs, if any.
401
402 options.stubsDir?.let {
403 createStubFiles(
404 it, codebase, docStubs = false,
405 writeStubList = options.stubsSourceList != null
406 )
407
408 val stubAnnotations = options.copyStubAnnotationsFrom
409 if (stubAnnotations != null) {
410 // Support pointing to both stub-annotations and stub-annotations/src/main/java
411 val src = File(stubAnnotations, "src${File.separator}main${File.separator}java")
412 val source = if (src.isDirectory) src else stubAnnotations
413 source.listFiles()?.forEach { file ->
414 RewriteAnnotations().copyAnnotations(codebase, file, File(it, file.name))
415 }
416 }
417 }
418
419 if (options.docStubsDir == null && options.stubsDir == null) {
420 val writeStubsFile: (File) -> Unit = { file ->
421 val root = File("").absoluteFile
422 val sources = options.sources
423 val rootPath = root.path
424 val contents = sources.joinToString(" ") {
425 val path = it.path
426 if (path.startsWith(rootPath)) {
427 path.substring(rootPath.length)
428 } else {
429 path
430 }
431 }
432 file.writeText(contents)
433 }
434 options.stubsSourceList?.let(writeStubsFile)
435 options.docStubsSourceList?.let(writeStubsFile)
436 }
437 options.externalAnnotations?.let { extractAnnotations(codebase, it) }
438 progress("\n")
439
440 // Coverage stats?
441 if (options.dumpAnnotationStatistics) {
442 progress("\nMeasuring annotation statistics: ")
443 AnnotationStatistics(codebase).count()
444 }
445 if (options.annotationCoverageOf.isNotEmpty()) {
446 progress("\nMeasuring annotation coverage: ")
447 AnnotationStatistics(codebase).measureCoverageOf(options.annotationCoverageOf)
448 }
449
450 if (options.verbose) {
451 val packageCount = codebase.size()
452 options.stdout.println("\n$PROGRAM_NAME finished handling $packageCount packages in $stopwatch")
453 options.stdout.flush()
454 }
455
456 invokeDocumentationTool()
457 }
458
subtractApinull459 fun subtractApi(codebase: Codebase, subtractApiFile: File) {
460 val path = subtractApiFile.path
461 val oldCodebase =
462 when {
463 path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile)
464 path.endsWith(DOT_JAR) -> loadFromJarFile(subtractApiFile)
465 else -> throw DriverException("Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}")
466 }
467
468 CodebaseComparator().compare(object : ComparisonVisitor() {
469 override fun compare(old: ClassItem, new: ClassItem) {
470 new.included = false
471 new.emit = false
472 }
473 }, oldCodebase, codebase, ApiType.ALL.getReferenceFilter())
474 }
475
processNonCodebaseFlagsnull476 fun processNonCodebaseFlags() {
477 // --copy-annotations?
478 val privateAnnotationsSource = options.privateAnnotationsSource
479 val privateAnnotationsTarget = options.privateAnnotationsTarget
480 if (privateAnnotationsSource != null && privateAnnotationsTarget != null) {
481 val rewrite = RewriteAnnotations()
482 // Support pointing to both stub-annotations and stub-annotations/src/main/java
483 val src = File(privateAnnotationsSource, "src${File.separator}main${File.separator}java")
484 val source = if (src.isDirectory) src else privateAnnotationsSource
485 source.listFiles()?.forEach { file ->
486 rewrite.modifyAnnotationSources(null, file, File(privateAnnotationsTarget, file.name))
487 }
488 }
489
490 // --rewrite-annotations?
491 options.rewriteAnnotations?.let { RewriteAnnotations().rewriteAnnotations(it) }
492
493 // Convert android.jar files?
494 options.androidJarSignatureFiles?.let { root ->
495 // Generate API signature files for all the historical JAR files
496 ConvertJarsToSignatureFiles().convertJars(root)
497 }
498
499 for (convert in options.convertToXmlFiles) {
500 val signatureApi = SignatureFileLoader.load(
501 file = convert.fromApiFile,
502 kotlinStyleNulls = options.inputKotlinStyleNulls
503 )
504
505 val apiType = ApiType.ALL
506 val apiEmit = apiType.getEmitFilter()
507 val strip = convert.strip
508 val apiReference = if (strip) apiType.getEmitFilter() else apiType.getReferenceFilter()
509 val baseFile = convert.baseApiFile
510
511 val outputApi =
512 if (baseFile != null) {
513 // Convert base on a diff
514 val baseApi = SignatureFileLoader.load(
515 file = baseFile,
516 kotlinStyleNulls = options.inputKotlinStyleNulls
517 )
518
519 val includeFields =
520 if (convert.outputFormat == FileFormat.V2) true else compatibility.includeFieldsInApiDiff
521 TextCodebase.computeDelta(baseFile, baseApi, signatureApi, includeFields)
522 } else {
523 signatureApi
524 }
525
526 if (outputApi.isEmpty() && baseFile != null && compatibility.compat) {
527 // doclava compatibility: emits error warning instead of emitting empty <api/> element
528 options.stdout.println("No API change detected, not generating diff")
529 } else {
530 val output = convert.outputFile
531 if (convert.outputFormat == FileFormat.JDIFF) {
532 // See JDiff's XMLToAPI#nameAPI
533 val apiName = convert.outputFile.nameWithoutExtension.replace(' ', '_')
534 createReportFile(outputApi, output, "JDiff File") { printWriter ->
535 JDiffXmlWriter(printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip, apiName)
536 }
537 } else {
538 val prevOptions = options
539 val prevCompatibility = compatibility
540 try {
541 when (convert.outputFormat) {
542 FileFormat.V1 -> {
543 compatibility = Compatibility(true)
544 options = Options(emptyArray(), options.stdout, options.stderr)
545 FileFormat.V1.configureOptions(options, compatibility)
546 }
547 FileFormat.V2 -> {
548 compatibility = Compatibility(false)
549 options = Options(emptyArray(), options.stdout, options.stderr)
550 FileFormat.V2.configureOptions(options, compatibility)
551 }
552 else -> error("Unsupported format ${convert.outputFormat}")
553 }
554
555 createReportFile(outputApi, output, "Diff API File") { printWriter ->
556 SignatureWriter(
557 printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip
558 )
559 }
560 } finally {
561 options = prevOptions
562 compatibility = prevCompatibility
563 }
564 }
565 }
566 }
567 }
568
569 /**
570 * Checks compatibility of the given codebase with the codebase described in the
571 * signature file.
572 */
checkCompatibilitynull573 fun checkCompatibility(
574 codebase: Codebase,
575 check: CheckRequest
576 ) {
577 val signatureFile = check.file
578
579 val current =
580 if (signatureFile.path.endsWith(DOT_JAR)) {
581 loadFromJarFile(signatureFile)
582 } else {
583 SignatureFileLoader.load(
584 file = signatureFile,
585 kotlinStyleNulls = options.inputKotlinStyleNulls
586 )
587 }
588
589 if (current is TextCodebase && current.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
590 throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${current.format} without analyzing current codebase with $ARG_FORMAT=${current.format}")
591 }
592
593 var base: Codebase? = null
594 val releaseType = check.releaseType
595 val apiType = check.apiType
596
597 // If diffing with a system-api or test-api (or other signature-based codebase
598 // generated from --show-annotations), the API is partial: it's only listing
599 // the API that is *different* from the base API. This really confuses the
600 // codebase comparison when diffing with a complete codebase, since it looks like
601 // many classes and members have been added and removed. Therefore, the comparison
602 // is simpler if we just make the comparison with the same generated signature
603 // file. If we've only emitted one for the new API, use it directly, if not, generate
604 // it first
605 val new =
606 if (check.codebase != null) {
607 SignatureFileLoader.load(
608 file = check.codebase,
609 kotlinStyleNulls = options.inputKotlinStyleNulls
610 )
611 } else if (options.showAnnotations.isNotEmpty() || apiType != ApiType.PUBLIC_API) {
612 val apiFile = apiType.getSignatureFile(codebase, "compat-check-signatures-$apiType")
613
614 // Fast path: if the signature files are identical, we're already good!
615 if (apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
616 return
617 }
618
619 base = codebase
620
621 SignatureFileLoader.load(
622 file = apiFile,
623 kotlinStyleNulls = options.inputKotlinStyleNulls
624 )
625 } else {
626 // Fast path: if we've already generated a signature file and it's identical, we're good!
627 val apiFile = options.apiFile
628 if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
629 return
630 }
631
632 codebase
633 }
634
635 // If configured, compares the new API with the previous API and reports
636 // any incompatibilities.
637 CompatibilityCheck.checkCompatibility(new, current, releaseType, apiType, base)
638
639 // Make sure the text files are identical too? (only applies for *current.txt;
640 // last-released is expected to differ)
641 if (releaseType == ReleaseType.DEV && !options.allowCompatibleDifferences) {
642 val apiFile = if (new.location.isFile)
643 new.location
644 else
645 apiType.getSignatureFile(codebase, "compat-diff-signatures-$apiType")
646
647 fun getCanonicalSignatures(file: File): String {
648 // Get rid of trailing newlines and Windows line endings
649 val text = file.readText(UTF_8)
650 return text.replace("\r\n", "\n").trim()
651 }
652 val currentTxt = getCanonicalSignatures(signatureFile)
653 val newTxt = getCanonicalSignatures(apiFile)
654 if (newTxt != currentTxt) {
655 val diff = getNativeDiff(signatureFile, apiFile) ?: getDiff(currentTxt, newTxt, 1)
656 val updateApi = if (isBuildingAndroid())
657 "Run make update-api to update.\n"
658 else
659 ""
660 val message =
661 """
662 Aborting: Your changes have resulted in differences in the signature file
663 for the ${apiType.displayName} API.
664
665 The changes may be compatible, but the signature file needs to be updated.
666 $updateApi
667 Diffs:
668 """.trimIndent() + "\n" + diff
669
670 throw DriverException(exitCode = -1, stderr = message)
671 }
672 }
673 }
674
createTempFilenull675 fun createTempFile(namePrefix: String, nameSuffix: String): File {
676 val tempFolder = options.tempFolder
677 return if (tempFolder != null) {
678 val preferred = File(tempFolder, namePrefix + nameSuffix)
679 if (!preferred.exists()) {
680 return preferred
681 }
682 File.createTempFile(namePrefix, nameSuffix, tempFolder)
683 } else {
684 File.createTempFile(namePrefix, nameSuffix)
685 }
686 }
687
invokeDocumentationToolnull688 fun invokeDocumentationTool() {
689 if (options.noDocs) {
690 return
691 }
692
693 val args = options.invokeDocumentationToolArguments
694 if (args.isNotEmpty()) {
695 if (!options.quiet) {
696 options.stdout.println(
697 "Invoking external documentation tool ${args[0]} with arguments\n\"${
698 args.slice(1 until args.size).joinToString(separator = "\",\n\"") { it }}\""
699 )
700 options.stdout.flush()
701 }
702
703 val builder = ProcessInfoBuilder()
704
705 builder.setExecutable(File(args[0]))
706 builder.addArgs(args.slice(1 until args.size))
707
708 val processOutputHandler =
709 if (options.quiet) {
710 CachedProcessOutputHandler()
711 } else {
712 object : ProcessOutputHandler {
713 override fun handleOutput(processOutput: ProcessOutput?) {
714 }
715
716 override fun createOutput(): ProcessOutput {
717 val out = PrintWriterOutputStream(options.stdout)
718 val err = PrintWriterOutputStream(options.stderr)
719 return object : ProcessOutput {
720 override fun getStandardOutput(): OutputStream {
721 return out
722 }
723
724 override fun getErrorOutput(): OutputStream {
725 return err
726 }
727
728 override fun close() {
729 out.flush()
730 err.flush()
731 }
732 }
733 }
734 }
735 }
736
737 val result = DefaultProcessExecutor(StdLogger(ERROR))
738 .execute(builder.createProcess(), processOutputHandler)
739
740 val exitCode = result.exitValue
741 if (!options.quiet) {
742 options.stdout.println("${args[0]} finished with exitCode $exitCode")
743 options.stdout.flush()
744 }
745 if (exitCode != 0) {
746 val stdout = if (processOutputHandler is CachedProcessOutputHandler)
747 processOutputHandler.processOutput.standardOutputAsString
748 else ""
749 val stderr = if (processOutputHandler is CachedProcessOutputHandler)
750 processOutputHandler.processOutput.errorOutputAsString
751 else ""
752 throw DriverException(
753 stdout = "Invoking documentation tool ${args[0]} failed with exit code $exitCode\n$stdout",
754 stderr = stderr,
755 exitCode = exitCode
756 )
757 }
758 }
759 }
760
761 class PrintWriterOutputStream(private val writer: PrintWriter) : OutputStream() {
762
writenull763 override fun write(b: ByteArray) {
764 writer.write(String(b, UTF_8))
765 }
766
writenull767 override fun write(b: Int) {
768 write(byteArrayOf(b.toByte()), 0, 1)
769 }
770
writenull771 override fun write(b: ByteArray, off: Int, len: Int) {
772 writer.write(String(b, off, len, UTF_8))
773 }
774
flushnull775 override fun flush() {
776 writer.flush()
777 }
778
closenull779 override fun close() {
780 writer.close()
781 }
782 }
783
migrateNullsnull784 private fun migrateNulls(codebase: Codebase, previous: Codebase) {
785 previous.compareWith(NullnessMigration(), codebase, ApiPredicate())
786 }
787
loadFromSourcesnull788 private fun loadFromSources(): Codebase {
789 progress("\nProcessing sources: ")
790
791 val sources = if (options.sources.isEmpty()) {
792 if (options.verbose) {
793 options.stdout.println("No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})")
794 }
795 gatherSources(options.sourcePath)
796 } else {
797 options.sources
798 }
799
800 progress("\nReading Codebase: ")
801 val codebase = parseSources(sources, "Codebase loaded from source folders")
802
803 progress("\nAnalyzing API: ")
804
805 val analyzer = ApiAnalyzer(codebase)
806 analyzer.mergeExternalInclusionAnnotations()
807 analyzer.computeApi()
808 analyzer.mergeExternalQualifierAnnotations()
809 options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
810 options.nullabilityAnnotationsValidator?.report()
811 analyzer.handleStripping()
812
813 if (options.checkKotlinInterop) {
814 KotlinInteropChecks().check(codebase)
815 }
816
817 // General API checks for Android APIs
818 AndroidApiChecks().check(codebase)
819
820 if (options.checkApi) {
821 val localTimer = Stopwatch.createStarted()
822 // See if we should provide a previous codebase to provide a delta from?
823 val previousApiFile = options.checkApiBaselineApiFile
824 val previous =
825 when {
826 previousApiFile == null -> null
827 previousApiFile.path.endsWith(DOT_JAR) -> loadFromJarFile(previousApiFile)
828 else -> SignatureFileLoader.load(
829 file = previousApiFile,
830 kotlinStyleNulls = options.inputKotlinStyleNulls
831 )
832 }
833 ApiLint.check(codebase, previous)
834 progress("\n$PROGRAM_NAME ran api-lint in ${localTimer.elapsed(SECONDS)} seconds")
835 }
836
837 val filterEmit = ApiPredicate(ignoreShown = true, ignoreRemoved = false)
838 val apiEmit = ApiPredicate(ignoreShown = true)
839 val apiReference = ApiPredicate(ignoreShown = true)
840
841 // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary
842 progress("\nInsert missing stubs methods: ")
843 analyzer.generateInheritedStubs(apiEmit, apiReference)
844
845 // Compute default constructors (and add missing package private constructors
846 // to make stubs compilable if necessary)
847 if (options.stubsDir != null || options.docStubsDir != null) {
848 progress("\nInsert missing constructors: ")
849 analyzer.addConstructors(filterEmit)
850 }
851
852 progress("\nPerforming misc API checks: ")
853 analyzer.performChecks()
854
855 return codebase
856 }
857
858 /**
859 * Returns a codebase initialized from the given Java or Kotlin source files, with the given
860 * description. The codebase will use a project environment initialized according to the current
861 * [options].
862 */
parseSourcesnull863 internal fun parseSources(
864 sources: List<File>,
865 description: String,
866 sourcePath: List<File> = options.sourcePath,
867 classpath: List<File> = options.classpath,
868 javaLanguageLevel: LanguageLevel = options.javaLanguageLevel,
869 manifest: File? = options.manifest,
870 currentApiLevel: Int = options.currentApiLevel + if (options.currentCodeName != null) 1 else 0
871 ): PsiBasedCodebase {
872 val projectEnvironment = createProjectEnvironment()
873 val project = projectEnvironment.project
874
875 // Push language level to PSI handler
876 project.getComponent(LanguageLevelProjectExtension::class.java)?.languageLevel = javaLanguageLevel
877
878 val joined = mutableListOf<File>()
879 joined.addAll(sourcePath.mapNotNull { if (it.path.isNotBlank()) it.absoluteFile else null })
880 joined.addAll(classpath.map { it.absoluteFile })
881 // Add in source roots implied by the source files
882 val sourceRoots = mutableListOf<File>()
883 extractRoots(sources, sourceRoots)
884 joined.addAll(sourceRoots)
885
886 // Create project environment with those paths
887 projectEnvironment.registerPaths(joined)
888
889 val kotlinFiles = sources.filter { it.path.endsWith(DOT_KT) }
890 val trace = KotlinLintAnalyzerFacade().analyze(kotlinFiles, joined, project)
891
892 val rootDir = sourceRoots.firstOrNull() ?: sourcePath.firstOrNull() ?: File("").canonicalFile
893
894 val units = Extractor.createUnitsForFiles(project, sources)
895 val packageDocs = gatherHiddenPackagesFromJavaDocs(sourcePath)
896
897 val codebase = PsiBasedCodebase(rootDir, description)
898 codebase.initialize(project, units, packageDocs)
899 codebase.manifest = manifest
900 codebase.apiLevel = currentApiLevel
901 codebase.bindingContext = trace.bindingContext
902 return codebase
903 }
904
loadFromJarFilenull905 fun loadFromJarFile(apiJar: File, manifest: File? = null, preFiltered: Boolean = false): Codebase {
906 val projectEnvironment = createProjectEnvironment()
907
908 progress("\nProcessing jar file: ")
909
910 // Create project environment with those paths
911 val project = projectEnvironment.project
912 projectEnvironment.registerPaths(listOf(apiJar))
913
914 val kotlinFiles = emptyList<File>()
915 val trace = KotlinLintAnalyzerFacade().analyze(kotlinFiles, listOf(apiJar), project)
916
917 val codebase = PsiBasedCodebase(apiJar, "Codebase loaded from $apiJar")
918 codebase.initialize(project, apiJar, preFiltered)
919 if (manifest != null) {
920 codebase.manifest = options.manifest
921 }
922 val apiEmit = ApiPredicate(ignoreShown = true)
923 val apiReference = ApiPredicate(ignoreShown = true)
924 val analyzer = ApiAnalyzer(codebase)
925 analyzer.mergeExternalInclusionAnnotations()
926 analyzer.computeApi()
927 analyzer.mergeExternalQualifierAnnotations()
928 options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
929 options.nullabilityAnnotationsValidator?.report()
930 analyzer.generateInheritedStubs(apiEmit, apiReference)
931 codebase.bindingContext = trace.bindingContext
932 return codebase
933 }
934
createProjectEnvironmentnull935 private fun createProjectEnvironment(): LintCoreProjectEnvironment {
936 ensurePsiFileCapacity()
937 val appEnv = LintCoreApplicationEnvironment.get()
938 val parentDisposable = appEnv.parentDisposable
939
940 if (!assertionsEnabled() &&
941 System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) == null &&
942 !isUnderTest()
943 ) {
944 DefaultLogger.disableStderrDumping(parentDisposable)
945 }
946
947 val environment = LintCoreProjectEnvironment.create(parentDisposable, appEnv)
948
949 // Missing service needed in metalava but not in lint: javadoc handling
950 environment.project.registerService(
951 com.intellij.psi.javadoc.JavadocManager::class.java,
952 com.intellij.psi.impl.source.javadoc.JavadocManagerImpl::class.java
953 )
954 environment.registerProjectExtensionPoint(JavadocTagInfo.EP_NAME,
955 com.intellij.psi.javadoc.JavadocTagInfo::class.java)
956 CoreApplicationEnvironment.registerExtensionPoint(
957 Extensions.getRootArea(), CustomJavadocTagProvider.EP_NAME, CustomJavadocTagProvider::class.java
958 )
959
960 return environment
961 }
962
ensurePsiFileCapacitynull963 private fun ensurePsiFileCapacity() {
964 val fileSize = System.getProperty("idea.max.intellisense.filesize")
965 if (fileSize == null) {
966 // Ensure we can handle large compilation units like android.R
967 System.setProperty("idea.max.intellisense.filesize", "100000")
968 }
969 }
970
extractAnnotationsnull971 private fun extractAnnotations(codebase: Codebase, file: File) {
972 val localTimer = Stopwatch.createStarted()
973
974 options.externalAnnotations?.let { outputFile ->
975 @Suppress("UNCHECKED_CAST")
976 ExtractAnnotations(
977 codebase,
978 outputFile
979 ).extractAnnotations()
980 if (options.verbose) {
981 options.stdout.print("\n$PROGRAM_NAME extracted annotations into $file in $localTimer")
982 options.stdout.flush()
983 }
984 }
985 }
986
createStubFilesnull987 private fun createStubFiles(stubDir: File, codebase: Codebase, docStubs: Boolean, writeStubList: Boolean) {
988 // Generating stubs from a sig-file-based codebase is problematic
989 assert(codebase.supportsDocumentation())
990
991 // Temporary bug workaround for org.chromium.arc
992 if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
993 codebase.findClass("org.chromium.mojo.bindings.Callbacks")?.hidden = true
994 }
995
996 if (docStubs) {
997 progress("\nGenerating documentation stub files: ")
998 } else {
999 progress("\nGenerating stub files: ")
1000 }
1001
1002 val localTimer = Stopwatch.createStarted()
1003 val prevCompatibility = compatibility
1004 if (compatibility.compat) {
1005 compatibility = Compatibility(false)
1006 // But preserve the setting for whether we want to erase throws signatures (to ensure the API
1007 // stays compatible)
1008 compatibility.useErasureInThrows = prevCompatibility.useErasureInThrows
1009 }
1010
1011 val stubWriter =
1012 StubWriter(
1013 codebase = codebase,
1014 stubsDir = stubDir,
1015 generateAnnotations = options.generateAnnotations,
1016 preFiltered = codebase.preFiltered,
1017 docStubs = docStubs
1018 )
1019 codebase.accept(stubWriter)
1020
1021 if (docStubs) {
1022 // Overview docs? These are generally in the empty package.
1023 codebase.findPackage("")?.let { empty ->
1024 val overview = codebase.getPackageDocs()?.getOverviewDocumentation(empty)
1025 if (overview != null && overview.isNotBlank()) {
1026 stubWriter.writeDocOverview(empty, overview)
1027 }
1028 }
1029 }
1030
1031 if (writeStubList) {
1032 // Optionally also write out a list of source files that were generated; used
1033 // for example to point javadoc to the stubs output to generate documentation
1034 val file = if (docStubs) {
1035 options.docStubsSourceList ?: options.stubsSourceList
1036 } else {
1037 options.stubsSourceList
1038 }
1039 file?.let {
1040 val root = File("").absoluteFile
1041 stubWriter.writeSourceList(it, root)
1042 }
1043 }
1044
1045 compatibility = prevCompatibility
1046
1047 progress(
1048 "\n$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
1049 localTimer.elapsed(SECONDS)} seconds"
1050 )
1051 }
1052
progressnull1053 fun progress(message: String) {
1054 if (options.verbose) {
1055 options.stdout.print(message)
1056 options.stdout.flush()
1057 }
1058 }
1059
createReportFilenull1060 fun createReportFile(
1061 codebase: Codebase,
1062 apiFile: File,
1063 description: String?,
1064 createVisitor: (PrintWriter) -> ApiVisitor
1065 ) {
1066 if (description != null) {
1067 progress("\nWriting $description file: ")
1068 }
1069 val localTimer = Stopwatch.createStarted()
1070 try {
1071 val writer = PrintWriter(Files.asCharSink(apiFile, UTF_8).openBufferedStream())
1072 writer.use { printWriter ->
1073 val apiWriter = createVisitor(printWriter)
1074 codebase.accept(apiWriter)
1075 }
1076 } catch (e: IOException) {
1077 reporter.report(Errors.IO_ERROR, apiFile, "Cannot open file for write.")
1078 }
1079 if (description != null && options.verbose) {
1080 options.stdout.print("\n$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds")
1081 }
1082 }
1083
1084 /** Used for verbose output to show progress bar */
1085 private var tick = 0
1086
1087 /** Needed for tests to ensure we don't get unpredictable behavior of "." in output */
resetTickernull1088 fun resetTicker() {
1089 tick = 0
1090 }
1091
1092 /** Print progress */
ticknull1093 fun tick() {
1094 tick++
1095 if (tick % 100 == 0) {
1096 if (!options.verbose) {
1097 return
1098 }
1099 options.stdout.print(".")
1100 options.stdout.flush()
1101 }
1102 }
1103
skippableDirectorynull1104 private fun skippableDirectory(file: File): Boolean = file.path.endsWith(".git") && file.name == ".git"
1105
1106 private fun addSourceFiles(list: MutableList<File>, file: File) {
1107 if (file.isDirectory) {
1108 if (skippableDirectory(file)) {
1109 return
1110 }
1111 if (java.nio.file.Files.isSymbolicLink(file.toPath())) {
1112 reporter.report(
1113 Errors.IGNORING_SYMLINK, file,
1114 "Ignoring symlink during source file discovery directory traversal"
1115 )
1116 return
1117 }
1118 val files = file.listFiles()
1119 if (files != null) {
1120 for (child in files) {
1121 addSourceFiles(list, child)
1122 }
1123 }
1124 } else {
1125 if (file.isFile && (file.path.endsWith(DOT_JAVA) || file.path.endsWith(DOT_KT))) {
1126 list.add(file)
1127 }
1128 }
1129 }
1130
gatherSourcesnull1131 fun gatherSources(sourcePath: List<File>): List<File> {
1132 val sources = Lists.newArrayList<File>()
1133 for (file in sourcePath) {
1134 if (file.path.isBlank()) {
1135 // --source-path "" means don't search source path; use "." for pwd
1136 continue
1137 }
1138 addSourceFiles(sources, file.absoluteFile)
1139 }
1140 return sources
1141 }
1142
addHiddenPackagesnull1143 private fun addHiddenPackages(
1144 packageToDoc: MutableMap<String, String>,
1145 packageToOverview: MutableMap<String, String>,
1146 hiddenPackages: MutableSet<String>,
1147 file: File,
1148 pkg: String
1149 ) {
1150 if (file.isDirectory) {
1151 if (skippableDirectory(file)) {
1152 return
1153 }
1154 // Ignore symbolic links during traversal
1155 if (java.nio.file.Files.isSymbolicLink(file.toPath())) {
1156 reporter.report(
1157 Errors.IGNORING_SYMLINK, file,
1158 "Ignoring symlink during package.html discovery directory traversal"
1159 )
1160 return
1161 }
1162 val files = file.listFiles()
1163 if (files != null) {
1164 for (child in files) {
1165 var subPkg =
1166 if (child.isDirectory)
1167 if (pkg.isEmpty())
1168 child.name
1169 else pkg + "." + child.name
1170 else pkg
1171
1172 if (subPkg.endsWith("src.main.java")) {
1173 // It looks like the source path was incorrectly configured; make corrections here
1174 // to ensure that we map the package.html files to the real packages.
1175 subPkg = ""
1176 }
1177
1178 addHiddenPackages(packageToDoc, packageToOverview, hiddenPackages, child, subPkg)
1179 }
1180 }
1181 } else if (file.isFile) {
1182 var javadoc = false
1183 val map = when {
1184 file.name == "package.html" -> {
1185 javadoc = true; packageToDoc
1186 }
1187 file.name == "overview.html" -> {
1188 packageToOverview
1189 }
1190 else -> return
1191 }
1192 var contents = Files.asCharSource(file, UTF_8).read()
1193 if (javadoc) {
1194 contents = packageHtmlToJavadoc(contents)
1195 }
1196
1197 var realPkg = pkg
1198 // Sanity check the package; it's computed from the directory name
1199 // relative to the source path, but if the real source path isn't
1200 // passed in (and is instead some directory containing the source path)
1201 // then we compute the wrong package here. Instead, look for an adjacent
1202 // java class and pick the package from it
1203 for (sibling in file.parentFile.listFiles()) {
1204 if (sibling.path.endsWith(DOT_JAVA)) {
1205 val javaPkg = ClassName(sibling.readText()).packageName
1206 if (javaPkg != null) {
1207 realPkg = pkg
1208 break
1209 }
1210 }
1211 }
1212
1213 map[realPkg] = contents
1214 if (contents.contains("@hide")) {
1215 hiddenPackages.add(realPkg)
1216 }
1217 }
1218 }
1219
gatherHiddenPackagesFromJavaDocsnull1220 private fun gatherHiddenPackagesFromJavaDocs(sourcePath: List<File>): PackageDocs {
1221 val packageComments = HashMap<String, String>(100)
1222 val overviewHtml = HashMap<String, String>(10)
1223 val hiddenPackages = HashSet<String>(100)
1224 for (file in sourcePath) {
1225 if (file.path.isBlank()) {
1226 // Ignoring empty paths, which means "no source path search". Use "." for current directory.
1227 continue
1228 }
1229 addHiddenPackages(packageComments, overviewHtml, hiddenPackages, file, "")
1230 }
1231 return PackageDocs(packageComments, overviewHtml, hiddenPackages)
1232 }
1233
extractRootsnull1234 fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> {
1235 // Cache for each directory since computing root for a source file is
1236 // expensive
1237 val dirToRootCache = mutableMapOf<String, File>()
1238 for (file in sources) {
1239 val parent = file.parentFile ?: continue
1240 val found = dirToRootCache[parent.path]
1241 if (found != null) {
1242 continue
1243 }
1244
1245 val root = findRoot(file) ?: continue
1246 dirToRootCache[parent.path] = root
1247
1248 if (!sourceRoots.contains(root)) {
1249 sourceRoots.add(root)
1250 }
1251 }
1252
1253 return sourceRoots
1254 }
1255
1256 /**
1257 * If given a full path to a Java or Kotlin source file, produces the path to
1258 * the source root if possible.
1259 */
findRootnull1260 private fun findRoot(file: File): File? {
1261 val path = file.path
1262 if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) {
1263 val pkg = findPackage(file) ?: return null
1264 val parent = file.parentFile ?: return null
1265 val endIndex = parent.path.length - pkg.length
1266 val before = path[endIndex - 1]
1267 if (before == '/' || before == '\\') {
1268 return File(path.substring(0, endIndex))
1269 } else {
1270 reporter.report(
1271 Errors.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name. " +
1272 "This usually means that a source file was where the directory does not seem to match the package " +
1273 "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
1274 )
1275 }
1276 }
1277
1278 return null
1279 }
1280
1281 /** Finds the package of the given Java/Kotlin source file, if possible */
findPackagenull1282 fun findPackage(file: File): String? {
1283 val source = Files.asCharSource(file, UTF_8).read()
1284 return findPackage(source)
1285 }
1286
1287 /** Finds the package of the given Java/Kotlin source code, if possible */
findPackagenull1288 fun findPackage(source: String): String? {
1289 return ClassName(source).packageName
1290 }
1291
1292 /** Whether metalava is running unit tests */
isUnderTestnull1293 fun isUnderTest() = java.lang.Boolean.getBoolean(ENV_VAR_METALAVA_TESTS_RUNNING)
1294
1295 /** Whether metalava is being invoked as part of an Android platform build */
1296 fun isBuildingAndroid() = System.getenv("ANDROID_BUILD_TOP") != null && !isUnderTest()
1297