001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect.testing.features;
018
019import static com.google.common.collect.testing.Helpers.copyToSet;
020import static java.util.Collections.disjoint;
021import static java.util.Collections.unmodifiableList;
022
023import com.google.common.annotations.GwtIncompatible;
024import com.google.errorprone.annotations.CanIgnoreReturnValue;
025import java.lang.annotation.Annotation;
026import java.lang.reflect.AnnotatedElement;
027import java.lang.reflect.Method;
028import java.util.ArrayDeque;
029import java.util.ArrayList;
030import java.util.HashMap;
031import java.util.LinkedHashSet;
032import java.util.List;
033import java.util.Locale;
034import java.util.Map;
035import java.util.Queue;
036import java.util.Set;
037import org.jspecify.annotations.NullMarked;
038
039/**
040 * Utilities for collecting and validating tester requirements from annotations.
041 *
042 * @author George van den Driessche
043 */
044@GwtIncompatible
045public final class FeatureUtil {
046  /** A cache of annotated objects (typically a Class or Method) to its set of annotations. */
047  private static final Map<AnnotatedElement, List<Annotation>> annotationCache = new HashMap<>();
048
049  private static final Map<Class<?>, TesterRequirements> classTesterRequirementsCache =
050      new HashMap<>();
051
052  private static final Map<Method, TesterRequirements> methodTesterRequirementsCache =
053      new HashMap<>();
054
055  /**
056   * Given a set of features, add to it all the features directly or indirectly implied by any of
057   * them, and return it.
058   *
059   * @param features the set of features to expand
060   * @return the same set of features, expanded with all implied features
061   */
062  @CanIgnoreReturnValue
063  public static Set<Feature<?>> addImpliedFeatures(Set<Feature<?>> features) {
064    Queue<Feature<?>> queue = new ArrayDeque<>(features);
065    while (!queue.isEmpty()) {
066      Feature<?> feature = queue.remove();
067      for (Feature<?> implied : feature.getImpliedFeatures()) {
068        if (features.add(implied)) {
069          queue.add(implied);
070        }
071      }
072    }
073    return features;
074  }
075
076  /**
077   * Given a set of features, return a new set of all features directly or indirectly implied by any
078   * of them.
079   *
080   * @param features the set of features whose implications to find
081   * @return the implied set of features
082   */
083  public static Set<Feature<?>> impliedFeatures(Set<Feature<?>> features) {
084    Set<Feature<?>> impliedSet = new LinkedHashSet<>();
085    Queue<Feature<?>> queue = new ArrayDeque<>(features);
086    while (!queue.isEmpty()) {
087      Feature<?> feature = queue.remove();
088      for (Feature<?> implied : feature.getImpliedFeatures()) {
089        if (!features.contains(implied) && impliedSet.add(implied)) {
090          queue.add(implied);
091        }
092      }
093    }
094    return impliedSet;
095  }
096
097  /**
098   * Get the full set of requirements for a tester class.
099   *
100   * @param testerClass a tester class
101   * @return all the constraints implicitly or explicitly required by the class or any of its
102   *     superclasses.
103   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
104   */
105  public static TesterRequirements getTesterRequirements(Class<?> testerClass)
106      throws ConflictingRequirementsException {
107    synchronized (classTesterRequirementsCache) {
108      TesterRequirements requirements = classTesterRequirementsCache.get(testerClass);
109      if (requirements == null) {
110        requirements = buildTesterRequirements(testerClass);
111        classTesterRequirementsCache.put(testerClass, requirements);
112      }
113      return requirements;
114    }
115  }
116
117  /**
118   * Get the full set of requirements for a tester class.
119   *
120   * @param testerMethod a test method of a tester class
121   * @return all the constraints implicitly or explicitly required by the method, its declaring
122   *     class, or any of its superclasses.
123   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
124   */
125  public static TesterRequirements getTesterRequirements(Method testerMethod)
126      throws ConflictingRequirementsException {
127    synchronized (methodTesterRequirementsCache) {
128      TesterRequirements requirements = methodTesterRequirementsCache.get(testerMethod);
129      if (requirements == null) {
130        requirements = buildTesterRequirements(testerMethod);
131        methodTesterRequirementsCache.put(testerMethod, requirements);
132      }
133      return requirements;
134    }
135  }
136
137  /**
138   * Construct the full set of requirements for a tester class.
139   *
140   * @param testerClass a tester class
141   * @return all the constraints implicitly or explicitly required by the class or any of its
142   *     superclasses.
143   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
144   */
145  static TesterRequirements buildTesterRequirements(Class<?> testerClass)
146      throws ConflictingRequirementsException {
147    TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerClass);
148    Class<?> baseClass = testerClass.getSuperclass();
149    if (baseClass == null) {
150      return declaredRequirements;
151    } else {
152      TesterRequirements clonedBaseRequirements =
153          new TesterRequirements(getTesterRequirements(baseClass));
154      return incorporateRequirements(clonedBaseRequirements, declaredRequirements, testerClass);
155    }
156  }
157
158  /**
159   * Construct the full set of requirements for a tester method.
160   *
161   * @param testerMethod a test method of a tester class
162   * @return all the constraints implicitly or explicitly required by the method, its declaring
163   *     class, or any of its superclasses.
164   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
165   */
166  static TesterRequirements buildTesterRequirements(Method testerMethod)
167      throws ConflictingRequirementsException {
168    TesterRequirements clonedClassRequirements =
169        new TesterRequirements(getTesterRequirements(testerMethod.getDeclaringClass()));
170    TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerMethod);
171    return incorporateRequirements(clonedClassRequirements, declaredRequirements, testerMethod);
172  }
173
174  /**
175   * Find all the constraints explicitly or implicitly specified by a single tester annotation.
176   *
177   * @param testerAnnotation a tester annotation
178   * @return the requirements specified by the annotation
179   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
180   */
181  private static TesterRequirements buildTesterRequirements(Annotation testerAnnotation)
182      throws ConflictingRequirementsException {
183    Class<? extends Annotation> annotationClass = testerAnnotation.annotationType();
184    Feature<?>[] presentFeatures;
185    Feature<?>[] absentFeatures;
186    try {
187      presentFeatures = (Feature<?>[]) annotationClass.getMethod("value").invoke(testerAnnotation);
188      absentFeatures = (Feature<?>[]) annotationClass.getMethod("absent").invoke(testerAnnotation);
189    } catch (Exception e) {
190      throw new IllegalArgumentException("Error extracting features from tester annotation.", e);
191    }
192    Set<Feature<?>> allPresentFeatures = addImpliedFeatures(copyToSet(presentFeatures));
193    Set<Feature<?>> allAbsentFeatures = copyToSet(absentFeatures);
194    if (!disjoint(allPresentFeatures, allAbsentFeatures)) {
195      throw new ConflictingRequirementsException(
196          "Annotation explicitly or "
197              + "implicitly requires one or more features to be both present "
198              + "and absent.",
199          intersection(allPresentFeatures, allAbsentFeatures),
200          testerAnnotation);
201    }
202    return new TesterRequirements(allPresentFeatures, allAbsentFeatures);
203  }
204
205  /**
206   * Construct the set of requirements specified by annotations directly on a tester class or
207   * method.
208   *
209   * @param classOrMethod a tester class or a test method thereof
210   * @return all the constraints implicitly or explicitly required by annotations on the class or
211   *     method.
212   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
213   */
214  public static TesterRequirements buildDeclaredTesterRequirements(AnnotatedElement classOrMethod)
215      throws ConflictingRequirementsException {
216    TesterRequirements requirements = new TesterRequirements();
217
218    Iterable<Annotation> testerAnnotations = getTesterAnnotations(classOrMethod);
219    for (Annotation testerAnnotation : testerAnnotations) {
220      TesterRequirements moreRequirements = buildTesterRequirements(testerAnnotation);
221      incorporateRequirements(requirements, moreRequirements, testerAnnotation);
222    }
223
224    return requirements;
225  }
226
227  /**
228   * Find all the tester annotations declared on a tester class or method.
229   *
230   * @param classOrMethod a class or method whose tester annotations to find
231   * @return an iterable sequence of tester annotations on the class
232   */
233  public static Iterable<Annotation> getTesterAnnotations(AnnotatedElement classOrMethod) {
234    synchronized (annotationCache) {
235      List<Annotation> annotations = annotationCache.get(classOrMethod);
236      if (annotations == null) {
237        annotations = new ArrayList<>();
238        for (Annotation a : classOrMethod.getDeclaredAnnotations()) {
239          /*
240           * We avoid reflecting on NullMarked because its @Target(..., MODULE) causes problems
241           * under JDK 8.
242           */
243          if (!(a instanceof NullMarked)
244              && a.annotationType().isAnnotationPresent(TesterAnnotation.class)) {
245            annotations.add(a);
246          }
247        }
248        annotations = unmodifiableList(annotations);
249        annotationCache.put(classOrMethod, annotations);
250      }
251      return annotations;
252    }
253  }
254
255  /**
256   * Incorporate additional requirements into an existing requirements object.
257   *
258   * @param requirements the existing requirements object
259   * @param moreRequirements more requirements to incorporate
260   * @param source the source of the additional requirements (used only for error reporting)
261   * @return the existing requirements object, modified to include the additional requirements
262   * @throws ConflictingRequirementsException if the additional requirements are inconsistent with
263   *     the existing requirements
264   */
265  @CanIgnoreReturnValue
266  private static TesterRequirements incorporateRequirements(
267      TesterRequirements requirements, TesterRequirements moreRequirements, Object source)
268      throws ConflictingRequirementsException {
269    Set<Feature<?>> presentFeatures = requirements.getPresentFeatures();
270    Set<Feature<?>> absentFeatures = requirements.getAbsentFeatures();
271    Set<Feature<?>> morePresentFeatures = moreRequirements.getPresentFeatures();
272    Set<Feature<?>> moreAbsentFeatures = moreRequirements.getAbsentFeatures();
273    checkConflict("absent", absentFeatures, "present", morePresentFeatures, source);
274    checkConflict("present", presentFeatures, "absent", moreAbsentFeatures, source);
275    presentFeatures.addAll(morePresentFeatures);
276    absentFeatures.addAll(moreAbsentFeatures);
277    return requirements;
278  }
279
280  // Used by incorporateRequirements() only
281  private static void checkConflict(
282      String earlierRequirement,
283      Set<Feature<?>> earlierFeatures,
284      String newRequirement,
285      Set<Feature<?>> newFeatures,
286      Object source)
287      throws ConflictingRequirementsException {
288    if (!disjoint(newFeatures, earlierFeatures)) {
289      throw new ConflictingRequirementsException(
290          String.format(
291              Locale.ROOT,
292              "Annotation requires to be %s features that earlier "
293                  + "annotations required to be %s.",
294              newRequirement,
295              earlierRequirement),
296          intersection(newFeatures, earlierFeatures),
297          source);
298    }
299  }
300
301  /**
302   * Construct a new {@link java.util.Set} that is the intersection of the given sets.
303   *
304   * @deprecated Use {@link com.google.common.collect.Sets#intersection(Set, Set)} instead.
305   */
306  @Deprecated
307  public static <T> Set<T> intersection(Set<? extends T> set1, Set<? extends T> set2) {
308    Set<T> result = copyToSet(set1);
309    result.retainAll(set2);
310    return result;
311  }
312
313  private FeatureUtil() {}
314}