1 /* 2 * Copyright (C) 2023 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.common.collect; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 import static java.lang.reflect.Modifier.PRIVATE; 21 import static java.lang.reflect.Modifier.PROTECTED; 22 import static java.lang.reflect.Modifier.PUBLIC; 23 import static java.util.Arrays.asList; 24 25 import com.google.common.base.Optional; 26 import com.google.common.reflect.ClassPath; 27 import com.google.common.reflect.ClassPath.ClassInfo; 28 import com.google.common.reflect.TypeToken; 29 import java.lang.reflect.Method; 30 import junit.framework.TestCase; 31 32 /** 33 * Tests that all package-private {@code writeReplace} methods are overridden in any existing 34 * subclasses. Without such overrides, optimizers might put a {@code writeReplace}-containing class 35 * and its subclass in different packages, causing the serialization system to fail to invoke {@code 36 * writeReplace} when serializing an instance of the subclass. For an example of this problem, see 37 * b/310253115. 38 */ 39 public class WriteReplaceOverridesTest extends TestCase { 40 private static final ImmutableSet<String> GUAVA_PACKAGES = 41 FluentIterable.of( 42 "base", 43 "cache", 44 "collect", 45 "escape", 46 "eventbus", 47 "graph", 48 "hash", 49 "html", 50 "io", 51 "math", 52 "net", 53 "primitives", 54 "reflect", 55 "util.concurrent", 56 "xml") 57 .transform("com.google.common."::concat) 58 .toSet(); 59 testClassesHaveOverrides()60 public void testClassesHaveOverrides() throws Exception { 61 for (ClassInfo info : ClassPath.from(getClass().getClassLoader()).getAllClasses()) { 62 if (!GUAVA_PACKAGES.contains(info.getPackageName())) { 63 continue; 64 } 65 if (info.getName().endsWith("GwtSerializationDependencies")) { 66 continue; // These classes exist only for the GWT compiler, not to be used. 67 } 68 if ( 69 /* 70 * At least one of the classes nested inside TypeResolverTest triggers a bug under older JDKs: 71 * https://bugs.openjdk.org/browse/JDK-8215328 -> https://bugs.openjdk.org/browse/JDK-8215470 72 * https://github.com/google/guava/blob/4f12c5891a7adedbaa1d99fc9f77d8cc4e9da206/guava-tests/test/com/google/common/reflect/TypeResolverTest.java#L201 73 */ 74 info.getName().contains("TypeResolverTest") 75 /* 76 * And at least one of the classes inside TypeTokenTest ends up with a null value in 77 * TypeMappingIntrospector.mappings. That happens only under older JDKs, too, so it may 78 * well be a JDK bug. 79 */ 80 || info.getName().contains("TypeTokenTest") 81 /* 82 * "IllegalAccess tried to access class 83 * com.google.common.collect.testing.AbstractIteratorTester from class 84 * com.google.common.collect.MultimapsTest" 85 * 86 * ...when we build with JDK 22 and run under JDK 8. 87 */ 88 || info.getName().contains("MultimapsTest") 89 /* 90 * Luckily, we don't care about analyzing tests at all. We'd skip them all if we could do so 91 * trivially, but it's enough to skip these ones. 92 */ 93 ) { 94 continue; 95 } 96 Class<?> clazz = info.load(); 97 try { 98 Method unused = clazz.getDeclaredMethod("writeReplace"); 99 continue; // It overrides writeReplace, so it's safe. 100 } catch (NoSuchMethodException e) { 101 // This is a class whose supertypes we want to examine. We'll do that below. 102 } 103 Optional<Class<?>> supersWithPackagePrivateWriteReplace = 104 FluentIterable.from(TypeToken.of(clazz).getTypes()) 105 .transform(TypeToken::getRawType) 106 .transformAndConcat(c -> asList(c.getDeclaredMethods())) 107 .firstMatch( 108 m -> 109 m.getName().equals("writeReplace") 110 && m.getParameterTypes().length == 0 111 // Only package-private methods are a problem. 112 && (m.getModifiers() & (PUBLIC | PROTECTED | PRIVATE)) == 0) 113 .transform(Method::getDeclaringClass); 114 if (!supersWithPackagePrivateWriteReplace.isPresent()) { 115 continue; 116 } 117 assertWithMessage( 118 "To help optimizers, any class that inherits a package-private writeReplace() method" 119 + " should override that method.\n" 120 + "(An override that delegates to the supermethod is fine.)\n" 121 + "%s has no such override despite inheriting writeReplace() from %s", 122 clazz.getName(), supersWithPackagePrivateWriteReplace.get().getName()) 123 .fail(); 124 } 125 } 126 } 127