KotlinComposeFilter.java

/*******************************************************************************
 * Copyright (c) 2009, 2024 Mountainminds GmbH & Co. KG and Contributors
 * This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Evgeny Mandrikov - initial API and implementation
 *
 *******************************************************************************/
package org.jacoco.core.internal.analysis.filter;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

/**
 * Filters bytecode generated by Compose Kotlin compiler plugin.
 */
final class KotlinComposeFilter implements IFilter {

	public void filter(final MethodNode methodNode,
			final IFilterContext context, final IFilterOutput output) {
		if (!KotlinGeneratedFilter.isKotlinClass(context)) {
			return;
		}
		if (!isComposable(methodNode)) {
			return;
		}
		for (final AbstractInsnNode i : methodNode.instructions) {
			if (i.getType() != AbstractInsnNode.METHOD_INSN) {
				continue;
			}
			final MethodInsnNode mi = (MethodInsnNode) i;
			if ("androidx/compose/runtime/Composer".equals(mi.owner)
					&& "getSkipping".equals(mi.name) && "()Z".equals(mi.desc)
					&& mi.getNext().getOpcode() == Opcodes.IFNE) {
				// https://github.com/JetBrains/kotlin/blob/v2.0.0-RC2/plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#L361-L384
				final JumpInsnNode ji = (JumpInsnNode) mi.getNext();
				output.ignore(methodNode.instructions.getFirst(), ji);
				output.ignore(ji.label, methodNode.instructions.getLast());
			} else if ("androidx/compose/runtime/Composer".equals(mi.owner)
					&& "endRestartGroup".equals(mi.name)
					&& "()Landroidx/compose/runtime/ScopeUpdateScope;"
							.equals(mi.desc)
					&& mi.getNext().getOpcode() == Opcodes.DUP
					&& mi.getNext().getNext().getOpcode() == Opcodes.IFNULL) {
				// https://github.com/JetBrains/kotlin/blob/v2.0.0-RC2/plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#L430-L450
				final JumpInsnNode ji = (JumpInsnNode) mi.getNext().getNext();
				final AbstractInsnNode jumpTarget = AbstractMatcher
						.skipNonOpcodes(ji.label);
				if (jumpTarget.getOpcode() == Opcodes.POP) {
					output.ignore(ji, jumpTarget);
				}
			} else if ("androidx/compose/runtime/ComposerKt".equals(mi.owner)
					&& "isTraceInProgress".equals(mi.name)
					&& "()Z".equals(mi.desc)
					&& mi.getNext().getOpcode() == Opcodes.IFEQ) {
				// https://github.com/JetBrains/kotlin/blob/v2.0.0-RC2/plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#L2123-L2163
				final JumpInsnNode ji = (JumpInsnNode) mi.getNext();
				output.ignore(ji, ji.label);
			}
		}
	}

	private static boolean isComposable(final MethodNode methodNode) {
		if (methodNode.invisibleAnnotations == null) {
			return false;
		}
		for (final AnnotationNode a : methodNode.invisibleAnnotations) {
			if ("Landroidx/compose/runtime/Composable;".equals(a.desc)) {
				return true;
			}
		}
		return false;
	}

}