From 96883a1fa39f29146144f9ccf2e43e9932dca6a1 Mon Sep 17 00:00:00 2001 From: Rafael Winterhalter Date: Thu, 19 Aug 2021 16:37:52 +0200 Subject: [PATCH] Add checks for sealed types (#2392) --- .../bytebuddy/InlineBytecodeGenerator.java | 4 +- .../bytebuddy/SubclassByteBuddyMockMaker.java | 7 +++- .../creation/bytebuddy/TypeSupport.java | 42 +++++++++++++++++++ .../SubclassByteBuddyMockMakerTest.java | 29 +++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/mockito/internal/creation/bytebuddy/TypeSupport.java diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java index 78c9d440a1..077bf9780d 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java @@ -344,7 +344,9 @@ private void checkSupportedCombination( if (subclassingRequired && !features.mockedType.isArray() && !features.mockedType.isPrimitive() - && Modifier.isFinal(features.mockedType.getModifiers())) { + && (Modifier.isFinal(features.mockedType.getModifiers()) + || TypeSupport.INSTANCE.isSealed(features.mockedType) + || features.interfaces.stream().anyMatch(TypeSupport.INSTANCE::isSealed))) { throw new MockitoException( "Unsupported settings with this type '" + features.mockedType.getName() + "'"); } diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java index dc1e81752c..533e2872df 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java @@ -162,7 +162,9 @@ public TypeMockability isTypeMockable(final Class type) { return new TypeMockability() { @Override public boolean mockable() { - return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers()); + return !type.isPrimitive() + && !Modifier.isFinal(type.getModifiers()) + && !TypeSupport.INSTANCE.isSealed(type); } @Override @@ -176,6 +178,9 @@ public String nonMockableReason() { if (Modifier.isFinal(type.getModifiers())) { return "final class"; } + if (TypeSupport.INSTANCE.isSealed(type)) { + return "sealed class"; + } return join("not handled type"); } }; diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/TypeSupport.java b/src/main/java/org/mockito/internal/creation/bytebuddy/TypeSupport.java new file mode 100644 index 0000000000..17df601f9b --- /dev/null +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/TypeSupport.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation.bytebuddy; + +import org.mockito.exceptions.base.MockitoException; + +import java.lang.reflect.Method; + +class TypeSupport { + + static final TypeSupport INSTANCE; + + static { + Method isSealed; + try { + isSealed = Class.class.getMethod("isSealed"); + } catch (NoSuchMethodException ignored) { + isSealed = null; + } + INSTANCE = new TypeSupport(isSealed); + } + + private final Method isSealed; + + private TypeSupport(Method isSealed) { + this.isSealed = isSealed; + } + + boolean isSealed(Class type) { + if (isSealed == null) { + return false; + } + try { + return (boolean) isSealed.invoke(type); + } catch (Throwable t) { + throw new MockitoException( + "Failed to check if type is sealed using handle " + isSealed, t); + } + } +} diff --git a/src/test/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMakerTest.java b/src/test/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMakerTest.java index 35c74d6bd4..154fc9a856 100644 --- a/src/test/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMakerTest.java +++ b/src/test/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMakerTest.java @@ -11,6 +11,10 @@ import java.util.Observable; import java.util.Observer; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.description.modifier.TypeManifestation; +import net.bytebuddy.dynamic.DynamicType; import org.junit.Test; import org.mockito.internal.creation.MockSettingsImpl; import org.mockito.plugins.MockMaker; @@ -29,6 +33,31 @@ public void is_type_mockable_excludes_primitive_wrapper_classes() { assertThat(mockable.nonMockableReason()).contains("final"); } + @Test + public void is_type_mockable_excludes_sealed_classes() { + // is only supported on Java 17 and later + if (ClassFileVersion.ofThisVm().isAtMost(ClassFileVersion.JAVA_V16)) { + return; + } + DynamicType.Builder base = new ByteBuddy().subclass(Object.class); + DynamicType.Unloaded dynamic = + new ByteBuddy() + .subclass(Object.class) + .permittedSubclass(base.toTypeDescription()) + .make(); + Class type = + new ByteBuddy() + .subclass(base.toTypeDescription()) + .merge(TypeManifestation.FINAL) + .make() + .include(dynamic) + .load(null) + .getLoaded(); + MockMaker.TypeMockability mockable = mockMaker.isTypeMockable(type); + assertThat(mockable.mockable()).isFalse(); + assertThat(mockable.nonMockableReason()).contains("sealed"); + } + @Test public void is_type_mockable_excludes_primitive_classes() { MockMaker.TypeMockability mockable = mockMaker.isTypeMockable(int.class);