private static @NotNull GenericSubstitutions collectTypeSubstitutions(@NotNull PyClassType classType, @NotNull TypeEvalContext context) {
    GenericSubstitutions result = new GenericSubstitutions();
    for (PyTypeProvider provider : PyTypeProvider.EP_NAME.getExtensionList()) {
      Map<PyType, PyType> substitutionsFromClassDefinition = provider.getGenericSubstitutions(classType.getPyClass(), context);
      for (Map.Entry<PyType, PyType> entry : substitutionsFromClassDefinition.entrySet()) {
        if (entry.getKey() instanceof PyTypeVarType typeVarType) {
          result.typeVars.put(typeVarType, entry.getValue());
        }
        else if (entry.getKey() instanceof PyTypeVarTupleType typeVarTuple) {
          assert entry.getValue() instanceof PyVariadicType;
          result.typeVarTuples.put(typeVarTuple, (PyVariadicType)entry.getValue());
        }
        // TODO Handle ParamSpecs here
      }
      if (!classType.isDefinition()) {
        PyCollectionType genericDefinitionType = as(provider.getGenericType(classType.getPyClass(), context), PyCollectionType.class);
        // TODO Re-use PyTypeParameterMapping, at the moment C[*Ts] <- C leads to *Ts being mapped to *tuple[], which breaks inference later on
        if (genericDefinitionType != null) {
          List<PyType> definitionTypeParameters = genericDefinitionType.getElementTypes();
          if (!(classType instanceof PyCollectionType genericType)) {
            for (PyType typeParameter : definitionTypeParameters) {
              if (typeParameter instanceof PyTypeVarTupleType typeVarTupleType) {
                result.typeVarTuples.put(typeVarTupleType, null);
              }
              else if (typeParameter instanceof PyParamSpecType paramSpecType) {
                result.paramSpecs.put(paramSpecType, null);
              }
              else if (typeParameter instanceof PyTypeVarType typeVarType) {
                result.typeVars.put(typeVarType, null);
              }
            }
          }
          else {
            PyTypeParameterMapping mapping = PyTypeParameterMapping.mapByShape(definitionTypeParameters, genericType.getElementTypes(),
                                                                               Option.MAP_UNMATCHED_EXPECTED_TYPES_TO_ANY);
            if (mapping != null) {
              for (Couple<PyType> pair : mapping.getMappedTypes()) {
                PyType typeParameter = pair.getFirst();
                PyType typeArgument = pair.getSecond();
                if (typeParameter instanceof PyTypeVarType typeVarType) {
                  result.typeVars.put(typeVarType, typeArgument);
                }
                else if (typeParameter instanceof PyTypeVarTupleType typeVarTuple) {
                  assert typeArgument instanceof PyVariadicType || typeArgument == null;
                  result.typeVarTuples.put(typeVarTuple, (PyVariadicType)typeArgument);
                }
                else if (typeParameter instanceof PyParamSpecType) {
                  result.paramSpecs.put((PyParamSpecType)typeParameter, as(typeArgument, PyParamSpecType.class));
                }
              }
            }
          }
        }
      }
      if (!result.typeVars.isEmpty() || !result.typeVarTuples.isEmpty() || !result.paramSpecs.isEmpty()) {
        return result;
      }
    }
    return result;
  }
private static @NotNull GenericSubstitutions collectTypeSubstitutions(@NotNull PyClassType classType, @NotNull TypeEvalContext context)