• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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