Having been in development for a long time I've come across a lot of headscratching odd behaviour in code. Those issus have lead to a fair few conversations where people have looked at the odd behaviour and asked "is it a compiler/runtime bug?". And in all these years it never has been that sort of bug. But for the first time I have recently found a situation where some odd behaviour is C#'s fault...
So here's some info about the issue I saw this time, and what Microsoft are fixing as a result:
I was hacking about with some code that for a personal project recently. It was a .Net 8 console application, which loaded some config and did some processing. In my first pass through the code I'd made a config POCO which exposed settings as properties, like:
public class ToolConfig { // logic to load config from disk public string DataSourceFile { get; set } }
But as I was working I realised this made the logic of the tool too closely coupled to the config. Adding new logic meant changing the config class to expose new settings. So I refactored this towards a more generic
Dictionary
based solution where this class doesn't need to know what config it stores:
public class ToolConfig : Dictionary<string,string> { // logic to load config from disk }
Nothing too dramatic...
But in the tool's code, I had originally written a constructor something like:
public class SourceDataModel { private string _SourceFile; public SourceDataModel(ToolConfig cfg) { _SourceFile = cfg.DataSourceFile ?? throw new InvalidDataException($"No value supplied for {nameof(cfg.DataSourceFile)}"); } }
So while I was refactoring, I changed this over to the new config approach and unthinkingly wrote something like:
public class SourceDataModel { public constr string SourceFileConfigKey = "DataSourceFile"; private string _SourceFile; public SourceDataModel(ToolConfig cfg) { _SourceFile = cfg[SourceFileConfigKey] ?? throw new InvalidDataException($"No value supplied for {nameof(cfg[SourceFileConfigKey])}"); } }
And stuff started going wrong...
The initial problem was that as I typed that change I noticed this appear:
For Google's benefit, the message there is "Feature 'Diagnostic analyzer runner' is currently unavailable due to an internal error."
I shrugged that off when I saw it, as I've seen issues like that happen on occasion before. An add-in crashes, or gets slow and Visual Studio complains. I dismissed the warning bar and carried on writing the refactoring.
But the next time I hit compile it seemed to take a surprisingly long time (for a fairly small project) and I got this in the output window:
Build started at 10:53... 1>------ Build started: Project: BugTest, Configuration: Debug Any CPU ------ 1>C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\Roslyn\Microsoft.CSharp.Core.targets(84,5): error MSB6006: "csc.exe" exited with code -2146232797. 1>Done building project "BugTest.csproj" -- FAILED. ========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ========== ========== Build completed at 10:54 and took 16.694 seconds ==========
That's not a syntax error - that's the C# compiler crashing. Bad times...
My first thought here was that something got corrupted and I needed to restart. So I closed Visual Studio, and reopened my solution. Same issue. So I rebooted my computer and reloaded again. But sill same issue. The yellow warning bar appeared after reloading, and I still could not compile.
Clearly something more significant was up.
My next thought was that I'd had a VS update applied recently, so perhaps that was dodgy. So I tried a restore on my VS install. But other than taking an annoyingly long time, that didn't affect the issue at all.
So I did what I should probably have done initially: I clicked the "Show Stack Trace" link in the yellow warning banner. And that gave me a big old pile of crash:
StreamJsonRpc.RemoteInvocationException: Unexpected value 'Unknown' of type 'Microsoft.CodeAnalysis.CSharp.AccessorKind' at StreamJsonRpc.JsonRpc.<InvokeCoreAsync>d__156`1.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.CodeAnalysis.Remote.BrokeredServiceConnection`1.<TryInvokeAsync>d__18`1.MoveNext() RPC server exception: System.InvalidOperationException: Unexpected value 'Unknown' of type 'Microsoft.CodeAnalysis.CSharp.AccessorKind' at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.MethodInfo.Create(PropertySymbol property, AccessorKind accessorKind) at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.VisitIndexerAccess(BoundIndexerAccess node) at Microsoft.CodeAnalysis.CSharp.BoundTreeVisitor.VisitExpressionOrPatternWithStackGuard(Int32& recursionDepth, BoundNode node) at Microsoft.CodeAnalysis.CSharp.BoundTreeWalker.VisitNameOfOperator(BoundNameOfOperator node) at Microsoft.CodeAnalysis.CSharp.BoundTreeVisitor.VisitExpressionOrPatternWithStackGuard(BoundNode node) at Microsoft.CodeAnalysis.CSharp.BoundTreeVisitor.VisitExpressionOrPatternWithStackGuard(Int32& recursionDepth, BoundNode node) at Microsoft.CodeAnalysis.CSharp.BoundTreeWalker.VisitLocalDeclaration(BoundLocalDeclaration node) at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.VisitLocalDeclaration(BoundLocalDeclaration node) at Microsoft.CodeAnalysis.CSharp.BoundTreeWalker.VisitList[T](ImmutableArray`1 list) at Microsoft.CodeAnalysis.CSharp.BoundTreeWalker.VisitBlock(BoundBlock node) at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.VisitBlock(BoundBlock node) at Microsoft.CodeAnalysis.CSharp.BoundTreeWalker.VisitNonConstructorMethodBody(BoundNonConstructorMethodBody node) at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.Analyze(CSharpCompilation compilation, MethodSymbol symbol, BoundNode node, BindingDiagnosticBag diagnostics) at Microsoft.CodeAnalysis.CSharp.MethodCompiler.BindMethodBody(MethodSymbol method, TypeCompilationState compilationState, BindingDiagnosticBag diagnostics, Boolean includeInitializersInBody, BoundNode initializersBody, Boolean reportNullableDiagnostics, ImportChain& importChain, Boolean& originalBodyNested, Boolean& prependedDefaultValueTypeConstructorInitializer, InitialState& forSemanticModel) at Microsoft.CodeAnalysis.CSharp.MethodCompiler.CompileMethod(MethodSymbol methodSymbol, Int32 methodOrdinal, ProcessedFieldInitializers& processedInitializers, SynthesizedSubmissionFields previousSubmissionFields, TypeCompilationState compilationState) at Microsoft.CodeAnalysis.CSharp.MethodCompiler.CompileNamedType(NamedTypeSymbol containingType) at Microsoft.CodeAnalysis.CSharp.MethodCompiler.<>c__DisplayClass25_0.<CompileNamedTypeAsync>b__0() at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) --- End of stack trace from previous location --- at Microsoft.CodeAnalysis.CSharp.MethodCompiler.WaitForWorkers() at Microsoft.CodeAnalysis.CSharp.MethodCompiler.CompileMethodBodies(CSharpCompilation compilation, PEModuleBuilder moduleBeingBuiltOpt, Boolean emittingPdb, Boolean hasDeclarationErrors, Boolean emitMethodBodies, BindingDiagnosticBag diagnostics, Predicate`1 filterOpt, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.CSharp.CSharpCompilation.<GetDiagnosticsForMethodBodiesInTree>g__compileMethodBodiesAndDocComments|238_0(SyntaxTree filterTree, Nullable`1 filterSpan, BindingDiagnosticBag bindingDiagnostics, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.CSharp.CSharpCompilation.GetDiagnosticsForMethodBodiesInTree(SyntaxTree tree, Nullable`1 span, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.CSharp.CSharpCompilation.GetDiagnosticsForSyntaxTree(CompilationStage stage, SyntaxTree syntaxTree, Nullable`1 filterSpanWithinTree, Boolean includeEarlierStages, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.CSharp.SyntaxTreeSemanticModel.GetDiagnostics(Nullable`1 span, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzers.<GetCompilationEventsForSingleFileAnalysis>g__generateCompilationEvents|64_0(Compilation compilation, AnalysisScope analysisScope, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzers.GetCompilationEventsForSingleFileAnalysis(Compilation compilation, AnalysisScope analysisScope, ImmutableArray`1 additionalFiles, Boolean hasAnyActionsRequiringCompilationEvents, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzers.ComputeAnalyzerDiagnosticsAsync(AnalysisScope analysisScope, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzers.GetAnalysisResultCoreAsync(SemanticModel model, Nullable`1 filterSpan, ImmutableArray`1 analyzers, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersPair.GetAnalysisResultAsync(SemanticModel model, Nullable`1 filterSpan, ImmutableArray`1 projectAnalyzers, ImmutableArray`1 hostAnalyzers, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.Extensions.GetAnalysisResultAsync(CompilationWithAnalyzersPair compilationWithAnalyzers, DocumentAnalysisScope documentAnalysisScope, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Diagnostics.Extensions.GetAnalysisResultAsync(CompilationWithAnalyzersPair compilationWithAnalyzers, DocumentAnalysisScope documentAnalysisScope, Project project, DiagnosticAnalyzerInfoCache analyzerInfoCache, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.Diagnostics.DiagnosticComputer.AnalyzeAsync(CompilationWithAnalyzersPair compilationWithAnalyzers, BidirectionalMap`2 analyzerToIdMap, ImmutableArray`1 projectAnalyzers, ImmutableArray`1 hostAnalyzers, SkippedHostAnalyzersInfo skippedAnalyzersInfo, Boolean reportSuppressedDiagnostics, Boolean logPerformanceInfo, Boolean getTelemetryInfo, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.Diagnostics.DiagnosticComputer.GetDiagnosticsAsync(ImmutableArray`1 projectAnalyzerIds, ImmutableArray`1 hostAnalyzerIds, Boolean reportSuppressedDiagnostics, Boolean logPerformanceInfo, Boolean getTelemetryInfo, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.Diagnostics.DiagnosticComputer.GetNormalPriorityDiagnosticsAsync(ImmutableArray`1 projectAnalyzerIds, ImmutableArray`1 hostAnalyzerIds, Boolean reportSuppressedDiagnostics, Boolean logPerformanceInfo, Boolean getTelemetryInfo, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.RemoteDiagnosticAnalyzerService.<>c__DisplayClass3_0.<<CalculateDiagnosticsAsync>b__0>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.CodeAnalysis.Remote.RemoteWorkspace.<>c__DisplayClass7_0`1.<<RunWithSolutionAsync>g__ProcessSolutionAsync|1>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.CodeAnalysis.Remote.RemoteWorkspace.RunWithSolutionAsync[T](AssetProvider assetProvider, Checksum solutionChecksum, Boolean updatePrimaryBranch, Func`2 implementation, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.RemoteWorkspace.RunWithSolutionAsync[T](AssetProvider assetProvider, Checksum solutionChecksum, Boolean updatePrimaryBranch, Func`2 implementation, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.BrokeredServiceBase.RunWithSolutionAsync[T](Checksum solutionChecksum, Func`2 implementation, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Remote.RemoteDiagnosticAnalyzerService.CalculateDiagnosticsAsync(Checksum solutionChecksum, DiagnosticArguments arguments, CancellationToken cancellationToken)
At this point I also noted that trying to run
dotnet build
from the command line gave a similar stack trace, and also had
csc.exe
crashing out with a failure.
So what does that stack trace tell us?
Well it looks like this is a failure in some code using the Roslyn parser framework to analyse the code - and most of this is from a
CodeAnalysis
namespace. So it seems like the problem is with how the compiler framework is trying to process the source code in question. And this bit gives an important clue about where the issue is:
at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.MethodInfo.Create(PropertySymbol property, AccessorKind accessorKind) at Microsoft.CodeAnalysis.CSharp.RefSafetyAnalysis.VisitIndexerAccess(BoundIndexerAccess node) at Microsoft.CodeAnalysis.CSharp.BoundTreeVisitor.VisitExpressionOrPatternWithStackGuard(Int32& recursionDepth, BoundNode node) at Microsoft.CodeAnalysis.CSharp.BoundTreeWalker.VisitNameOfOperator(BoundNameOfOperator node)
(I think the point it crashes is
here in the source?) And the trace is saying that the
NameOfOperator
is involved - it's trying to parse an
ExpressionOrPattern
with an
Indexer
while running the
RefSafetyAnalysis
analyser. So that points back to this bit of the source code:
_SourceFile = cfg[SourceFileConfigKey] ?? throw new InvalidDataException($"No value supplied for {nameof(cfg[SourceFileConfigKey])}");
The interpolated string passed to the exception constructor is calling
nameof()
on an expression with an indexer...
And when I changed
nameof(cfg[SourceFileConfigKey])
to
nameof(cfg)
all the errors went away and I could compile my code again. So it's the indexer here which is triggering the problem.
To my mind this is an error in how the analyser in question handles
nameof()
being invoked on an indexer. The parsing of an expression like
cfg["test"]
should either succeed or return a compiler error. It should never crash.
However I'm also pretty sure this isn't valid syntax. What is the "name" of an indexer operation on a dictionary? That's not a thing you can give a name to - it's an expression which can be evaluated to a value. And while I'm not an expert on reading the C# language spec, I think this backs up that assessment.
It's interesting to note that it is specifically object indexers that seem to cause the issue here. Accessing an array like this:
var u = new int[1]; var name = nameof(u[1]);
gives the compiler error CS8081 - "Expression does not have a name". It does not crash. And it seems likely that this is what the correct behaviour should be for my
Dictionary
example too?
So I've submitted this as a bug to Microsoft, and they say a fix has been built and is scheduled for release at some point.
So once they ship that it should turn my bad code into the syntax error it deserves to be...
↑ Back to top