diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 4bd0ba429fa..867f4b6965f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -35,7 +35,6 @@ public abstract class UtilityAnalyzerBase : SonarDiagnosticAnalyzer { protected static readonly ISet FileExtensionWhitelist = new HashSet { ".cs", ".csx", ".vb" }; private readonly DiagnosticDescriptor rule; - protected override bool EnableConcurrentExecution => false; public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(rule); @@ -98,26 +97,47 @@ protected sealed override void Initialize(SonarAnalysisContext context) => { return; } - var treeMessages = new ConcurrentStack(); + var cancel = startContext.Cancel; + var outPath = parameters.OutPath; + var treeMessages = new BlockingCollection(); + var consumerTask = Task.Factory.StartNew(() => + { + // Consume all messages as they arrive during the compilation and write them to disk. + // The Task starts on CompilationStart and in CompilationEnd we block until it is finished via CompleteAdding(). + // Note: CompilationEndAction is not guaranteed to be called for each CompilationStart. + // Therefore it is important to properly handle cancelation here. + // LongRunning: We probably run on a dedicated thread outside of the thread pool + // If any of the IO operations throw, CompilationEnd takes care of the clean up. + Directory.CreateDirectory(outPath); + using var stream = File.Create(Path.Combine(outPath, FileName)); + foreach (var message in treeMessages.GetConsumingEnumerable(cancel).WhereNotNull()) + { + message.WriteDelimitedTo(stream); + } + }, cancel, TaskCreationOptions.LongRunning, TaskScheduler.Default); startContext.RegisterSemanticModelAction(modelContext => { - if (ShouldGenerateMetrics(parameters, modelContext)) + if (ShouldGenerateMetrics(parameters, modelContext) && !cancel.IsCancellationRequested) { var message = CreateMessage(parameters, modelContext.Tree, modelContext.SemanticModel); - treeMessages.Push(message); + treeMessages.Add(message); } }); startContext.RegisterCompilationEndAction(endContext => { - var allMessages = CreateAnalysisMessages(endContext) - .Concat(treeMessages) - .WhereNotNull() - .ToArray(); - Directory.CreateDirectory(parameters.OutPath); - using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); - foreach (var message in allMessages) + var analysisMessages = CreateAnalysisMessages(endContext); + foreach (var message in analysisMessages) { - message.WriteDelimitedTo(stream); + treeMessages.Add(message); + } + treeMessages.CompleteAdding(); + try + { + consumerTask.Wait(cancel); // Wait until all messages are written to disk. Throws, if the task failed. + } + finally + { + treeMessages.Dispose(); } }); });