Skip to content

System.InvalidCastException in OperationExtensions.GetSymbol() on DATABASE:: and CODEUNIT:: expressions with a custom Code Analyzer #8240

@Arthurvdv

Description

@Arthurvdv

1. Describe the bug

The public OperationExtensions.GetSymbol() method in Microsoft.Dynamics.Nav.CodeAnalysis.dll throws an InvalidCastException when called on an IOperation that represents a DATABASE::<ObjectName>, CODEUNIT::<ObjectName>, or similar application object reference expression.

This occurs because two internal bound types (BoundApplicationObjectAccess and BoundObjectAccess) both set ExpressionKind => OperationKind.FieldAccess, but implement IApplicationObjectAccess and IObjectAccess respectively, not IFieldAccess. The GetSymbol() switch statement unconditionally casts FieldAccess operations to IFieldAccess:

// OperationExtensions.cs line 44-45 (decompiled from SDK 16.0.27.57058)
OperationKind.FieldAccess => ((IFieldAccess)operation).FieldSymbol,  // InvalidCastException here

This is the same class of issue as #7977 (GetSymbolInfo NullReferenceException), where internal SDK types produce unexpected exceptions when accessed through public API methods. As with #7977, this issue surfaces in custom code analyzers. We have not encountered it in any of the CodeCops shipped with Business Central (CodeCop, AppSourceCop, UICop, PerTenantExtensionCop).

2. To Reproduce

Minimal custom analyzer (C#)

using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

namespace MyCustomCodeAnalyzer;

[DiagnosticAnalyzer]
public class MyDiagnosticAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "TEST0001",
        title: "Test rule",
        messageFormat: "Found symbol: {0}",
        category: "Test",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
        = ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context) =>
        context.RegisterOperationAction(AnalyzeOperation, OperationKind.InvocationExpression);

    private static void AnalyzeOperation(OperationAnalysisContext ctx)
    {
        if (ctx.Operation is not IInvocationExpression invocation)
            return;

        foreach (var arg in invocation.Arguments)
        {
            // This line throws InvalidCastException when the argument
            // is a DATABASE::X or CODEUNIT::X expression
            var symbol = arg.Value.GetSymbol();
        }
    }
}

AL code that triggers the crash

codeunit 50000 "My Codeunit"
{
    procedure MyProcedure()
    var
        MyTable: Record "My Table";
    begin
        MyTable.MyMethod(DATABASE::"My Table");
    end;
}

table 50000 "My Table"
{
    fields
    {
        field(1; "Entry No."; Integer) { }
    }

    keys
    {
        key(PK; "Entry No.") { }
    }

    procedure MyMethod(SourceType: Integer)
    begin
    end;
}

Error message

Analyzer 'MyCustomCodeAnalyzer.MyDiagnosticAnalyzer' threw an exception of type 'System.InvalidCastException' with message 'System.InvalidCastException: Unable to cast object of type 'Microsoft.Dynamics.Nav.CodeAnalysis.BoundApplicationObjectAccess' to type 'Microsoft.Dynamics.Nav.CodeAnalysis.IFieldAccess'.
   at Microsoft.Dynamics.Nav.CodeAnalysis.OperationExtensions.GetSymbol(IOperation operation) in X:\source\Prod\Microsoft.Dynamics.Nav.CodeAnalysis\Compilation\OperationExtensions.cs:line 37'

The same crash occurs with CODEUNIT::, XMLPORT::, QUERY::, and REPORT:: expressions, as they all produce BoundApplicationObjectAccess operations internally.

A related but distinct type, BoundObjectAccess (implementing internal IObjectAccess), exhibits the same problem: it also sets ExpressionKind => OperationKind.FieldAccess without implementing IFieldAccess.

3. Expected behavior

GetSymbol() should handle BoundApplicationObjectAccess and BoundObjectAccess gracefully. For example:

  • Return the ApplicationObjectTypeSymbol for IApplicationObjectAccess operations (the meaningful symbol)
  • Return the ObjectTypeSymbol for IObjectAccess operations
  • Or at minimum, return null instead of throwing

A possible fix in OperationExtensions.GetSymbol():

public static ISymbol? GetSymbol(this IOperation operation)
{
    return operation.Kind switch
    {
        OperationKind.LocalReferenceExpression => ((ILocalReferenceExpression)operation).LocalVariable,
        OperationKind.GlobalReferenceExpression => ((IGlobalReferenceExpression)operation).GlobalVariable,
        OperationKind.ParameterReferenceExpression => ((IParameterReferenceExpression)operation).Parameter,
        OperationKind.FieldAccess when operation is IFieldAccess fieldAccess => fieldAccess.FieldSymbol,
        OperationKind.FieldAccess when operation is IApplicationObjectAccess appAccess => appAccess.ApplicationObjectTypeSymbol,
        OperationKind.ReturnValueReferenceExpression => ((IReturnValueReferenceExpression)operation).ReturnValue,
        OperationKind.ReportDataItemAccess => ((IReportDataItemAccess)operation).DataItemSymbol,
        _ => null,
    };
}

4. Actual behavior

GetSymbol() throws System.InvalidCastException because BoundApplicationObjectAccess reports OperationKind.FieldAccess but does not implement IFieldAccess.

5. Versions

  • AL Language: 16.0.27.57058 (Microsoft.Dynamics.Nav.CodeAnalysis.dll)
  • Visual Studio Code: 1.116.0 (system setup), Commit: 560a9dba96f961efea7b1612916f89e5d5d4d679
  • Business Central: not directly applicable (analyzer-side issue)
  • Operating System: Windows_NT x64 10.0.20348

Final Checklist

  • Search the issue repository to ensure you are reporting a new issue
  • Reproduce the issue after disabling all extensions except the AL Language extension
  • Simplify your code around the issue to better isolate the problem

Note

This issue was created with help from AI using GitHub Copilot. The analysis, reproduction code, and suggested fix were developed collaboratively.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions