Skip to content

Commit f609808

Browse files
committed
fix: synthesize minimal unresolved class stubs for main sources
1 parent 7c52196 commit f609808

1 file changed

Lines changed: 204 additions & 3 deletions

File tree

  • amber-pipeline/src/main/kotlin/dev/amber/pipeline/codegen

amber-pipeline/src/main/kotlin/dev/amber/pipeline/codegen/JavaEmitter.kt

Lines changed: 204 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import dev.amber.core.model.SourcePartitionKind
1212
import dev.amber.core.model.StmtIr
1313
import dev.amber.core.model.TermIr
1414
import dev.amber.core.model.placeholderReasons
15+
import dev.amber.pipeline.dex.SourcePartitioner
1516
import dev.amber.pipeline.dex.TypeAnnotatedMethod
1617
import dev.amber.pipeline.mapping.MappingRenameIndex
1718
import 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

Comments
 (0)