public override void VisitArrayAccessSyntax()

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);
            }
        }