@@ -12,6 +12,7 @@ import dev.amber.core.model.SourcePartitionKind
1212import dev.amber.core.model.StmtIr
1313import dev.amber.core.model.TermIr
1414import dev.amber.core.model.placeholderReasons
15+ import dev.amber.pipeline.dex.SourcePartitioner
1516import dev.amber.pipeline.dex.TypeAnnotatedMethod
1617import dev.amber.pipeline.mapping.MappingRenameIndex
1718import java.nio.file.Path
@@ -58,6 +59,11 @@ class JavaEmitter {
5859 val statementCount : Int ,
5960 )
6061
62+ private data class StubClassNode (
63+ val simpleName : String ,
64+ val children : MutableMap <String , StubClassNode > = sortedMapOf(),
65+ )
66+
6167 fun emit (methods : List <TypeAnnotatedMethod >, classes : List <ClassIr > = emptyList()): EmitResult {
6268 val diagnostics = mutableListOf<String >()
6369 val fileBuilders = mutableMapOf<String , TypeSpec .Builder >()
@@ -143,6 +149,9 @@ class JavaEmitter {
143149 .forEach { (sourceSetName, partitionMethods) ->
144150 val partitionOutputDir = outputDir.resolve(sourceSetName).resolve(" java" )
145151 partitionOutputDir.createDirectories()
152+ val partitionKind = partitionMethods.firstOrNull()?.sourcePartition?.kind
153+ ? : SourcePartitionKind .PRIMARY_APP
154+ val includeInMainCompile = partitionKind != SourcePartitionKind .LIKELY_LIBRARY
146155 val classMethodCounts = partitionMethods.groupingBy { annotated ->
147156 parseClassName(annotated.emittedMethodId)
148157 }.eachCount()
@@ -167,8 +176,15 @@ class JavaEmitter {
167176 )
168177 }
169178
170- val partitionKind = partitionMethods.firstOrNull()?.sourcePartition?.kind
171- ? : SourcePartitionKind .PRIMARY_APP
179+ if (includeInMainCompile) {
180+ emitUnresolvedReferenceStubs(
181+ methods = partitionMethods,
182+ outputDir = partitionOutputDir,
183+ diagnostics = diagnostics,
184+ packageFilter = packageFilter,
185+ )
186+ }
187+
172188 val partitionSummary = summarizeSourcePartitions(
173189 partitionMethods.map { annotated -> annotated.sourcePartition },
174190 )
@@ -183,7 +199,7 @@ class JavaEmitter {
183199 language = EmittedSourceLanguage .JAVA ,
184200 partitionKind = partitionKind,
185201 classCount = emittedClassCount,
186- includeInMainCompile = partitionKind != SourcePartitionKind . LIKELY_LIBRARY ,
202+ includeInMainCompile = includeInMainCompile ,
187203 partitionHeuristics = partitionSummary.heuristics,
188204 heuristicCounts = partitionSummary.heuristicCounts,
189205 tplFingerprintDigests = partitionSummary.fingerprintDigests,
@@ -400,6 +416,56 @@ class JavaEmitter {
400416 }
401417 }
402418
419+ private fun emitUnresolvedReferenceStubs (
420+ methods : List <TypeAnnotatedMethod >,
421+ outputDir : Path ,
422+ diagnostics : MutableList <String >,
423+ packageFilter : String? ,
424+ ) {
425+ val emittedClassNames = methods.asSequence()
426+ .map { annotated -> parseClassName(annotated.emittedMethodId) }
427+ .filter { className -> packageFilter == null || className.startsWith(packageFilter) }
428+ .toSet()
429+ val emittedPackages = emittedClassNames.map(::internalPackageName).toSet()
430+ val unresolvedClassNames = methods.asSequence()
431+ .flatMap { annotated -> collectReferencedClassNames(annotated).asSequence() }
432+ .distinct()
433+ .filter { className ->
434+ shouldStubReferencedClass(
435+ className = className,
436+ emittedClassNames = emittedClassNames,
437+ emittedPackages = emittedPackages,
438+ )
439+ }
440+ .sorted()
441+ .toList()
442+
443+ if (unresolvedClassNames.isEmpty()) {
444+ return
445+ }
446+
447+ val stubTrees = linkedMapOf<String , StubClassNode >()
448+ unresolvedClassNames.forEach { className ->
449+ addStubClassNode(stubTrees, className)
450+ }
451+
452+ stubTrees.forEach { (topLevelClassName, rootNode) ->
453+ try {
454+ JavaFile .builder(packageName(topLevelClassName), stubTypeSpec(rootNode, isNested = false ).build())
455+ .skipJavaLangImports(true )
456+ .build()
457+ .writeTo(outputDir)
458+ } catch (e: Exception ) {
459+ diagnostics.add(" Failed to write unresolved stub L$topLevelClassName ;: ${e.message} " )
460+ }
461+ }
462+
463+ diagnostics.add(
464+ " Stubbed ${unresolvedClassNames.size} unresolved referenced classes: ${unresolvedClassNames.take(10 ).joinToString()} " +
465+ if (unresolvedClassNames.size > 10 ) " ..." else " " ,
466+ )
467+ }
468+
403469 private fun emitClassBuilder (
404470 className : String ,
405471 classMethods : List <TypeAnnotatedMethod >,
@@ -440,6 +506,141 @@ class JavaEmitter {
440506 )
441507 }
442508
509+ private fun stubTypeSpec (node : StubClassNode , isNested : Boolean ): TypeSpec .Builder {
510+ val modifiers = if (isNested) {
511+ arrayOf(Modifier .PUBLIC , Modifier .STATIC )
512+ } else {
513+ arrayOf(Modifier .PUBLIC )
514+ }
515+ return TypeSpec .classBuilder(sanitizeIdentifier(node.simpleName, isClass = true ))
516+ .addModifiers(* modifiers)
517+ .apply {
518+ node.children.values.forEach { child ->
519+ addType(stubTypeSpec(child, isNested = true ).build())
520+ }
521+ }
522+ }
523+
524+ private fun addStubClassNode (
525+ stubTrees : MutableMap <String , StubClassNode >,
526+ className : String ,
527+ ) {
528+ val packageName = internalPackageName(className)
529+ val classPart = className.substringAfterLast(' /' )
530+ val segments = classPart.split(' $' ).filter { segment -> segment.isNotEmpty() }
531+ if (segments.isEmpty()) {
532+ return
533+ }
534+
535+ val topLevelClassName = if (packageName.isEmpty()) segments.first() else " $packageName /${segments.first()} "
536+ val rootNode = stubTrees.getOrPut(topLevelClassName) {
537+ StubClassNode (simpleName = segments.first())
538+ }
539+ var current = rootNode
540+ segments.drop(1 ).forEach { segment ->
541+ current = current.children.getOrPut(segment) {
542+ StubClassNode (simpleName = segment)
543+ }
544+ }
545+ }
546+
547+ private fun shouldStubReferencedClass (
548+ className : String ,
549+ emittedClassNames : Set <String >,
550+ emittedPackages : Set <String >,
551+ ): Boolean {
552+ if (className.isBlank()) {
553+ return false
554+ }
555+ if (className in emittedClassNames) {
556+ return false
557+ }
558+ val topLevelClassName = topLevelInternalClassName(className)
559+ if (topLevelClassName in emittedClassNames) {
560+ return false
561+ }
562+ if (internalPackageName(className) !in emittedPackages) {
563+ return false
564+ }
565+ if (SourcePartitioner .partitionClassId(" L$className ;" ).kind == SourcePartitionKind .LIKELY_LIBRARY ) {
566+ return false
567+ }
568+ return true
569+ }
570+
571+ private fun collectReferencedClassNames (annotated : TypeAnnotatedMethod ): Set <String > {
572+ val referencedClassNames = linkedSetOf<String >()
573+ collectClassNamesFromMethodRef(annotated.emittedMethodId, referencedClassNames)
574+ annotated.typeFacts.values.forEach { fact ->
575+ collectClassNamesFromDescriptor(annotated.emittedDescriptor(fact.descriptor), referencedClassNames)
576+ }
577+ annotated.syntheticAccessor?.targetMethodRef?.let { methodRef ->
578+ collectClassNamesFromMethodRef(annotated.emittedMethodRef(methodRef), referencedClassNames)
579+ }
580+ annotated.syntheticAccessor?.targetFieldRef?.let { fieldRef ->
581+ val emittedFieldRef = annotated.emittedFieldRef(fieldRef)
582+ collectClassNamesFromDescriptor(emittedFieldRef.definingClass, referencedClassNames)
583+ collectClassNamesFromDescriptor(emittedFieldRef.type, referencedClassNames)
584+ }
585+ annotated.methodIr.tryRegions.forEach { tryRegion ->
586+ tryRegion.handlers.forEach { handler ->
587+ handler.catchType?.let { catchType ->
588+ collectClassNamesFromDescriptor(annotated.emittedDescriptor(catchType), referencedClassNames)
589+ }
590+ }
591+ }
592+ annotated.methodIr.handlers.forEach { handler ->
593+ handler.caughtTypes.forEach { caughtType ->
594+ collectClassNamesFromDescriptor(annotated.emittedDescriptor(caughtType), referencedClassNames)
595+ }
596+ }
597+ annotated.renameResult.blocks.forEach { block ->
598+ block.stmts.forEach { stmt ->
599+ stmt.invoke?.let { invoke ->
600+ collectClassNamesFromMethodRef(annotated.emittedMethodRef(invoke.methodRef), referencedClassNames)
601+ }
602+ stmt.fieldRef?.let { fieldRef ->
603+ val emittedFieldRef = annotated.emittedFieldRef(fieldRef)
604+ collectClassNamesFromDescriptor(emittedFieldRef.definingClass, referencedClassNames)
605+ collectClassNamesFromDescriptor(emittedFieldRef.type, referencedClassNames)
606+ }
607+ stmt.imm?.typeValue?.let { typeValue ->
608+ collectClassNamesFromDescriptor(annotated.emittedDescriptor(typeValue), referencedClassNames)
609+ }
610+ }
611+ }
612+ return referencedClassNames
613+ }
614+
615+ private fun collectClassNamesFromMethodRef (methodRef : String , sink : MutableSet <String >) {
616+ collectClassNamesFromDescriptor(parseOwnerClass(methodRef), sink)
617+ parseParams(methodRef).forEach { descriptor ->
618+ collectClassNamesFromDescriptor(descriptor, sink)
619+ }
620+ collectClassNamesFromDescriptor(parseReturnType(methodRef), sink)
621+ }
622+
623+ private fun collectClassNamesFromDescriptor (descriptor : String , sink : MutableSet <String >) {
624+ val normalized = descriptor.trim().trimEnd(' ;' )
625+ when {
626+ normalized.isEmpty() -> Unit
627+ normalized.startsWith(" [" ) -> collectClassNamesFromDescriptor(normalized.substring(1 ), sink)
628+ normalized.length == 1 && normalized[0 ] in " VZBCSIJFD" -> Unit
629+ normalized == " java/lang/Object" || normalized == " java/lang/String" -> Unit
630+ normalized.startsWith(" L" ) -> sink + = normalized.substring(1 )
631+ descriptor.startsWith(" L" ) && descriptor.endsWith(" ;" ) -> sink + = descriptor.substring(1 , descriptor.length - 1 )
632+ ' /' in normalized -> sink + = normalized.removePrefix(" L" )
633+ }
634+ }
635+
636+ private fun internalPackageName (className : String ): String = className.substringBeforeLast(' /' , " " )
637+
638+ private fun topLevelInternalClassName (className : String ): String {
639+ val packageName = internalPackageName(className)
640+ val topLevelSimpleName = className.substringAfterLast(' /' ).substringBefore(' $' )
641+ return if (packageName.isEmpty()) topLevelSimpleName else " $packageName /$topLevelSimpleName "
642+ }
643+
443644 private fun methodsAreContiguousByClass (methods : List <TypeAnnotatedMethod >): Boolean {
444645 val closedClasses = mutableSetOf<String >()
445646 var currentClassName: String? = null
0 commit comments