Skip to content

Commit 2eb2d4c

Browse files
authored
Merge branch 'stage' into fix/ADFA-3489-overlay-permissions
2 parents 338ab77 + d300cff commit 2eb2d4c

File tree

8 files changed

+614
-382
lines changed

8 files changed

+614
-382
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
/** Replaces a bare internal class name (slash-notation). */
108+
private fun replace(internalName: String): String =
109+
classReplacements[internalName] ?: internalName
110+
111+
/**
112+
* Substitutes every `L<from>;` token in a JVM descriptor or generic
113+
* signature with `L<to>;`.
114+
*/
115+
private fun replaceInDescriptor(descriptor: String): String {
116+
if (classReplacements.isEmpty()) return descriptor
117+
var result = descriptor
118+
for ((from, to) in classReplacements) {
119+
result = result.replace("L$from;", "L$to;")
120+
}
121+
return result
122+
}
123+
124+
/** Delegates to [replaceInDescriptor]; returns `null` for `null` input. */
125+
private fun replaceInSignature(signature: String?): String? =
126+
signature?.let { replaceInDescriptor(it) }
127+
}

composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,101 @@
1414
* You should have received a copy of the GNU General Public License
1515
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
1616
*/
17-
1817
package com.itsaky.androidide.desugaring
1918

2019
import com.android.build.api.instrumentation.ClassContext
2120
import org.objectweb.asm.ClassVisitor
21+
import org.objectweb.asm.FieldVisitor
2222
import org.objectweb.asm.MethodVisitor
2323

2424
/**
2525
* [ClassVisitor] implementation for desugaring.
2626
*
27+
* Applies two transformations to every method body, in priority order:
28+
*
29+
* 1. **[DesugarMethodVisitor]** (outermost / highest priority) — fine-grained
30+
* per-method-call replacement defined via [DesugarReplacementsContainer.replaceMethod].
31+
* Its output flows into the next layer.
32+
*
33+
* 2. **[ClassRefReplacingMethodVisitor]** (innermost) — bulk class-reference
34+
* replacement defined via [DesugarReplacementsContainer.replaceClass].
35+
* Handles every site where a class name can appear in a method body.
36+
*
37+
* Class references that appear in field and method *declarations* (descriptors
38+
* and generic signatures at the class-structure level) are also rewritten here.
39+
*
2740
* @author Akash Yadav
2841
*/
29-
class DesugarClassVisitor(private val params: DesugarParams,
30-
private val classContext: ClassContext, api: Int,
31-
classVisitor: ClassVisitor
42+
class DesugarClassVisitor(
43+
private val params: DesugarParams,
44+
private val classContext: ClassContext,
45+
api: Int,
46+
classVisitor: ClassVisitor,
3247
) : ClassVisitor(api, classVisitor) {
3348

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

112+
private fun replaceInSignature(signature: String?): String? =
113+
signature?.let { replaceInDescriptor(it) }
114+
}

composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* You should have received a copy of the GNU General Public License
1515
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
1616
*/
17-
1817
package com.itsaky.androidide.desugaring
1918

2019
import com.android.build.api.instrumentation.AsmClassVisitorFactory
@@ -28,51 +27,49 @@ import org.slf4j.LoggerFactory
2827
*
2928
* @author Akash Yadav
3029
*/
31-
abstract class DesugarClassVisitorFactory :
32-
AsmClassVisitorFactory<DesugarParams> {
33-
34-
companion object {
30+
abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory<DesugarParams> {
3531

36-
private val log =
37-
LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java)
38-
}
32+
companion object {
33+
private val log =
34+
LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java)
35+
}
3936

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-
}
37+
private val desugarParams: DesugarParams?
38+
get() = parameters.orNull ?: run {
39+
log.warn("Could not find desugaring parameters. Disabling desugaring.")
40+
null
41+
}
4842

49-
return DesugarClassVisitor(params, classContext,
50-
instrumentationContext.apiVersion.get(), nextClassVisitor)
51-
}
43+
override fun createClassVisitor(
44+
classContext: ClassContext,
45+
nextClassVisitor: ClassVisitor,
46+
): ClassVisitor {
47+
val params = desugarParams ?: return nextClassVisitor
48+
return DesugarClassVisitor(
49+
params = params,
50+
classContext = classContext,
51+
api = instrumentationContext.apiVersion.get(),
52+
classVisitor = nextClassVisitor,
53+
)
54+
}
5255

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-
}
56+
override fun isInstrumentable(classData: ClassData): Boolean {
57+
val params = desugarParams ?: return false
5958

60-
val isEnabled = params.enabled.get().also { isEnabled ->
61-
log.debug("Is desugaring enabled: $isEnabled")
62-
}
59+
val isEnabled = params.enabled.get().also { log.debug("Is desugaring enabled: $it") }
60+
if (!isEnabled) return false
6361

64-
if (!isEnabled) {
65-
return false
66-
}
62+
// Class-reference replacement must scan every class — any class may
63+
// contain a reference to the one being replaced, regardless of package.
64+
if (params.classReplacements.get().isNotEmpty()) return true
6765

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-
}
66+
val includedPackages = params.includedPackages.get()
67+
if (includedPackages.isNotEmpty()) {
68+
if (!includedPackages.any { classData.className.startsWith(it) }) {
69+
return false
70+
}
71+
}
7572

76-
return true
77-
}
73+
return true
74+
}
7875
}

0 commit comments

Comments
 (0)