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