private static string StringifyCore()

in src/Bicep.LangServer/Refactor/TypeStringifier.cs [97:266]


    private static string StringifyCore(TypeSymbol? type, TypeProperty? typeProperty, Strictness strictness, TypeSymbol[] visitedTypes, bool removeTopLevelNullability = false)
    {
        if (type == null)
        {
            return UnknownTypeName;
        }

        if (visitedTypes.Contains(type))
        {
            return RecursiveTypeName;
        }

        TypeSymbol[] previousVisitedTypes = visitedTypes;
        visitedTypes = [.. previousVisitedTypes, type];

        type = WidenType(type, strictness);

        // If from an object property that is implicitly allowed to be null (like for many resource properties)
        if (!removeTopLevelNullability && typeProperty?.Flags.HasFlag(TypePropertyFlags.AllowImplicitNull) == true)
        {
            // Won't recursive forever because now typeProperty = null
            // Note though that because this is by nature recursive with the same type, we must pass in previousVisitedTypes
            return StringifyCore(TypeHelper.MakeNullable(type), null, strictness, previousVisitedTypes);
        }

        // Show nullable types (always represented as a union type containing "null" as a member")
        //   as "type?" rather than "type|null"
        if (TypeHelper.TryRemoveNullability(type) is TypeSymbol nonNullableType)
        {
            if (removeTopLevelNullability)
            {
                return StringifyCore(nonNullableType, null, strictness, visitedTypes);
            }
            else
            {
                return Nullableify(nonNullableType, strictness, visitedTypes);
            }
        }

        switch (type)
        {
            // Literal types - keep as is if strict
            case StringLiteralType
               or IntegerLiteralType
               or BooleanLiteralType
               when strictness == Strictness.Strict:
                return type.Name;
            // ... otherwise widen to simple type
            case StringLiteralType:
                return LanguageConstants.String.Name;
            case IntegerLiteralType:
                return LanguageConstants.Int.Name;
            case BooleanLiteralType:
                return LanguageConstants.Bool.Name;

            // Tuple types
            case TupleType tupleType:
                if (strictness == Strictness.Loose)
                {
                    return LanguageConstants.Array.Name;
                }
                else if (strictness == Strictness.Medium)
                {
                    var widenedTypes = tupleType.Items.Select(t => WidenType(t.Type, strictness)).ToArray();
                    var firstItemType = widenedTypes.FirstOrDefault()?.Type;
                    if (firstItemType == null)
                    {
                        // Empty tuple - use "array" to allow items
                        return LanguageConstants.Array.Name;
                    }
                    else if (widenedTypes.All(t => t.Type.Name == firstItemType.Name))
                    {
                        // Bicep infers a tuple type from literals such as "[1, 2]", turn these
                        // into the more likely intended int[] if all the members are of the same type
                        return Arrayify(widenedTypes[0], strictness, visitedTypes);
                    }
                }

                return $"[{string.Join(", ", tupleType.Items.Select(tt => StringifyCore(tt.Type, null, strictness, visitedTypes)))}]";

            // Typed arrays (e.g. int[])
            case TypedArrayType when strictness == Strictness.Loose:
                return LanguageConstants.Array.Name;
            case TypedArrayType typedArrayType:
                return Arrayify(typedArrayType.Item.Type, strictness, visitedTypes);

            // Plain old "array"
            case ArrayType:
                return LanguageConstants.Array.Name;

            // Nullable types are union types with one of the members being the null type
            case UnionType unionType:
                if (unionType.Members.Any(m => !IsLiteralType(m.Type)))
                {
                    // This handles the "open enum" type scenario (e.g. "type t = 'abc' | 'def' | string"), 
                    //   which is supported by swagger but not by Bicep syntax.
                    // In this case, we will generate the widened type with a comment indicating the exact actual type,
                    //   e.g. "string /* 'abc' | 'def' | string */"
                    var widenedUnionType = WidenType(unionType, Strictness.Loose);
                    string widenedTypeString;
                    if (widenedUnionType != unionType)
                    {
                        if (TypeHelper.IsNullable(unionType))
                        {
                            widenedUnionType = TypeHelper.MakeNullable(widenedUnionType);
                        }
                        widenedTypeString = StringifyCore(widenedUnionType, null, Strictness.Loose, visitedTypes);
                    }
                    else
                    {
                        // Invalid or unsupported (CONSIDER: handle tagged union types)
                        widenedTypeString = TypeHelper.IsNullable(unionType) ? "object?" : "object";
                    }
                    return $"{widenedTypeString} /* {type.Name} */";
                }
                return type.Name;

            case BooleanType:
                return LanguageConstants.Bool.Name;
            case IntegerType:
                return LanguageConstants.Int.Name;
            case StringType:
                return LanguageConstants.String.Name;
            case NullType:
                return NullTypeName;

            case ObjectType objectType:
                if (strictness == Strictness.Loose)
                {
                    return LanguageConstants.Object.Name;
                }

                var writeableProperties = GetWriteableProperties(objectType);

                // strict: {} with additional properties allowed should be "object" not "{}"
                // medium: Bicep infers {} with no allowable members from the literal "{}", the user more likely wants to allow members
                if (writeableProperties.Length == 0 &&
                    (strictness == Strictness.Medium || !IsEmptyObjectLiteral(objectType)))
                {
                    return "object";
                }

                return $"{{ {string.Join(", ", writeableProperties
                        .Select(p => GetFormattedTypeProperty(p, strictness, visitedTypes)))} }}";

            case AnyType:
                return AnyTypeName;
            case ErrorType:
                return ErrorTypeName;

            // Anything else we don't know about (e.g. a type from a resource's swagger)
            default:
                return $"object? /* {type.Name} */";
        }
    }

    private static TypeSymbol WidenType(TypeSymbol type, Strictness strictness)
    {
        if (strictness == Strictness.Strict)
        {
            return type;
        }

        if (type is UnionType unionType && strictness == Strictness.Loose)
        {
            // Widen non-null members to a single type if they're all literal types of the same type, or null.
            // (If they're not all of the same literal type, either it's invalid or it's a tagged union.)
            var nonNullMembers = NonNullUnionMembers(unionType).ToArray();
            if (nonNullMembers.Select(m => WidenLiteralType(m).Name).Distinct().Count() == 1) // using "Name" comparison because we don't care about ValidationFlags
            {