KotlinSerializableFilter.java

/*******************************************************************************
 * Copyright (c) 2009, 2026 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
 * https://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.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodNode;

/**
 * Filters methods generated by <a href=
 * "https://github.com/JetBrains/kotlin/tree/v2.1.20/plugins/kotlinx-serialization">Kotlin
 * serialization compiler plugin</a>.
 */
final class KotlinSerializableFilter implements IFilter {

	private int lineNumber = -2;

	public void filter(final MethodNode methodNode,
			final IFilterContext context, final IFilterOutput output) {
		if (context.getClassName().endsWith("$Companion")
				&& methodNode.name.equals("<init>")
				&& methodNode.desc.equals("()V")) {
			lineNumber = -1;
			for (final AbstractInsnNode i : methodNode.instructions) {
				if (i.getType() == AbstractInsnNode.LINE) {
					lineNumber = ((LineNumberNode) i).line;
					return;
				}
			}
			return;
		}

		if (methodNode.name.equals("serializer") && methodNode.desc
				.equals("()Lkotlinx/serialization/KSerializer;")) {
			final Matcher matcher = new Matcher();
			if (matcher.matchSerializer(methodNode, lineNumber)
					|| matcher.matchCachedSerializer(methodNode,
							context.getClassName())) {
				output.ignore(methodNode.instructions.getFirst(),
						methodNode.instructions.getLast());
			}
			return;
		}

		if ((methodNode.access & Opcodes.ACC_SYNTHETIC) == 0) {
			return;
		}

		if (methodNode.name.startsWith("write$Self$")) {
			output.ignore(methodNode.instructions.getFirst(),
					methodNode.instructions.getLast());
			return;
		}

		if ((methodNode.name.equals("get$cachedSerializer")
				// https://github.com/JetBrains/kotlin/blob/v2.2.20/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrBuilderWithPluginContext.kt#L61
				|| methodNode.name.startsWith("_init_$_anonymous_")
				|| methodNode.name.startsWith("_childSerializers$_anonymous_"))
				&& methodNode.desc
						.equals("()Lkotlinx/serialization/KSerializer;")) {
			output.ignore(methodNode.instructions.getFirst(),
					methodNode.instructions.getLast());
			return;
		}

		final Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc);
		if (argumentTypes.length > 1 && argumentTypes[argumentTypes.length - 1]
				.getClassName()
				.equals("kotlinx.serialization.internal.SerializationConstructorMarker")) {
			output.ignore(methodNode.instructions.getFirst(),
					methodNode.instructions.getLast());
		}
	}

	private static class Matcher extends AbstractMatcher {
		public boolean matchSerializer(final MethodNode methodNode,
				final int lineNumber) {
			cursor = methodNode.instructions.getFirst();
			nextIs(Opcodes.GETSTATIC);
			final FieldInsnNode getStaticInstruction = (FieldInsnNode) cursor;
			if (cursor == null) {
				return false;
			}
			final AbstractInsnNode lineNumberInstruction = cursor.getPrevious();
			nextIsType(Opcodes.CHECKCAST, "kotlinx/serialization/KSerializer");
			nextIs(Opcodes.ARETURN);
			return cursor != null
					&& getStaticInstruction.name.equals("INSTANCE")
					&& (lineNumberInstruction instanceof LineNumberNode)
					&& (lineNumber == -1
							|| lineNumber == ((LineNumberNode) lineNumberInstruction).line);
		}

		private boolean matchCachedSerializer(final MethodNode methodNode,
				final String className) {
			firstIsALoad0(methodNode);
			nextIsInvoke(Opcodes.INVOKESPECIAL, className,
					"get$cachedSerializer",
					"()Lkotlinx/serialization/KSerializer;");
			nextIs(Opcodes.ARETURN);
			return cursor != null;
		}
	}

}