Skip to content

Commit 1b069cb

Browse files
committed
fix: add ability to re-write class name references in desugar plugin
Signed-off-by: Akash Yadav <akashyadav@appdevforall.org>
1 parent 08a2184 commit 1b069cb

9 files changed

Lines changed: 670 additions & 459 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.itsaky.androidide.desugaring
2+
3+
import org.objectweb.asm.Label
4+
import org.objectweb.asm.MethodVisitor
5+
import org.objectweb.asm.Type
6+
7+
/**
8+
* Replaces all bytecode references to one or more classes within a method body.
9+
*
10+
* Covered visit sites:
11+
* - [visitMethodInsn] — owner and embedded descriptor
12+
* - [visitFieldInsn] — owner and field descriptor
13+
* - [visitTypeInsn] — NEW / CHECKCAST / INSTANCEOF / ANEWARRAY operand
14+
* - [visitLdcInsn] — class-literal Type constants
15+
* - [visitLocalVariable] — local variable descriptor and generic signature
16+
* - [visitMultiANewArrayInsn]— array descriptor
17+
* - [visitTryCatchBlock] — caught exception type
18+
*
19+
* @param classReplacements Mapping from source internal name (slash-notation)
20+
* to target internal name (slash-notation). An empty map is a no-op.
21+
*
22+
* @author Akash Yadav
23+
*/
24+
class ClassRefReplacingMethodVisitor(
25+
api: Int,
26+
mv: MethodVisitor?,
27+
private val classReplacements: Map<String, String>,
28+
) : MethodVisitor(api, mv) {
29+
30+
override fun visitMethodInsn(
31+
opcode: Int,
32+
owner: String,
33+
name: String,
34+
descriptor: String,
35+
isInterface: Boolean,
36+
) {
37+
super.visitMethodInsn(
38+
opcode,
39+
replace(owner),
40+
name,
41+
replaceInDescriptor(descriptor),
42+
isInterface,
43+
)
44+
}
45+
46+
override fun visitFieldInsn(
47+
opcode: Int,
48+
owner: String,
49+
name: String,
50+
descriptor: String,
51+
) {
52+
super.visitFieldInsn(
53+
opcode,
54+
replace(owner),
55+
name,
56+
replaceInDescriptor(descriptor),
57+
)
58+
}
59+
60+
override fun visitTypeInsn(opcode: Int, type: String) {
61+
super.visitTypeInsn(opcode, replace(type))
62+
}
63+
64+
override fun visitLdcInsn(value: Any?) {
65+
// Replace class-literal constants: Foo.class → Bar.class
66+
if (value is Type && value.sort == Type.OBJECT) {
67+
val replaced = replace(value.internalName)
68+
if (replaced !== value.internalName) {
69+
super.visitLdcInsn(Type.getObjectType(replaced))
70+
return
71+
}
72+
}
73+
super.visitLdcInsn(value)
74+
}
75+
76+
override fun visitLocalVariable(
77+
name: String,
78+
descriptor: String,
79+
signature: String?,
80+
start: Label,
81+
end: Label,
82+
index: Int,
83+
) {
84+
super.visitLocalVariable(
85+
name,
86+
replaceInDescriptor(descriptor),
87+
replaceInSignature(signature),
88+
start,
89+
end,
90+
index,
91+
)
92+
}
93+
94+
override fun visitMultiANewArrayInsn(descriptor: String, numDimensions: Int) {
95+
super.visitMultiANewArrayInsn(replaceInDescriptor(descriptor), numDimensions)
96+
}
97+
98+
override fun visitTryCatchBlock(
99+
start: Label,
100+
end: Label,
101+
handler: Label,
102+
type: String?,
103+
) {
104+
super.visitTryCatchBlock(start, end, handler, type?.let { replace(it) })
105+
}
106+
107+
// -------------------------------------------------------------------------
108+
// Helpers
109+
// -------------------------------------------------------------------------
110+
111+
/** Replaces a bare internal class name (slash-notation). */
112+
private fun replace(internalName: String): String =
113+
classReplacements[internalName] ?: internalName
114+
115+
/**
116+
* Substitutes every `L<from>;` token in a JVM descriptor or generic
117+
* signature with `L<to>;`.
118+
*/
119+
private fun replaceInDescriptor(descriptor: String): String {
120+
if (classReplacements.isEmpty()) return descriptor
121+
var result = descriptor
122+
for ((from, to) in classReplacements) {
123+
result = result.replace("L$from;", "L$to;")
124+
}
125+
return result
126+
}
127+
128+
/** Delegates to [replaceInDescriptor]; returns `null` for `null` input. */
129+
private fun replaceInSignature(signature: String?): String? =
130+
signature?.let { replaceInDescriptor(it) }
131+
}
Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,106 @@
1-
/*
2-
* This file is part of AndroidIDE.
3-
*
4-
* AndroidIDE is free software: you can redistribute it and/or modify
5-
* it under the terms of the GNU General Public License as published by
6-
* the Free Software Foundation, either version 3 of the License, or
7-
* (at your option) any later version.
8-
*
9-
* AndroidIDE is distributed in the hope that it will be useful,
10-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12-
* GNU General Public License for more details.
13-
*
14-
* You should have received a copy of the GNU General Public License
15-
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
16-
*/
17-
181
package com.itsaky.androidide.desugaring
192

203
import com.android.build.api.instrumentation.ClassContext
214
import org.objectweb.asm.ClassVisitor
5+
import org.objectweb.asm.FieldVisitor
226
import org.objectweb.asm.MethodVisitor
237

248
/**
259
* [ClassVisitor] implementation for desugaring.
2610
*
11+
* Applies two transformations to every method body, in priority order:
12+
*
13+
* 1. **[DesugarMethodVisitor]** (outermost / highest priority) — fine-grained
14+
* per-method-call replacement defined via [DesugarReplacementsContainer.replaceMethod].
15+
* Its output flows into the next layer.
16+
*
17+
* 2. **[ClassRefReplacingMethodVisitor]** (innermost) — bulk class-reference
18+
* replacement defined via [DesugarReplacementsContainer.replaceClass].
19+
* Handles every site where a class name can appear in a method body.
20+
*
21+
* Class references that appear in field and method *declarations* (descriptors
22+
* and generic signatures at the class-structure level) are also rewritten here.
23+
*
2724
* @author Akash Yadav
2825
*/
29-
class DesugarClassVisitor(private val params: DesugarParams,
30-
private val classContext: ClassContext, api: Int,
31-
classVisitor: ClassVisitor
26+
class DesugarClassVisitor(
27+
private val params: DesugarParams,
28+
private val classContext: ClassContext,
29+
api: Int,
30+
classVisitor: ClassVisitor,
3231
) : ClassVisitor(api, classVisitor) {
3332

34-
override fun visitMethod(access: Int, name: String?, descriptor: String?,
35-
signature: String?, exceptions: Array<out String>?
36-
): MethodVisitor {
37-
return DesugarMethodVisitor(params, classContext, api,
38-
super.visitMethod(access, name, descriptor, signature, exceptions))
39-
}
40-
}
33+
/**
34+
* Class replacement map in ASM internal (slash) notation.
35+
* Derived lazily from the dot-notation map stored in [params].
36+
*/
37+
private val slashClassReplacements: Map<String, String> by lazy {
38+
params.classReplacements.get()
39+
.entries.associate { (from, to) ->
40+
from.replace('.', '/') to to.replace('.', '/')
41+
}
42+
}
43+
44+
// -------------------------------------------------------------------------
45+
// Class-structure level: rewrite descriptors in field / method declarations
46+
// -------------------------------------------------------------------------
47+
48+
override fun visitField(
49+
access: Int,
50+
name: String,
51+
descriptor: String,
52+
signature: String?,
53+
value: Any?,
54+
): FieldVisitor? = super.visitField(
55+
access,
56+
name,
57+
replaceInDescriptor(descriptor),
58+
replaceInSignature(signature),
59+
value,
60+
)
61+
62+
override fun visitMethod(
63+
access: Int,
64+
name: String?,
65+
descriptor: String?,
66+
signature: String?,
67+
exceptions: Array<out String>?,
68+
): MethodVisitor {
69+
// Rewrite the method's own descriptor/signature at the class-structure level.
70+
val base = super.visitMethod(
71+
access,
72+
name,
73+
descriptor?.let { replaceInDescriptor(it) },
74+
replaceInSignature(signature),
75+
exceptions,
76+
)
77+
78+
// Layer 1 — class-reference replacement inside the method body.
79+
// Skip instantiation entirely when there are no class replacements.
80+
val withClassRefs: MethodVisitor = when {
81+
slashClassReplacements.isNotEmpty() ->
82+
ClassRefReplacingMethodVisitor(api, base, slashClassReplacements)
83+
else -> base
84+
}
85+
86+
// Layer 2 — fine-grained method-call replacement.
87+
// Runs first; any instruction it emits flows through withClassRefs.
88+
return DesugarMethodVisitor(params, classContext, api, withClassRefs)
89+
}
90+
91+
// -------------------------------------------------------------------------
92+
// Descriptor / signature helpers
93+
// -------------------------------------------------------------------------
94+
95+
private fun replaceInDescriptor(descriptor: String): String {
96+
if (slashClassReplacements.isEmpty()) return descriptor
97+
var result = descriptor
98+
for ((from, to) in slashClassReplacements) {
99+
result = result.replace("L$from;", "L$to;")
100+
}
101+
return result
102+
}
41103

104+
private fun replaceInSignature(signature: String?): String? =
105+
signature?.let { replaceInDescriptor(it) }
106+
}
Lines changed: 45 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,3 @@
1-
/*
2-
* This file is part of AndroidIDE.
3-
*
4-
* AndroidIDE is free software: you can redistribute it and/or modify
5-
* it under the terms of the GNU General Public License as published by
6-
* the Free Software Foundation, either version 3 of the License, or
7-
* (at your option) any later version.
8-
*
9-
* AndroidIDE is distributed in the hope that it will be useful,
10-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12-
* GNU General Public License for more details.
13-
*
14-
* You should have received a copy of the GNU General Public License
15-
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
16-
*/
17-
181
package com.itsaky.androidide.desugaring
192

203
import com.android.build.api.instrumentation.AsmClassVisitorFactory
@@ -28,51 +11,49 @@ import org.slf4j.LoggerFactory
2811
*
2912
* @author Akash Yadav
3013
*/
31-
abstract class DesugarClassVisitorFactory :
32-
AsmClassVisitorFactory<DesugarParams> {
33-
34-
companion object {
35-
36-
private val log =
37-
LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java)
38-
}
39-
40-
override fun createClassVisitor(classContext: ClassContext,
41-
nextClassVisitor: ClassVisitor
42-
): ClassVisitor {
43-
val params = parameters.orNull
44-
if (params == null) {
45-
log.warn("Could not find desugaring parameters. Disabling desugaring.")
46-
return nextClassVisitor
47-
}
48-
49-
return DesugarClassVisitor(params, classContext,
50-
instrumentationContext.apiVersion.get(), nextClassVisitor)
51-
}
52-
53-
override fun isInstrumentable(classData: ClassData): Boolean {
54-
val params = parameters.orNull
55-
if (params == null) {
56-
log.warn("Could not find desugaring parameters. Disabling desugaring.")
57-
return false
58-
}
59-
60-
val isEnabled = params.enabled.get().also { isEnabled ->
61-
log.debug("Is desugaring enabled: $isEnabled")
62-
}
63-
64-
if (!isEnabled) {
65-
return false
66-
}
67-
68-
val includedPackages = params.includedPackages.get()
69-
if (includedPackages.isNotEmpty()) {
70-
val className = classData.className
71-
if (!includedPackages.any { className.startsWith(it) }) {
72-
return false
73-
}
74-
}
75-
76-
return true
77-
}
14+
abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory<DesugarParams> {
15+
16+
companion object {
17+
private val log =
18+
LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java)
19+
}
20+
21+
private val desugarParams: DesugarParams?
22+
get() = parameters.orNull ?: run {
23+
log.warn("Could not find desugaring parameters. Disabling desugaring.")
24+
null
25+
}
26+
27+
override fun createClassVisitor(
28+
classContext: ClassContext,
29+
nextClassVisitor: ClassVisitor,
30+
): ClassVisitor {
31+
val params = desugarParams ?: return nextClassVisitor
32+
return DesugarClassVisitor(
33+
params = params,
34+
classContext = classContext,
35+
api = instrumentationContext.apiVersion.get(),
36+
classVisitor = nextClassVisitor,
37+
)
38+
}
39+
40+
override fun isInstrumentable(classData: ClassData): Boolean {
41+
val params = desugarParams ?: return false
42+
43+
val isEnabled = params.enabled.get().also { log.debug("Is desugaring enabled: $it") }
44+
if (!isEnabled) return false
45+
46+
// Class-reference replacement must scan every class — any class may
47+
// contain a reference to the one being replaced, regardless of package.
48+
if (params.classReplacements.get().isNotEmpty()) return true
49+
50+
val includedPackages = params.includedPackages.get()
51+
if (includedPackages.isNotEmpty()) {
52+
if (!includedPackages.any { classData.className.startsWith(it) }) {
53+
return false
54+
}
55+
}
56+
57+
return true
58+
}
7859
}

0 commit comments

Comments
 (0)