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
{