From 66fac76e792abdcea8ffcb45436dcda85db263d9 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 21 Aug 2025 23:58:04 -0400 Subject: [PATCH 01/10] Inline method --- .../Service/DiagnosticAnalyzerService_CoreAnalyze.cs | 8 -------- .../Diagnostics/Service/DocumentAnalysisExecutor.cs | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs index 66ab61681705f..5043442fac4c0 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs @@ -19,14 +19,6 @@ namespace Microsoft.CodeAnalysis.Diagnostics; internal sealed partial class DiagnosticAnalyzerService { - private Task> AnalyzeDocumentInProcessAsync( - DocumentAnalysisScope documentAnalysisScope, - CompilationWithAnalyzersPair compilationWithAnalyzers, - bool logPerformanceInfo, - bool getTelemetryInfo, - CancellationToken cancellationToken) - => AnalyzeInProcessAsync(documentAnalysisScope, documentAnalysisScope.TextDocument.Project, compilationWithAnalyzers, logPerformanceInfo, getTelemetryInfo, cancellationToken); - private Task> AnalyzeProjectInProcessAsync( Project project, CompilationWithAnalyzersPair compilationWithAnalyzers, diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index d0f98af89f067..ec392ee1b8837 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -157,8 +157,8 @@ private async Task Date: Fri, 22 Aug 2025 00:02:40 -0400 Subject: [PATCH 02/10] Inline method --- .../Diagnostics/Service/DiagnosticAnalyzerService.cs | 2 +- ...ticAnalyzerService_ComputeDiagnosticAnalysisResults.cs | 4 ++-- .../Service/DiagnosticAnalyzerService_CoreAnalyze.cs | 8 -------- .../Diagnostics/Service/DocumentAnalysisExecutor.cs | 6 ++++-- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs index ac1daba1c0654..f6bb1d2680205 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs @@ -187,6 +187,6 @@ public ImmutableArray GetAnalyzers(Project project) public Task> AnalyzeProjectInProcessAsync( Project project, CompilationWithAnalyzersPair compilationWithAnalyzers, bool logPerformanceInfo, bool getTelemetryInfo, CancellationToken cancellationToken) - => service.AnalyzeProjectInProcessAsync(project, compilationWithAnalyzers, logPerformanceInfo, getTelemetryInfo, cancellationToken); + => service.AnalyzeInProcessAsync(documentAnalysisScope: null, project, compilationWithAnalyzers, logPerformanceInfo, getTelemetryInfo, cancellationToken); } } diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_ComputeDiagnosticAnalysisResults.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_ComputeDiagnosticAnalysisResults.cs index df05a313af2ba..a8a8256a4a419 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_ComputeDiagnosticAnalysisResults.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_ComputeDiagnosticAnalysisResults.cs @@ -97,8 +97,8 @@ async Task> Co || compilationWithAnalyzers?.HostAnalyzers.Length > 0) { // calculate regular diagnostic analyzers diagnostics - var resultMap = await this.AnalyzeProjectInProcessAsync( - project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: true, cancellationToken).ConfigureAwait(false); + var resultMap = await this.AnalyzeInProcessAsync( + documentAnalysisScope: null, project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: true, cancellationToken).ConfigureAwait(false); result = resultMap.AnalysisResult; diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs index 5043442fac4c0..6fc1cbaf7055c 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CoreAnalyze.cs @@ -19,14 +19,6 @@ namespace Microsoft.CodeAnalysis.Diagnostics; internal sealed partial class DiagnosticAnalyzerService { - private Task> AnalyzeProjectInProcessAsync( - Project project, - CompilationWithAnalyzersPair compilationWithAnalyzers, - bool logPerformanceInfo, - bool getTelemetryInfo, - CancellationToken cancellationToken) - => AnalyzeInProcessAsync(documentAnalysisScope: null, project, compilationWithAnalyzers, logPerformanceInfo, getTelemetryInfo, cancellationToken); - private async Task> AnalyzeInProcessAsync( DocumentAnalysisScope? documentAnalysisScope, Project project, diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index ec392ee1b8837..e228ca85843bc 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -70,7 +70,8 @@ public DocumentAnalysisExecutor With(DocumentAnalysisScope analysisScope) /// /// Return all local diagnostics (syntax, semantic) that belong to given document for the given analyzer by calculating them. /// - public async Task> ComputeDiagnosticsInProcessAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + public async Task> ComputeDiagnosticsInProcessAsync( + DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) { Contract.ThrowIfFalse(AnalysisScope.ProjectAnalyzers.Contains(analyzer) || AnalysisScope.HostAnalyzers.Contains(analyzer)); @@ -151,7 +152,8 @@ public async Task> ComputeDiagnosticsInProcessAsy return diagnostics; } - private async Task> GetAnalysisResultInProcessAsync(DocumentAnalysisScope analysisScope, CancellationToken cancellationToken) + private async Task> GetAnalysisResultInProcessAsync( + DocumentAnalysisScope analysisScope, CancellationToken cancellationToken) { RoslynDebug.Assert(_compilationWithAnalyzers != null); From 9336d6a2438faa2b01218941c3cc462a1d21fd35 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:06:00 -0400 Subject: [PATCH 03/10] Inline methods --- .../Service/DocumentAnalysisExecutor.cs | 310 +++++++++--------- 1 file changed, 154 insertions(+), 156 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index e228ca85843bc..29a440ee50886 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -123,8 +123,8 @@ public async Task> ComputeDiagnosticsInProcessAsy var diagnostics = kind switch { - AnalysisKind.Syntax => await GetSyntaxDiagnosticsInProcessAsync(analyzer, isCompilerAnalyzer, cancellationToken).ConfigureAwait(false), - AnalysisKind.Semantic => await GetSemanticDiagnosticsInProcessAsync(analyzer, isCompilerAnalyzer, cancellationToken).ConfigureAwait(false), + AnalysisKind.Syntax => await GetSyntaxDiagnosticsInProcessAsync().ConfigureAwait(false), + AnalysisKind.Semantic => await GetSemanticDiagnosticsInProcessAsync().ConfigureAwait(false), _ => throw ExceptionUtilities.UnexpectedValue(kind), }; @@ -150,202 +150,200 @@ public async Task> ComputeDiagnosticsInProcessAsy #endif return diagnostics; - } - - private async Task> GetAnalysisResultInProcessAsync( - DocumentAnalysisScope analysisScope, CancellationToken cancellationToken) - { - RoslynDebug.Assert(_compilationWithAnalyzers != null); - try - { - var resultAndTelemetry = await _diagnosticAnalyzerService.AnalyzeInProcessAsync( - analysisScope, analysisScope.TextDocument.Project, _compilationWithAnalyzers, _logPerformanceInfo, getTelemetryInfo: false, cancellationToken).ConfigureAwait(false); - return resultAndTelemetry.AnalysisResult; - } - catch when (_onAnalysisException != null) + async Task> GetSyntaxDiagnosticsInProcessAsync() { - _onAnalysisException.Invoke(); - throw; - } - } + // PERF: + // 1. Compute diagnostics for all analyzers with a single invocation into CompilationWithAnalyzers. + // This is critical for better analyzer execution performance. + // 2. Ensure that the compiler analyzer is treated specially and does not block on diagnostic computation + // for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing. - private async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(DiagnosticAnalyzer analyzer, TextSpan? span, CancellationToken cancellationToken) - { - RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); - RoslynDebug.Assert(_compilationWithAnalyzers != null); - RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); - RoslynDebug.Assert(AnalysisScope.TextDocument is Document); + RoslynDebug.Assert(_compilationWithAnalyzers != null); + RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); - var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) - ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) - : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); - var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); - if (!analysisResult.TryGetValue(analyzer, out var result)) - { - return []; - } - - return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); - } - - private async Task> GetSyntaxDiagnosticsInProcessAsync(DiagnosticAnalyzer analyzer, bool isCompilerAnalyzer, CancellationToken cancellationToken) - { - // PERF: - // 1. Compute diagnostics for all analyzers with a single invocation into CompilationWithAnalyzers. - // This is critical for better analyzer execution performance. - // 2. Ensure that the compiler analyzer is treated specially and does not block on diagnostic computation - // for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing. + if (isCompilerAnalyzer) + { + if (AnalysisScope.TextDocument is not Document) + return []; - RoslynDebug.Assert(_compilationWithAnalyzers != null); - RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); + return await GetCompilerAnalyzerDiagnosticsInProcessAsync(analyzer, AnalysisScope.Span, cancellationToken).ConfigureAwait(false); + } - if (isCompilerAnalyzer) - { - if (AnalysisScope.TextDocument is not Document) + if (_lazySyntaxDiagnostics == null) { - return []; + using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSyntaxDiagnosticsInProcessAsync)}.{nameof(GetAnalysisResultInProcessAsync)}"); + + var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); + var syntaxDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + Interlocked.CompareExchange(ref _lazySyntaxDiagnostics, syntaxDiagnostics, null); } - return await GetCompilerAnalyzerDiagnosticsInProcessAsync(analyzer, AnalysisScope.Span, cancellationToken).ConfigureAwait(false); + return _lazySyntaxDiagnostics.TryGetValue(analyzer, out var diagnosticAnalysisResult) + ? diagnosticAnalysisResult.GetDocumentDiagnostics(AnalysisScope.TextDocument.Id, AnalysisScope.Kind) + : []; } - if (_lazySyntaxDiagnostics == null) + async Task> GetSemanticDiagnosticsInProcessAsync() { - using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSyntaxDiagnosticsInProcessAsync)}.{nameof(GetAnalysisResultInProcessAsync)}"); - - var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); - var syntaxDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); - Interlocked.CompareExchange(ref _lazySyntaxDiagnostics, syntaxDiagnostics, null); - } - - return _lazySyntaxDiagnostics.TryGetValue(analyzer, out var diagnosticAnalysisResult) - ? diagnosticAnalysisResult.GetDocumentDiagnostics(AnalysisScope.TextDocument.Id, AnalysisScope.Kind) - : []; - } - - private async Task> GetSemanticDiagnosticsInProcessAsync(DiagnosticAnalyzer analyzer, bool isCompilerAnalyzer, CancellationToken cancellationToken) - { - // PERF: - // 1. Compute diagnostics for all analyzers with a single invocation into CompilationWithAnalyzers. - // This is critical for better analyzer execution performance through re-use of bound node cache. - // 2. Ensure that the compiler analyzer is treated specially and does not block on diagnostic computation - // for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing. + // PERF: + // 1. Compute diagnostics for all analyzers with a single invocation into CompilationWithAnalyzers. + // This is critical for better analyzer execution performance through re-use of bound node cache. + // 2. Ensure that the compiler analyzer is treated specially and does not block on diagnostic computation + // for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing. - RoslynDebug.Assert(_compilationWithAnalyzers != null); + RoslynDebug.Assert(_compilationWithAnalyzers != null); - var span = AnalysisScope.Span; - var document = (Document)AnalysisScope.TextDocument; - if (isCompilerAnalyzer) - { + var span = AnalysisScope.Span; + var document = (Document)AnalysisScope.TextDocument; + if (isCompilerAnalyzer) + { #if DEBUG - await VerifySpanBasedCompilerDiagnosticsAsync().ConfigureAwait(false); + await VerifySpanBasedCompilerDiagnosticsAsync().ConfigureAwait(false); #endif - var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync().ConfigureAwait(false); - return await GetCompilerAnalyzerDiagnosticsInProcessAsync(analyzer, adjustedSpan, cancellationToken).ConfigureAwait(false); - } - - if (_lazySemanticDiagnostics == null) - { - using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSemanticDiagnosticsInProcessAsync)}.{nameof(GetAnalysisResultInProcessAsync)}"); - - var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); - var semanticDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); - Interlocked.CompareExchange(ref _lazySemanticDiagnostics, semanticDiagnostics, null); - } + var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync().ConfigureAwait(false); + return await GetCompilerAnalyzerDiagnosticsInProcessAsync(analyzer, adjustedSpan, cancellationToken).ConfigureAwait(false); + } - return _lazySemanticDiagnostics.TryGetValue(analyzer, out var diagnosticAnalysisResult) - ? diagnosticAnalysisResult.GetDocumentDiagnostics(AnalysisScope.TextDocument.Id, AnalysisScope.Kind) - : []; + if (_lazySemanticDiagnostics == null) + { + using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSemanticDiagnosticsInProcessAsync)}.{nameof(GetAnalysisResultInProcessAsync)}"); - async Task GetAdjustedSpanForCompilerAnalyzerAsync() - { - // This method is to workaround a bug (https://github.com/dotnet/roslyn/issues/1557) - // once that bug is fixed, we should be able to use given span as it is. + var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); + var semanticDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + Interlocked.CompareExchange(ref _lazySemanticDiagnostics, semanticDiagnostics, null); + } - Debug.Assert(isCompilerAnalyzer); + return _lazySemanticDiagnostics.TryGetValue(analyzer, out var diagnosticAnalysisResult) + ? diagnosticAnalysisResult.GetDocumentDiagnostics(AnalysisScope.TextDocument.Id, AnalysisScope.Kind) + : []; - if (!span.HasValue) + async Task GetAdjustedSpanForCompilerAnalyzerAsync() { - return null; - } + // This method is to workaround a bug (https://github.com/dotnet/roslyn/issues/1557) + // once that bug is fixed, we should be able to use given span as it is. - var service = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var startNode = service.GetContainingMemberDeclaration(root, span.Value.Start); - var endNode = service.GetContainingMemberDeclaration(root, span.Value.End); + Debug.Assert(isCompilerAnalyzer); - if (startNode == endNode) - { - // use full member span - if (service.IsMethodLevelMember(startNode)) + if (!span.HasValue) { - return startNode.FullSpan; + return null; } - // use span as it is - return span; - } + var service = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var startNode = service.GetContainingMemberDeclaration(root, span.Value.Start); + var endNode = service.GetContainingMemberDeclaration(root, span.Value.End); + + if (startNode == endNode) + { + // use full member span + if (service.IsMethodLevelMember(startNode)) + { + return startNode.FullSpan; + } + + // use span as it is + return span; + } - var startSpan = service.IsMethodLevelMember(startNode) ? startNode.FullSpan : span.Value; - var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : span.Value; + var startSpan = service.IsMethodLevelMember(startNode) ? startNode.FullSpan : span.Value; + var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : span.Value; - return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End)); - } + return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End)); + } #if DEBUG - async Task VerifySpanBasedCompilerDiagnosticsAsync() - { - if (!span.HasValue) + async Task VerifySpanBasedCompilerDiagnosticsAsync() { - return; - } + if (!span.HasValue) + { + return; + } - // make sure what we got from range is same as what we got from whole diagnostics - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var rangeDeclaractionDiagnostics = model.GetDeclarationDiagnostics(span.Value, cancellationToken).ToArray(); - var rangeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(span.Value, cancellationToken).ToArray(); - var rangeDiagnostics = rangeDeclaractionDiagnostics.Concat(rangeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); + // make sure what we got from range is same as what we got from whole diagnostics + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var rangeDeclaractionDiagnostics = model.GetDeclarationDiagnostics(span.Value, cancellationToken).ToArray(); + var rangeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(span.Value, cancellationToken).ToArray(); + var rangeDiagnostics = rangeDeclaractionDiagnostics.Concat(rangeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); - var wholeDeclarationDiagnostics = model.GetDeclarationDiagnostics(cancellationToken: cancellationToken).ToArray(); - var wholeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(cancellationToken: cancellationToken).ToArray(); - var wholeDiagnostics = wholeDeclarationDiagnostics.Concat(wholeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); + var wholeDeclarationDiagnostics = model.GetDeclarationDiagnostics(cancellationToken: cancellationToken).ToArray(); + var wholeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(cancellationToken: cancellationToken).ToArray(); + var wholeDiagnostics = wholeDeclarationDiagnostics.Concat(wholeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); - if (!AreEquivalent(rangeDiagnostics, wholeDiagnostics)) - { - // otherwise, report non-fatal watson so that we can fix those cases - FatalError.ReportAndCatch(new Exception("Bug in GetDiagnostics")); - - // make sure we hold onto these for debugging. - GC.KeepAlive(rangeDeclaractionDiagnostics); - GC.KeepAlive(rangeMethodBodyDiagnostics); - GC.KeepAlive(rangeDiagnostics); - GC.KeepAlive(wholeDeclarationDiagnostics); - GC.KeepAlive(wholeMethodBodyDiagnostics); - GC.KeepAlive(wholeDiagnostics); - } + if (!AreEquivalent(rangeDiagnostics, wholeDiagnostics)) + { + // otherwise, report non-fatal watson so that we can fix those cases + FatalError.ReportAndCatch(new Exception("Bug in GetDiagnostics")); + + // make sure we hold onto these for debugging. + GC.KeepAlive(rangeDeclaractionDiagnostics); + GC.KeepAlive(rangeMethodBodyDiagnostics); + GC.KeepAlive(rangeDiagnostics); + GC.KeepAlive(wholeDeclarationDiagnostics); + GC.KeepAlive(wholeMethodBodyDiagnostics); + GC.KeepAlive(wholeDiagnostics); + } - return; + return; - static bool IsUnusedImportDiagnostic(Diagnostic d) - { - switch (d.Id) + static bool IsUnusedImportDiagnostic(Diagnostic d) { - case "CS8019": - case "BC50000": - case "BC50001": - return true; - default: - return false; + switch (d.Id) + { + case "CS8019": + case "BC50000": + case "BC50001": + return true; + default: + return false; + } } + + // Exclude unused import diagnostics since they are never reported when a span is passed. + // (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.) + bool shouldInclude(Diagnostic d) => span.Value.IntersectsWith(d.Location.SourceSpan) && !IsUnusedImportDiagnostic(d); } +#endif + } + } + + private async Task> GetAnalysisResultInProcessAsync( + DocumentAnalysisScope analysisScope, CancellationToken cancellationToken) + { + RoslynDebug.Assert(_compilationWithAnalyzers != null); - // Exclude unused import diagnostics since they are never reported when a span is passed. - // (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.) - bool shouldInclude(Diagnostic d) => span.Value.IntersectsWith(d.Location.SourceSpan) && !IsUnusedImportDiagnostic(d); + try + { + var resultAndTelemetry = await _diagnosticAnalyzerService.AnalyzeInProcessAsync( + analysisScope, analysisScope.TextDocument.Project, _compilationWithAnalyzers, _logPerformanceInfo, getTelemetryInfo: false, cancellationToken).ConfigureAwait(false); + return resultAndTelemetry.AnalysisResult; } -#endif + catch when (_onAnalysisException != null) + { + _onAnalysisException.Invoke(); + throw; + } + } + + private async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(DiagnosticAnalyzer analyzer, TextSpan? span, CancellationToken cancellationToken) + { + RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); + RoslynDebug.Assert(_compilationWithAnalyzers != null); + RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); + RoslynDebug.Assert(AnalysisScope.TextDocument is Document); + + var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) + ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) + : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); + var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + if (!analysisResult.TryGetValue(analyzer, out var result)) + { + return []; + } + + return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); } private static async Task> RemapDiagnosticLocationsIfRequiredAsync( From 8a65861416e0a4ef99cc67f86d665c1b6bd23838 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:07:32 -0400 Subject: [PATCH 04/10] Inline methods --- .../Service/DocumentAnalysisExecutor.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index 29a440ee50886..e39b0ab688e63 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -167,7 +167,7 @@ async Task> GetSyntaxDiagnosticsInProcessAsync() if (AnalysisScope.TextDocument is not Document) return []; - return await GetCompilerAnalyzerDiagnosticsInProcessAsync(analyzer, AnalysisScope.Span, cancellationToken).ConfigureAwait(false); + return await GetCompilerAnalyzerDiagnosticsInProcessAsync(AnalysisScope.Span).ConfigureAwait(false); } if (_lazySyntaxDiagnostics == null) @@ -203,7 +203,7 @@ async Task> GetSemanticDiagnosticsInProcessAsync( #endif var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync().ConfigureAwait(false); - return await GetCompilerAnalyzerDiagnosticsInProcessAsync(analyzer, adjustedSpan, cancellationToken).ConfigureAwait(false); + return await GetCompilerAnalyzerDiagnosticsInProcessAsync(adjustedSpan).ConfigureAwait(false); } if (_lazySemanticDiagnostics == null) @@ -307,6 +307,25 @@ static bool IsUnusedImportDiagnostic(Diagnostic d) } #endif } + + async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(TextSpan? span) + { + RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); + RoslynDebug.Assert(_compilationWithAnalyzers != null); + RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); + RoslynDebug.Assert(AnalysisScope.TextDocument is Document); + + var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) + ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) + : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); + var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + if (!analysisResult.TryGetValue(analyzer, out var result)) + { + return []; + } + + return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); + } } private async Task> GetAnalysisResultInProcessAsync( @@ -327,25 +346,6 @@ private async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(DiagnosticAnalyzer analyzer, TextSpan? span, CancellationToken cancellationToken) - { - RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); - RoslynDebug.Assert(_compilationWithAnalyzers != null); - RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); - RoslynDebug.Assert(AnalysisScope.TextDocument is Document); - - var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) - ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) - : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); - var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); - if (!analysisResult.TryGetValue(analyzer, out var result)) - { - return []; - } - - return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); - } - private static async Task> RemapDiagnosticLocationsIfRequiredAsync( TextDocument textDocument, ImmutableArray diagnostics, From e661ed22d6bf2489878ccf1c3a71fe7d38e0e23b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:09:28 -0400 Subject: [PATCH 05/10] Outline method --- .../Service/DocumentAnalysisExecutor.cs | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index e39b0ab688e63..d0f836fc2acc3 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -175,7 +175,7 @@ async Task> GetSyntaxDiagnosticsInProcessAsync() using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSyntaxDiagnosticsInProcessAsync)}.{nameof(GetAnalysisResultInProcessAsync)}"); var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); - var syntaxDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + var syntaxDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope).ConfigureAwait(false); Interlocked.CompareExchange(ref _lazySyntaxDiagnostics, syntaxDiagnostics, null); } @@ -199,7 +199,7 @@ async Task> GetSemanticDiagnosticsInProcessAsync( if (isCompilerAnalyzer) { #if DEBUG - await VerifySpanBasedCompilerDiagnosticsAsync().ConfigureAwait(false); + await VerifySpanBasedCompilerDiagnosticsAsync(document).ConfigureAwait(false); #endif var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync().ConfigureAwait(false); @@ -211,7 +211,7 @@ async Task> GetSemanticDiagnosticsInProcessAsync( using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSemanticDiagnosticsInProcessAsync)}.{nameof(GetAnalysisResultInProcessAsync)}"); var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); - var semanticDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + var semanticDiagnostics = await GetAnalysisResultInProcessAsync(analysisScope).ConfigureAwait(false); Interlocked.CompareExchange(ref _lazySemanticDiagnostics, semanticDiagnostics, null); } @@ -253,60 +253,60 @@ async Task> GetSemanticDiagnosticsInProcessAsync( return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End)); } + } #if DEBUG - async Task VerifySpanBasedCompilerDiagnosticsAsync() + async Task VerifySpanBasedCompilerDiagnosticsAsync(Document document) + { + if (!span.HasValue) { - if (!span.HasValue) - { - return; - } + return; + } - // make sure what we got from range is same as what we got from whole diagnostics - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var rangeDeclaractionDiagnostics = model.GetDeclarationDiagnostics(span.Value, cancellationToken).ToArray(); - var rangeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(span.Value, cancellationToken).ToArray(); - var rangeDiagnostics = rangeDeclaractionDiagnostics.Concat(rangeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); + // make sure what we got from range is same as what we got from whole diagnostics + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var rangeDeclaractionDiagnostics = model.GetDeclarationDiagnostics(span.Value, cancellationToken).ToArray(); + var rangeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(span.Value, cancellationToken).ToArray(); + var rangeDiagnostics = rangeDeclaractionDiagnostics.Concat(rangeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); - var wholeDeclarationDiagnostics = model.GetDeclarationDiagnostics(cancellationToken: cancellationToken).ToArray(); - var wholeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(cancellationToken: cancellationToken).ToArray(); - var wholeDiagnostics = wholeDeclarationDiagnostics.Concat(wholeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); + var wholeDeclarationDiagnostics = model.GetDeclarationDiagnostics(cancellationToken: cancellationToken).ToArray(); + var wholeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(cancellationToken: cancellationToken).ToArray(); + var wholeDiagnostics = wholeDeclarationDiagnostics.Concat(wholeMethodBodyDiagnostics).Where(shouldInclude).ToArray(); - if (!AreEquivalent(rangeDiagnostics, wholeDiagnostics)) - { - // otherwise, report non-fatal watson so that we can fix those cases - FatalError.ReportAndCatch(new Exception("Bug in GetDiagnostics")); - - // make sure we hold onto these for debugging. - GC.KeepAlive(rangeDeclaractionDiagnostics); - GC.KeepAlive(rangeMethodBodyDiagnostics); - GC.KeepAlive(rangeDiagnostics); - GC.KeepAlive(wholeDeclarationDiagnostics); - GC.KeepAlive(wholeMethodBodyDiagnostics); - GC.KeepAlive(wholeDiagnostics); - } + if (!AreEquivalent(rangeDiagnostics, wholeDiagnostics)) + { + // otherwise, report non-fatal watson so that we can fix those cases + FatalError.ReportAndCatch(new Exception("Bug in GetDiagnostics")); + + // make sure we hold onto these for debugging. + GC.KeepAlive(rangeDeclaractionDiagnostics); + GC.KeepAlive(rangeMethodBodyDiagnostics); + GC.KeepAlive(rangeDiagnostics); + GC.KeepAlive(wholeDeclarationDiagnostics); + GC.KeepAlive(wholeMethodBodyDiagnostics); + GC.KeepAlive(wholeDiagnostics); + } - return; + return; - static bool IsUnusedImportDiagnostic(Diagnostic d) + static bool IsUnusedImportDiagnostic(Diagnostic d) + { + switch (d.Id) { - switch (d.Id) - { - case "CS8019": - case "BC50000": - case "BC50001": - return true; - default: - return false; - } + case "CS8019": + case "BC50000": + case "BC50001": + return true; + default: + return false; } - - // Exclude unused import diagnostics since they are never reported when a span is passed. - // (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.) - bool shouldInclude(Diagnostic d) => span.Value.IntersectsWith(d.Location.SourceSpan) && !IsUnusedImportDiagnostic(d); } -#endif + + // Exclude unused import diagnostics since they are never reported when a span is passed. + // (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.) + bool shouldInclude(Diagnostic d) => span.Value.IntersectsWith(d.Location.SourceSpan) && !IsUnusedImportDiagnostic(d); } +#endif async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(TextSpan? span) { @@ -318,7 +318,7 @@ async Task> GetCompilerAnalyzerDiagnosticsInProce var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); - var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope, cancellationToken).ConfigureAwait(false); + var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope).ConfigureAwait(false); if (!analysisResult.TryGetValue(analyzer, out var result)) { return []; @@ -326,23 +326,23 @@ async Task> GetCompilerAnalyzerDiagnosticsInProce return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); } - } - - private async Task> GetAnalysisResultInProcessAsync( - DocumentAnalysisScope analysisScope, CancellationToken cancellationToken) - { - RoslynDebug.Assert(_compilationWithAnalyzers != null); - try + async Task> GetAnalysisResultInProcessAsync( + DocumentAnalysisScope analysisScope) { - var resultAndTelemetry = await _diagnosticAnalyzerService.AnalyzeInProcessAsync( - analysisScope, analysisScope.TextDocument.Project, _compilationWithAnalyzers, _logPerformanceInfo, getTelemetryInfo: false, cancellationToken).ConfigureAwait(false); - return resultAndTelemetry.AnalysisResult; - } - catch when (_onAnalysisException != null) - { - _onAnalysisException.Invoke(); - throw; + RoslynDebug.Assert(_compilationWithAnalyzers != null); + + try + { + var resultAndTelemetry = await _diagnosticAnalyzerService.AnalyzeInProcessAsync( + analysisScope, analysisScope.TextDocument.Project, _compilationWithAnalyzers, _logPerformanceInfo, getTelemetryInfo: false, cancellationToken).ConfigureAwait(false); + return resultAndTelemetry.AnalysisResult; + } + catch when (_onAnalysisException != null) + { + _onAnalysisException.Invoke(); + throw; + } } } From a3bb8c94dc5fba6d5ef920040c25f384ae863f4a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:11:59 -0400 Subject: [PATCH 06/10] Inline methods --- .../Service/DocumentAnalysisExecutor.cs | 96 +++++++++---------- 1 file changed, 45 insertions(+), 51 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index d0f836fc2acc3..ca3a0d2ade679 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -129,7 +129,7 @@ public async Task> ComputeDiagnosticsInProcessAsy }; // Remap diagnostic locations, if required. - diagnostics = await RemapDiagnosticLocationsIfRequiredAsync(textDocument, diagnostics, cancellationToken).ConfigureAwait(false); + diagnostics = await RemapDiagnosticLocationsIfRequiredAsync(diagnostics).ConfigureAwait(false); if (span.HasValue) { @@ -202,7 +202,7 @@ async Task> GetSemanticDiagnosticsInProcessAsync( await VerifySpanBasedCompilerDiagnosticsAsync(document).ConfigureAwait(false); #endif - var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync().ConfigureAwait(false); + var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync(document).ConfigureAwait(false); return await GetCompilerAnalyzerDiagnosticsInProcessAsync(adjustedSpan).ConfigureAwait(false); } @@ -218,41 +218,41 @@ async Task> GetSemanticDiagnosticsInProcessAsync( return _lazySemanticDiagnostics.TryGetValue(analyzer, out var diagnosticAnalysisResult) ? diagnosticAnalysisResult.GetDocumentDiagnostics(AnalysisScope.TextDocument.Id, AnalysisScope.Kind) : []; + } - async Task GetAdjustedSpanForCompilerAnalyzerAsync() - { - // This method is to workaround a bug (https://github.com/dotnet/roslyn/issues/1557) - // once that bug is fixed, we should be able to use given span as it is. + async Task GetAdjustedSpanForCompilerAnalyzerAsync(Document document) + { + // This method is to workaround a bug (https://github.com/dotnet/roslyn/issues/1557) + // once that bug is fixed, we should be able to use given span as it is. - Debug.Assert(isCompilerAnalyzer); + Debug.Assert(isCompilerAnalyzer); - if (!span.HasValue) - { - return null; - } + if (!span.HasValue) + { + return null; + } - var service = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var startNode = service.GetContainingMemberDeclaration(root, span.Value.Start); - var endNode = service.GetContainingMemberDeclaration(root, span.Value.End); + var service = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var startNode = service.GetContainingMemberDeclaration(root, span.Value.Start); + var endNode = service.GetContainingMemberDeclaration(root, span.Value.End); - if (startNode == endNode) + if (startNode == endNode) + { + // use full member span + if (service.IsMethodLevelMember(startNode)) { - // use full member span - if (service.IsMethodLevelMember(startNode)) - { - return startNode.FullSpan; - } - - // use span as it is - return span; + return startNode.FullSpan; } - var startSpan = service.IsMethodLevelMember(startNode) ? startNode.FullSpan : span.Value; - var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : span.Value; - - return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End)); + // use span as it is + return span; } + + var startSpan = service.IsMethodLevelMember(startNode) ? startNode.FullSpan : span.Value; + var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : span.Value; + + return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End)); } #if DEBUG @@ -344,34 +344,28 @@ async Task> Ge throw; } } - } - private static async Task> RemapDiagnosticLocationsIfRequiredAsync( - TextDocument textDocument, - ImmutableArray diagnostics, - CancellationToken cancellationToken) - { - if (diagnostics.IsEmpty) + async Task> RemapDiagnosticLocationsIfRequiredAsync( + ImmutableArray diagnostics) { - return diagnostics; - } + if (diagnostics.IsEmpty) + return diagnostics; - // Check if IWorkspaceVenusSpanMappingService is present for remapping. - var diagnosticSpanMappingService = textDocument.Project.Solution.Services.GetService(); - if (diagnosticSpanMappingService == null) - { - return diagnostics; - } + // Check if IWorkspaceVenusSpanMappingService is present for remapping. + var diagnosticSpanMappingService = textDocument.Project.Solution.Services.GetService(); + if (diagnosticSpanMappingService == null) + return diagnostics; - // Round tripping the diagnostics should ensure they get correctly remapped. - var builder = new FixedSizeArrayBuilder(diagnostics.Length); - foreach (var diagnosticData in diagnostics) - { - var diagnostic = await diagnosticData.ToDiagnosticAsync(textDocument.Project, cancellationToken).ConfigureAwait(false); - builder.Add(DiagnosticData.Create(diagnostic, textDocument)); - } + // Round tripping the diagnostics should ensure they get correctly remapped. + var builder = new FixedSizeArrayBuilder(diagnostics.Length); + foreach (var diagnosticData in diagnostics) + { + var diagnostic = await diagnosticData.ToDiagnosticAsync(textDocument.Project, cancellationToken).ConfigureAwait(false); + builder.Add(DiagnosticData.Create(diagnostic, textDocument)); + } - return builder.MoveToImmutable(); + return builder.MoveToImmutable(); + } } } } From 7b231f16dbeaae3f5bec7e80a4651674a6e660dc Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:15:21 -0400 Subject: [PATCH 07/10] reorder --- .../Service/DocumentAnalysisExecutor.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index ca3a0d2ade679..54f7b3e1430f1 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -151,6 +151,24 @@ public async Task> ComputeDiagnosticsInProcessAsy return diagnostics; + async Task> GetAnalysisResultInProcessAsync( + DocumentAnalysisScope analysisScope) + { + RoslynDebug.Assert(_compilationWithAnalyzers != null); + + try + { + var resultAndTelemetry = await _diagnosticAnalyzerService.AnalyzeInProcessAsync( + analysisScope, analysisScope.TextDocument.Project, _compilationWithAnalyzers, _logPerformanceInfo, getTelemetryInfo: false, cancellationToken).ConfigureAwait(false); + return resultAndTelemetry.AnalysisResult; + } + catch when (_onAnalysisException != null) + { + _onAnalysisException.Invoke(); + throw; + } + } + async Task> GetSyntaxDiagnosticsInProcessAsync() { // PERF: @@ -327,24 +345,6 @@ async Task> GetCompilerAnalyzerDiagnosticsInProce return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); } - async Task> GetAnalysisResultInProcessAsync( - DocumentAnalysisScope analysisScope) - { - RoslynDebug.Assert(_compilationWithAnalyzers != null); - - try - { - var resultAndTelemetry = await _diagnosticAnalyzerService.AnalyzeInProcessAsync( - analysisScope, analysisScope.TextDocument.Project, _compilationWithAnalyzers, _logPerformanceInfo, getTelemetryInfo: false, cancellationToken).ConfigureAwait(false); - return resultAndTelemetry.AnalysisResult; - } - catch when (_onAnalysisException != null) - { - _onAnalysisException.Invoke(); - throw; - } - } - async Task> RemapDiagnosticLocationsIfRequiredAsync( ImmutableArray diagnostics) { From 7036beb154359789db97c3ad510fc9fb5bf4610e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:16:05 -0400 Subject: [PATCH 08/10] reorder --- .../Service/DocumentAnalysisExecutor.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index 54f7b3e1430f1..42a908bf57fd2 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -169,6 +169,25 @@ async Task> Ge } } + async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(TextSpan? span) + { + RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); + RoslynDebug.Assert(_compilationWithAnalyzers != null); + RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); + RoslynDebug.Assert(AnalysisScope.TextDocument is Document); + + var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) + ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) + : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); + var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope).ConfigureAwait(false); + if (!analysisResult.TryGetValue(analyzer, out var result)) + { + return []; + } + + return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); + } + async Task> GetSyntaxDiagnosticsInProcessAsync() { // PERF: @@ -326,25 +345,6 @@ static bool IsUnusedImportDiagnostic(Diagnostic d) } #endif - async Task> GetCompilerAnalyzerDiagnosticsInProcessAsync(TextSpan? span) - { - RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); - RoslynDebug.Assert(_compilationWithAnalyzers != null); - RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); - RoslynDebug.Assert(AnalysisScope.TextDocument is Document); - - var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) - ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) - : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); - var analysisResult = await GetAnalysisResultInProcessAsync(analysisScope).ConfigureAwait(false); - if (!analysisResult.TryGetValue(analyzer, out var result)) - { - return []; - } - - return result.GetDocumentDiagnostics(analysisScope.TextDocument.Id, analysisScope.Kind); - } - async Task> RemapDiagnosticLocationsIfRequiredAsync( ImmutableArray diagnostics) { From 4844fc7a1251a668c1746618a12c5fea4569a445 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:26:20 -0400 Subject: [PATCH 09/10] Extract code --- ...zerService_CompilationWithAnalyzersPair.cs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CompilationWithAnalyzersPair.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CompilationWithAnalyzersPair.cs index 42e9a8a9d2c31..67710bdd738eb 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CompilationWithAnalyzersPair.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_CompilationWithAnalyzersPair.cs @@ -116,30 +116,30 @@ private static readonly ConditionalWeakTable< return true; }; - // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to - // async being used with synchronous blocking concurrency. - var projectCompilation = !filteredProjectAnalyzers.Any() - ? null - : compilation.WithAnalyzers(filteredProjectAnalyzers, new CompilationWithAnalyzersOptions( - options: project.State.ProjectAnalyzerOptions, - onAnalyzerException: null, - analyzerExceptionFilter: exceptionFilter, - concurrentAnalysis: false, - logAnalyzerExecutionTime: true, - reportSuppressedDiagnostics: true)); - - var hostCompilation = !filteredHostAnalyzers.Any() - ? null - : compilation.WithAnalyzers(filteredHostAnalyzers, new CompilationWithAnalyzersOptions( - options: project.HostAnalyzerOptions, - onAnalyzerException: null, - analyzerExceptionFilter: exceptionFilter, - concurrentAnalysis: false, - logAnalyzerExecutionTime: true, - reportSuppressedDiagnostics: true)); - // Create driver that holds onto compilation and associated analyzers - return new CompilationWithAnalyzersPair(projectCompilation, hostCompilation); + return new( + CreateCompilationWithAnalyzers(compilation, filteredProjectAnalyzers, project.State.ProjectAnalyzerOptions, exceptionFilter), + CreateCompilationWithAnalyzers(compilation, filteredHostAnalyzers, project.HostAnalyzerOptions, exceptionFilter)); + } + + static CompilationWithAnalyzers? CreateCompilationWithAnalyzers( + Compilation compilation, + ImmutableArray analyzers, + AnalyzerOptions? options, + Func exceptionFilter) + { + if (analyzers.Length == 0) + return null; + + return compilation.WithAnalyzers(analyzers, new CompilationWithAnalyzersOptions( + options: options, + onAnalyzerException: null, + analyzerExceptionFilter: exceptionFilter, + // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to + // async being used with synchronous blocking concurrency. + concurrentAnalysis: false, + logAnalyzerExecutionTime: true, + reportSuppressedDiagnostics: true)); } } } From 2463fc41ad6d58d2740bc8307bf884243104c39e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 22 Aug 2025 00:45:29 -0400 Subject: [PATCH 10/10] Make non-static --- .../DiagnosticAnalyzerService.ProjectStates.cs | 4 ---- .../Diagnostics/Service/DiagnosticAnalyzerService.cs | 12 ++++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.ProjectStates.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.ProjectStates.cs index 46e909aeabd08..3ed6b47e911e9 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.ProjectStates.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.ProjectStates.cs @@ -53,16 +53,12 @@ private ProjectAnalyzerInfo GetOrCreateProjectAnalyzerInfo(Project project) private ProjectAnalyzerInfo CreateProjectAnalyzerInfo(Project project) { if (project.AnalyzerReferences.Count == 0) - { return ProjectAnalyzerInfo.Default; - } var solutionAnalyzers = project.Solution.SolutionState.Analyzers; var analyzersPerReference = solutionAnalyzers.CreateProjectDiagnosticAnalyzersPerReference(project.State); if (analyzersPerReference.Count == 0) - { return ProjectAnalyzerInfo.Default; - } var (newHostAnalyzers, newAllAnalyzers) = PartitionAnalyzers( [.. analyzersPerReference.Values], hostAnalyzerCollection: [], includeWorkspacePlaceholderAnalyzers: false); diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs index f6bb1d2680205..5b0ebf808e17c 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs @@ -144,12 +144,12 @@ bool ShouldIncludeAnalyzer(Project project, DiagnosticAnalyzer analyzer) } } - public async Task> GetDiagnosticsForIdsAsync( + public Task> GetDiagnosticsForIdsAsync( Project project, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) { var analyzers = GetDiagnosticAnalyzers(project, diagnosticIds, shouldIncludeAnalyzer); - return await ProduceProjectDiagnosticsAsync( + return ProduceProjectDiagnosticsAsync( project, analyzers, diagnosticIds, // Ensure we compute and return diagnostics for both the normal docs and the additional docs in this // project if no specific document id was requested. @@ -158,23 +158,23 @@ public async Task> GetDiagnosticsForIdsAsync( includeNonLocalDocumentDiagnostics, // return diagnostics specific to one project or document includeProjectNonLocalResult: documentId == null, - cancellationToken).ConfigureAwait(false); + cancellationToken); } - public async Task> GetProjectDiagnosticsForIdsAsync( + public Task> GetProjectDiagnosticsForIdsAsync( Project project, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) { var analyzers = GetDiagnosticAnalyzers(project, diagnosticIds, shouldIncludeAnalyzer); - return await ProduceProjectDiagnosticsAsync( + return ProduceProjectDiagnosticsAsync( project, analyzers, diagnosticIds, documentIds: [], includeLocalDocumentDiagnostics: false, includeNonLocalDocumentDiagnostics: includeNonLocalDocumentDiagnostics, includeProjectNonLocalResult: true, - cancellationToken).ConfigureAwait(false); + cancellationToken); } public TestAccessor GetTestAccessor()