in src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs [1656:1837]
public override void VisitArrayAccessSyntax(ArrayAccessSyntax syntax)
=> AssignTypeWithDiagnostics(syntax, diagnostics => GetAccessedType(syntax, diagnostics));
private static TypeSymbol GetArrayItemType(ArrayAccessSyntax syntax, IDiagnosticWriter diagnostics, TypeSymbol baseType, TypeSymbol indexType)
{
var errors = new List<IDiagnostic>();
CollectErrors(errors, baseType);
CollectErrors(errors, indexType);
if (PropagateErrorType(errors, baseType, indexType))
{
return ErrorType.Create(errors);
}
baseType = UnwrapType(baseType);
// if the index type is nullable but otherwise valid, emit a fixable warning
if (TypeHelper.TryRemoveNullability(indexType) is { } nonNullableIndex)
{
var withNonNullableIndex = GetArrayItemType(syntax, diagnostics, baseType, nonNullableIndex);
if (withNonNullableIndex is not ErrorType)
{
diagnostics.Write(DiagnosticBuilder.ForPosition(syntax.IndexExpression).PossibleNullReferenceAssignment(nonNullableIndex, indexType, syntax.IndexExpression));
}
return withNonNullableIndex;
}
static TypeSymbol GetTypeAtIndex(TupleType baseType, IntegerLiteralType indexType, SyntaxBase indexSyntax, bool fromEnd) => indexType.Value switch
{
long value when value < 0 ||
(value == 0 && fromEnd) ||
value > baseType.Items.Length ||
(value == baseType.Items.Length && !fromEnd) ||
// unlikely to hit this given that we've established that the tuple has a item at the given position
value > int.MaxValue => ErrorType.Create(DiagnosticBuilder.ForPosition(indexSyntax)
.IndexOutOfBounds(baseType.Name, baseType.Items.Length, value)),
long otherwise when fromEnd => baseType.Items[^(int)otherwise].Type,
long otherwise => baseType.Items[(int)otherwise].Type,
};
switch (baseType)
{
case TypeSymbol when TypeHelper.TryRemoveNullability(baseType) is TypeSymbol nonNullableBaseType:
diagnostics.Write(DiagnosticBuilder.ForPosition(TextSpan.Between(syntax.OpenSquare, syntax.CloseSquare)).DereferenceOfPossiblyNullReference(baseType.Name, syntax));
return GetArrayItemType(syntax, diagnostics, nonNullableBaseType, indexType);
case AnyType:
// base expression is of type any
if (indexType.TypeKind == TypeKind.Any)
{
// index is also of type any
return LanguageConstants.Any;
}
if (TypeValidator.AreTypesAssignable(indexType, LanguageConstants.String) &&
syntax.FromEndMarker is not null)
{
return InvalidAccessExpression(
DiagnosticBuilder.ForPosition(syntax.FromEndMarker)
.FromEndArrayAccessNotSupportedWithIndexType(indexType),
diagnostics,
syntax.IsSafeAccess);
}
if (TypeValidator.AreTypesAssignable(indexType, LanguageConstants.Int) ||
TypeValidator.AreTypesAssignable(indexType, LanguageConstants.String))
{
// index expression is string | int but base is any
return LanguageConstants.Any;
}
// index was of the wrong type
return InvalidAccessExpression(DiagnosticBuilder.ForPosition(syntax.IndexExpression).StringOrIntegerIndexerRequired(indexType), diagnostics, syntax.IsSafeAccess);
case TupleType baseTuple when indexType is IntegerLiteralType integerLiteralIndex:
return GetTypeAtIndex(baseTuple, integerLiteralIndex, syntax.IndexExpression, syntax.FromEndMarker is not null);
case TupleType baseTuple when indexType is UnionType indexUnion && indexUnion.Members.All(t => t.Type is IntegerLiteralType):
var possibilities = indexUnion.Members.Select(t => t.Type)
.OfType<IntegerLiteralType>()
.Select(index => GetTypeAtIndex(baseTuple, index, syntax.IndexExpression, syntax.FromEndMarker is not null))
.ToImmutableArray();
if (possibilities.OfType<ErrorType>().Any())
{
return ErrorType.Create(possibilities.SelectMany(p => p.GetDiagnostics()));
}
return TypeHelper.CreateTypeUnion(possibilities);
case ArrayType baseArray:
if (indexType is IntegerLiteralType integerLiteralArrayIndex && integerLiteralArrayIndex.Value < 0)
{
return ErrorType.Create(
DiagnosticBuilder
.ForPosition(syntax.IndexExpression)
.ArrayIndexOutOfBounds(integerLiteralArrayIndex.Value));
}
// we are indexing over an array
if (TypeValidator.AreTypesAssignable(indexType, LanguageConstants.Int))
{
// the index is of "any" type or integer type
// return the item type
return baseArray.Item.Type;
}
return InvalidAccessExpression(DiagnosticBuilder.ForPosition(syntax.IndexExpression).ArraysRequireIntegerIndex(indexType), diagnostics, syntax.IsSafeAccess);
case ObjectType or DiscriminatedObjectType when syntax.FromEndMarker is not null:
return InvalidAccessExpression(
DiagnosticBuilder.ForPosition(syntax.FromEndMarker)
.FromEndArrayAccessNotSupportedOnBaseType(baseType),
diagnostics,
syntax.IsSafeAccess);
case ObjectType baseObject:
{
// we are indexing over an object
if (indexType.TypeKind == TypeKind.Any)
{
// index is of type "any"
return GetExpressionedPropertyType(baseObject, syntax.IndexExpression);
}
if (TypeValidator.AreTypesAssignable(indexType, LanguageConstants.String))
{
if (indexType is StringLiteralType literalIndex)
{
// indexing using a string literal so we know the name of the property
return TypeHelper.GetNamedPropertyType(baseObject,
syntax.IndexExpression,
literalIndex.RawStringValue,
syntax.IsSafeAccess,
shouldWarn: syntax.IsSafeAccess || TypeValidator.ShouldWarnForPropertyMismatch(baseObject),
diagnostics);
}
// the property name is itself an expression
return GetExpressionedPropertyType(baseObject, syntax.IndexExpression);
}
return InvalidAccessExpression(DiagnosticBuilder.ForPosition(syntax.IndexExpression).ObjectsRequireStringIndex(indexType), diagnostics, syntax.IsSafeAccess);
}
case DiscriminatedObjectType:
if (TypeValidator.AreTypesAssignable(indexType, LanguageConstants.String))
{
// index is assignable to string
// since we're not resolving the discriminator currently, we can just return the "any" type
// TODO: resolve the discriminator
return LanguageConstants.Any;
}
return InvalidAccessExpression(DiagnosticBuilder.ForPosition(syntax.IndexExpression).ObjectsRequireStringIndex(indexType), diagnostics, syntax.IsSafeAccess);
case UnionType unionType:
{
// ensure we enumerate only once since some paths include a side effect that writes a diagnostic
var arrayItemTypes = unionType.Members
.Select(baseMemberType => GetArrayItemType(syntax, diagnostics, baseMemberType.Type, indexType))
.ToList();
if (arrayItemTypes.OfType<ErrorType>().Any())
{
// some of the union members are not assignable
// base expression was of the wrong type
return ErrorType.Create(DiagnosticBuilder.ForPosition(syntax.BaseExpression).IndexerRequiresObjectOrArray(baseType));
}
// all of the union members are assignable - create the resulting item type
return TypeHelper.CreateTypeUnion(arrayItemTypes);
}
default:
// base expression was of the wrong type
return InvalidAccessExpression(DiagnosticBuilder.ForPosition(syntax.BaseExpression).IndexerRequiresObjectOrArray(baseType), diagnostics, syntax.IsSafeAccess);
}
}