code: private static boolean typesEquivalent(WildcardType typeToMatch, WildcardType type, ResolutionContext ctx) {
          if (typeToMatch.getUpperBounds().length != type.getUpperBounds().length) {
              return false;
          }
  
          if (typeToMatch.getLowerBounds().length != type.getLowerBounds().length) {
              return false;
          }
  
          for (int i = 0; i < typeToMatch.getUpperBounds().length; i++) {
              if (!typesEquivalent(typeToMatch.getUpperBounds()[i], ctx.resolve(type.getUpperBounds()[i]), ctx)) {
                  return false;
              }
          }
  
          for (int i = 0; i < typeToMatch.getLowerBounds().length; i++) {
              if (!typesEquivalent(typeToMatch.getLowerBounds()[i], ctx.resolve(type.getLowerBounds()[i]), ctx)) {
                  return false;
              }
          }
  
          return true;
      }
docs: Computes whether two WildcardTypes are equivalent.
  <p>
  This method will recursively check that the upper and lower wildcard bounds are equivalent
  
  @param typeToMatch the type to match against
  @param type the type to check, type variable resolution will be performed for this type
  @param ctx the resolution context to use to perform type variable resolution
  @return {@code true} if {@code type} is equivalent to {@code typeToMatch} after type resolution, otherwise {@code false}