public static @Nullable String getTypeParamsText(boolean presentable, PsiTypeParameterListOwner owner, PsiSubstitutor substitutor) {
    PsiTypeParameter[] parameters = owner.getTypeParameters();
    if (parameters.length == 0) return null;

    List<PsiType> substituted = ContainerUtil.map(parameters, parameter -> {
      PsiType type = substitutor.substitute(parameter);
      if (type instanceof PsiWildcardType) type = ((PsiWildcardType)type).getExtendsBound();
      return PsiUtil.resolveClassInClassTypeOnly(type) == parameter ? null : type;
    });
    if (ContainerUtil.exists(substituted, t -> t == null || t instanceof PsiCapturedWildcardType)) return null;
    if (substituted.equals(ContainerUtil.map(parameters, TypeConversionUtil::typeParameterErasure))) return null;

    String result = "<" + StringUtil.join(substituted, presentable ? PsiType::getPresentableText : PsiType::getCanonicalText, ", ") + ">";
    return result.contains("?") ? null : result;
  }
public static @Nullable String getTypeParamsText(boolean presentable, PsiTypeParameterListOwner owner, PsiSubstitutor substitutor)