Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
721db06
Extract class to its own file
CyrusNajmabadi Aug 21, 2025
687783a
Extract remote-host dispatching code to its own file
CyrusNajmabadi Aug 21, 2025
1d38cc0
Remove unnecessary 'diag incremental analyzer' indirection
CyrusNajmabadi Aug 21, 2025
da0c2db
Make fields private
CyrusNajmabadi Aug 21, 2025
84e49a4
Merge related code and rename file
CyrusNajmabadi Aug 21, 2025
0478e3e
Merge related code and rename file
CyrusNajmabadi Aug 21, 2025
ea82ef5
Merge function and helper
CyrusNajmabadi Aug 21, 2025
0c07bd3
Rename file
CyrusNajmabadi Aug 21, 2025
2b643cc
Rename files
CyrusNajmabadi Aug 21, 2025
00592e3
Name methods to make it clear where they run
CyrusNajmabadi Aug 21, 2025
bcdad14
Update tests
CyrusNajmabadi Aug 21, 2025
67b117a
Expose through test accessor
CyrusNajmabadi Aug 21, 2025
4a0c3d5
Inline methods
CyrusNajmabadi Aug 21, 2025
53ba623
Extract type into its own file and rename file
CyrusNajmabadi Aug 21, 2025
644a8a0
Rename file
CyrusNajmabadi Aug 21, 2025
575fc07
Move files up one directory
CyrusNajmabadi Aug 21, 2025
ca1b7c4
Use immutable arrays
CyrusNajmabadi Aug 21, 2025
a37c8d6
Remove 'state manager' indirection
CyrusNajmabadi Aug 21, 2025
2052db0
inline method
CyrusNajmabadi Aug 21, 2025
3004968
Simply arguments
CyrusNajmabadi Aug 21, 2025
693a77a
Disposable buildrs
CyrusNajmabadi Aug 21, 2025
4ed8d81
Switch to a non-async interlocked update for our dictionaries
CyrusNajmabadi Aug 21, 2025
d3b74d7
Merge branch 'main' into diagCleanup
CyrusNajmabadi Aug 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ public async Task TestDiagnosticSpan()
var compilerEngineCompilation = (CSharpCompilation)(await compilerEngineWorkspace.CurrentSolution.Projects.Single().GetRequiredCompilationAsync(CancellationToken.None));

var diagnostics = compilerEngineCompilation.GetAnalyzerDiagnostics([analyzer]);
AssertEx.Any(diagnostics, d => d.Id == DocumentAnalysisExecutor.AnalyzerExceptionDiagnosticId);
AssertEx.Any(diagnostics, d => d.Id == DiagnosticAnalyzerService.AnalyzerExceptionDiagnosticId);
}

private sealed class InvalidSpanAnalyzer : DiagnosticAnalyzer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Microsoft.CodeAnalysis.Diagnostics;

internal sealed partial class DiagnosticAnalyzerService
{
private sealed class DiagnosticAnalyzerComparer : IEqualityComparer<DiagnosticAnalyzer>
{
public static readonly DiagnosticAnalyzerComparer Instance = new();

public bool Equals(DiagnosticAnalyzer? x, DiagnosticAnalyzer? y)
=> (x, y) switch
{
(null, null) => true,
(null, _) => false,
(_, null) => false,
_ => GetAnalyzerIdAndLastWriteTime(x) == GetAnalyzerIdAndLastWriteTime(y)
};

public int GetHashCode(DiagnosticAnalyzer obj) => GetAnalyzerIdAndLastWriteTime(obj).GetHashCode();

private static (string analyzerId, DateTime lastWriteTime) GetAnalyzerIdAndLastWriteTime(DiagnosticAnalyzer analyzer)
{
// Get the unique ID for given diagnostic analyzer.
// note that we also put version stamp so that we can detect changed analyzer.
var typeInfo = analyzer.GetType().GetTypeInfo();
return (analyzer.GetAnalyzerId(), GetAnalyzerLastWriteTime(typeInfo.Assembly.Location));
}

private static DateTime GetAnalyzerLastWriteTime(string path)
{
if (path == null || !File.Exists(path))
return default;

return File.GetLastWriteTimeUtc(path);
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Diagnostics;

// this part contains the methods that will attempt to call out to OOP to do the work, and fall back
// to processing locally if it is not available (or we are already in OOP).

internal sealed partial class DiagnosticAnalyzerService : IDiagnosticAnalyzerService
{
public async Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var result = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableArray<DiagnosticData>>(
project,
(service, solution, cancellationToken) => service.ForceAnalyzeProjectAsync(solution, project.Id, cancellationToken),
cancellationToken).ConfigureAwait(false);

return result.HasValue ? result.Value : [];
}

// No OOP connection. Compute in proc.
return await ForceAnalyzeProjectInProcessAsync(project, cancellationToken).ConfigureAwait(false);
}

public async Task<ImmutableArray<DiagnosticDescriptor>> GetDiagnosticDescriptorsAsync(
Solution solution, ProjectId projectId, AnalyzerReference analyzerReference, string language, CancellationToken cancellationToken)
{
// Attempt to compute this OOP.
var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false);
if (client is not null &&
analyzerReference is AnalyzerFileReference analyzerFileReference)
{
var descriptors = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableArray<DiagnosticDescriptorData>>(
solution,
(service, solution, cancellationToken) => service.GetDiagnosticDescriptorsAsync(solution, projectId, analyzerFileReference.FullPath, language, cancellationToken),
cancellationToken).ConfigureAwait(false);
if (!descriptors.HasValue)
return [];

return descriptors.Value.SelectAsArray(d => d.ToDiagnosticDescriptor());
}

// Otherwise, fallback to computing in proc.
return analyzerReference
.GetAnalyzers(language)
.SelectManyAsArray(this._analyzerInfoCache.GetDiagnosticDescriptors);
}

public async Task<ImmutableDictionary<ImmutableArray<string>, ImmutableArray<DiagnosticDescriptor>>> GetLanguageKeyedDiagnosticDescriptorsAsync(
Solution solution, ProjectId projectId, AnalyzerReference analyzerReference, CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false);
if (client is not null &&
analyzerReference is AnalyzerFileReference analyzerFileReference)
{
var map = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableDictionary<ImmutableArray<string>, ImmutableArray<DiagnosticDescriptorData>>>(
solution,
(service, solution, cancellationToken) => service.GetLanguageKeyedDiagnosticDescriptorsAsync(solution, projectId, analyzerFileReference.FullPath, cancellationToken),
cancellationToken).ConfigureAwait(false);

if (!map.HasValue)
return ImmutableDictionary<ImmutableArray<string>, ImmutableArray<DiagnosticDescriptor>>.Empty;

return map.Value.ToImmutableDictionary(
kvp => kvp.Key,
kvp => kvp.Value.SelectAsArray(d => d.ToDiagnosticDescriptor()));
}

// Otherwise, fallback to computing in proc.
var mapBuilder = ImmutableDictionary.CreateBuilder<ImmutableArray<string>, ImmutableArray<DiagnosticDescriptor>>();

var csharpAnalyzers = analyzerReference.GetAnalyzers(LanguageNames.CSharp);
var visualBasicAnalyzers = analyzerReference.GetAnalyzers(LanguageNames.VisualBasic);

var dotnetAnalyzers = csharpAnalyzers.Intersect(visualBasicAnalyzers, DiagnosticAnalyzerComparer.Instance).ToImmutableArray();
csharpAnalyzers = [.. csharpAnalyzers.Except(dotnetAnalyzers, DiagnosticAnalyzerComparer.Instance)];
visualBasicAnalyzers = [.. visualBasicAnalyzers.Except(dotnetAnalyzers, DiagnosticAnalyzerComparer.Instance)];

mapBuilder.Add(s_csharpLanguageArray, GetDiagnosticDescriptors(csharpAnalyzers));
mapBuilder.Add(s_visualBasicLanguageArray, GetDiagnosticDescriptors(visualBasicAnalyzers));
mapBuilder.Add(s_csharpAndVisualBasicLanguageArray, GetDiagnosticDescriptors(dotnetAnalyzers));

return mapBuilder.ToImmutable();

ImmutableArray<DiagnosticDescriptor> GetDiagnosticDescriptors(ImmutableArray<DiagnosticAnalyzer> analyzers)
=> analyzers.SelectManyAsArray(this._analyzerInfoCache.GetDiagnosticDescriptors);
}

public async Task<ImmutableDictionary<string, DiagnosticDescriptor>> TryGetDiagnosticDescriptorsAsync(
Solution solution, ImmutableArray<string> diagnosticIds, CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var map = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableDictionary<string, DiagnosticDescriptorData>>(
solution,
(service, solution, cancellationToken) => service.TryGetDiagnosticDescriptorsAsync(solution, diagnosticIds, cancellationToken),
cancellationToken).ConfigureAwait(false);

if (!map.HasValue)
return ImmutableDictionary<string, DiagnosticDescriptor>.Empty;

return map.Value.ToImmutableDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToDiagnosticDescriptor());
}

var builder = ImmutableDictionary.CreateBuilder<string, DiagnosticDescriptor>();
foreach (var diagnosticId in diagnosticIds)
{
if (this._analyzerInfoCache.TryGetDescriptorForDiagnosticId(diagnosticId, out var descriptor))
builder[diagnosticId] = descriptor;
}

return builder.ToImmutable();
}

public async Task<ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptor>>> GetDiagnosticDescriptorsPerReferenceAsync(Solution solution, CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var map = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptorData>>>(
solution,
(service, solution, cancellationToken) => service.GetDiagnosticDescriptorsPerReferenceAsync(solution, cancellationToken),
cancellationToken).ConfigureAwait(false);
if (!map.HasValue)
return ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptor>>.Empty;

return map.Value.ToImmutableDictionary(
kvp => kvp.Key,
kvp => kvp.Value.SelectAsArray(d => d.ToDiagnosticDescriptor()));
}

return solution.SolutionState.Analyzers.GetDiagnosticDescriptorsPerReference(this._analyzerInfoCache);
}

public async Task<ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptor>>> GetDiagnosticDescriptorsPerReferenceAsync(Project project, CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var map = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptorData>>>(
project,
(service, solution, cancellationToken) => service.GetDiagnosticDescriptorsPerReferenceAsync(solution, project.Id, cancellationToken),
cancellationToken).ConfigureAwait(false);
if (!map.HasValue)
return ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptor>>.Empty;

return map.Value.ToImmutableDictionary(
kvp => kvp.Key,
kvp => kvp.Value.SelectAsArray(d => d.ToDiagnosticDescriptor()));
}

return project.Solution.SolutionState.Analyzers.GetDiagnosticDescriptorsPerReference(this._analyzerInfoCache, project);
}

public async Task<ImmutableArray<DiagnosticAnalyzer>> GetDeprioritizationCandidatesAsync(
Project project, ImmutableArray<DiagnosticAnalyzer> analyzers, CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var analyzerIds = analyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
var result = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableHashSet<string>>(
project,
(service, solution, cancellationToken) => service.GetDeprioritizationCandidatesAsync(
solution, project.Id, analyzerIds, cancellationToken),
cancellationToken).ConfigureAwait(false);
if (!result.HasValue)
return [];

return analyzers.FilterAnalyzers(result.Value);
}

using var _ = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(out var builder);

var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(
project.Solution.SolutionState, project.State, cancellationToken).ConfigureAwait(false);
var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync(
project, analyzers, hostAnalyzerInfo, this.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false);

foreach (var analyzer in analyzers)
{
if (await IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(analyzer).ConfigureAwait(false))
builder.Add(analyzer);
}

return builder.ToImmutableAndClear();

async Task<bool> IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(DiagnosticAnalyzer analyzer)
{
// We deprioritize SymbolStart/End and SemanticModel analyzers from 'Normal' to 'Low' priority bucket,
// as these are computationally more expensive.
// Note that we never de-prioritize compiler analyzer, even though it registers a SemanticModel action.
if (compilationWithAnalyzers == null ||
analyzer.IsWorkspaceDiagnosticAnalyzer() ||
analyzer.IsCompilerAnalyzer())
{
return false;
}

var telemetryInfo = await compilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken).ConfigureAwait(false);
if (telemetryInfo == null)
return false;

return telemetryInfo.SymbolStartActionsCount > 0 || telemetryInfo.SemanticModelActionsCount > 0;
}
}

internal async Task<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsync(
Project project,
ImmutableArray<DiagnosticAnalyzer> analyzers,
ImmutableHashSet<string>? diagnosticIds,
ImmutableArray<DocumentId> documentIds,
bool includeLocalDocumentDiagnostics,
bool includeNonLocalDocumentDiagnostics,
bool includeProjectNonLocalResult,
CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var analyzerIds = analyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
var result = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableArray<DiagnosticData>>(
project,
(service, solution, cancellationToken) => service.ProduceProjectDiagnosticsAsync(
solution, project.Id, analyzerIds, diagnosticIds, documentIds,
includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, includeProjectNonLocalResult,
cancellationToken),
cancellationToken).ConfigureAwait(false);
if (!result.HasValue)
return [];

return result.Value;
}

// Fallback to proccessing in proc.
return await ProduceProjectDiagnosticsInProcessAsync(
project, analyzers, diagnosticIds, documentIds,
includeLocalDocumentDiagnostics,
includeNonLocalDocumentDiagnostics,
includeProjectNonLocalResult,
cancellationToken).ConfigureAwait(false);
}

public async Task<ImmutableArray<DiagnosticData>> ComputeDiagnosticsAsync(
TextDocument document,
TextSpan? range,
ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
ImmutableArray<DiagnosticAnalyzer> syntaxAnalyzers,
ImmutableArray<DiagnosticAnalyzer> semanticSpanAnalyzers,
ImmutableArray<DiagnosticAnalyzer> semanticDocumentAnalyzers,
bool incrementalAnalysis,
bool logPerformanceInfo,
CancellationToken cancellationToken)
{
if (allAnalyzers.Length == 0)
return [];

var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var allAnalyzerIds = allAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
var syntaxAnalyzersIds = syntaxAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
var semanticSpanAnalyzersIds = semanticSpanAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();
var semanticDocumentAnalyzersIds = semanticDocumentAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet();

var result = await client.TryInvokeAsync<IRemoteDiagnosticAnalyzerService, ImmutableArray<DiagnosticData>>(
document.Project,
(service, solution, cancellationToken) => service.ComputeDiagnosticsAsync(
solution, document.Id, range,
allAnalyzerIds, syntaxAnalyzersIds, semanticSpanAnalyzersIds, semanticDocumentAnalyzersIds,
incrementalAnalysis, logPerformanceInfo, cancellationToken),
cancellationToken).ConfigureAwait(false);

return result.HasValue ? result.Value : [];
}

return await ComputeDiagnosticsInProcessAsync(
document, range, allAnalyzers, syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers,
incrementalAnalysis, logPerformanceInfo, cancellationToken).ConfigureAwait(false);
}
}
Loading
Loading