Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -11,6 +11,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics.Telemetry;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics;

Expand Down Expand Up @@ -41,8 +42,8 @@ public static void AppendAnalyzerMap(this Dictionary<string, DiagnosticAnalyzer>
}
}

public static IEnumerable<AnalyzerPerformanceInfo> ToAnalyzerPerformanceInfo(this IDictionary<DiagnosticAnalyzer, AnalyzerTelemetryInfo> analysisResult, DiagnosticAnalyzerInfoCache analyzerInfo)
=> analysisResult.Select(kv => new AnalyzerPerformanceInfo(kv.Key.GetAnalyzerId(), analyzerInfo.IsTelemetryCollectionAllowed(kv.Key), kv.Value.ExecutionTime));
public static ImmutableArray<AnalyzerPerformanceInfo> ToAnalyzerPerformanceInfo(this IDictionary<DiagnosticAnalyzer, AnalyzerTelemetryInfo> analysisResult, DiagnosticAnalyzerInfoCache analyzerInfo)
=> analysisResult.SelectAsArray(kv => new AnalyzerPerformanceInfo(kv.Key.GetAnalyzerId(), analyzerInfo.IsTelemetryCollectionAllowed(kv.Key), kv.Value.ExecutionTime));

public static Task<ImmutableArray<DiagnosticDescriptor>> GetDiagnosticDescriptorsAsync(
this Project project,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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.Collections.Generic;
using System.Collections.Immutable;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics;

internal sealed partial class DiagnosticAnalyzerService
{
private sealed class ChecksumAndAnalyzersEqualityComparer
: IEqualityComparer<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers)>
{
public static readonly ChecksumAndAnalyzersEqualityComparer Instance = new();

public bool Equals((Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) x, (Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) y)
{
if (x.checksum != y.checksum)
return false;

// Fast path for when the analyzers are the same reference.
return x.analyzers == y.analyzers || x.analyzers.SetEquals(y.analyzers);
}

public int GetHashCode((Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) obj)
{
var hashCode = obj.checksum.GetHashCode();

// Use addition so that we're resilient to any order for the analyzers.
foreach (var analyzer in obj.analyzers)
hashCode += analyzer.GetHashCode();

return hashCode;
}
}
}
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis.Diagnostics;

internal sealed partial class DiagnosticAnalyzerService
{
private sealed class HostAnalyzerInfo
{
private const int BuiltInCompilerPriority = -2;
private const int RegularDiagnosticAnalyzerPriority = -1;

private readonly ImmutableHashSet<DiagnosticAnalyzer> _hostAnalyzers;
private readonly ImmutableHashSet<DiagnosticAnalyzer> _allAnalyzers;
public readonly ImmutableArray<DiagnosticAnalyzer> OrderedAllAnalyzers;

public HostAnalyzerInfo(
ImmutableHashSet<DiagnosticAnalyzer> hostAnalyzers,
ImmutableHashSet<DiagnosticAnalyzer> allAnalyzers)
{
_hostAnalyzers = hostAnalyzers;
_allAnalyzers = allAnalyzers;

// order analyzers.
// order will be in this order
// BuiltIn Compiler Analyzer (C#/VB) < Regular DiagnosticAnalyzers < Document/ProjectDiagnosticAnalyzers
OrderedAllAnalyzers = [.. _allAnalyzers.OrderBy(PriorityComparison)];
}

public bool IsHostAnalyzer(DiagnosticAnalyzer analyzer)
=> _hostAnalyzers.Contains(analyzer);

public HostAnalyzerInfo WithExcludedAnalyzers(ImmutableHashSet<DiagnosticAnalyzer> excludedAnalyzers)
{
if (excludedAnalyzers.IsEmpty)
{
return this;
}

return new(_hostAnalyzers, _allAnalyzers.Except(excludedAnalyzers));
}

private int PriorityComparison(DiagnosticAnalyzer state1, DiagnosticAnalyzer state2)
=> GetPriority(state1) - GetPriority(state2);

private static int GetPriority(DiagnosticAnalyzer state)
{
// compiler gets highest priority
if (state.IsCompilerAnalyzer())
{
return BuiltInCompilerPriority;
}

return state switch
{
DocumentDiagnosticAnalyzer analyzer => analyzer.Priority,
_ => RegularDiagnosticAnalyzerPriority,
};
}
}

/// <summary>
/// Return <see cref="DiagnosticAnalyzer"/>s for the given <see cref="Project"/>.
/// </summary>
internal ImmutableArray<DiagnosticAnalyzer> GetProjectAnalyzers(Project project)
{
var hostAnalyzerInfo = GetOrCreateHostAnalyzerInfo(project);
var projectAnalyzerInfo = GetOrCreateProjectAnalyzerInfo(project);
return hostAnalyzerInfo.OrderedAllAnalyzers.AddRange(projectAnalyzerInfo.Analyzers);
}

private HostAnalyzerInfo GetOrCreateHostAnalyzerInfo(Project project)
{
var projectAnalyzerInfo = GetOrCreateProjectAnalyzerInfo(project);

var solution = project.Solution;
var key = new HostAnalyzerInfoKey(project.Language, project.State.HasSdkCodeStyleAnalyzers, solution.SolutionState.Analyzers.HostAnalyzerReferences);
// Some Host Analyzers may need to be treated as Project Analyzers so that they do not have access to the
// Host fallback options. These ids will be used when building up the Host and Project analyzer collections.
var referenceIdsToRedirect = GetReferenceIdsToRedirectAsProjectAnalyzers(project);
var hostAnalyzerInfo = ImmutableInterlocked.GetOrAdd(ref _hostAnalyzerStateMap, key, CreateLanguageSpecificAnalyzerMap, (solution.SolutionState.Analyzers, referenceIdsToRedirect));
return hostAnalyzerInfo.WithExcludedAnalyzers(projectAnalyzerInfo.SkippedAnalyzersInfo.SkippedAnalyzers);

static HostAnalyzerInfo CreateLanguageSpecificAnalyzerMap(HostAnalyzerInfoKey arg, (HostDiagnosticAnalyzers HostAnalyzers, ImmutableHashSet<object> ReferenceIdsToRedirect) state)
{
var language = arg.Language;
var analyzersPerReference = state.HostAnalyzers.GetOrCreateHostDiagnosticAnalyzersPerReference(language);

var (hostAnalyzerCollection, projectAnalyzerCollection) = GetAnalyzerCollections(analyzersPerReference, state.ReferenceIdsToRedirect);
var (hostAnalyzers, allAnalyzers) = PartitionAnalyzers(projectAnalyzerCollection, hostAnalyzerCollection, includeWorkspacePlaceholderAnalyzers: true);

return new HostAnalyzerInfo(hostAnalyzers, allAnalyzers);
}

static (ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> HostAnalyzerCollection, ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> ProjectAnalyzerCollection) GetAnalyzerCollections(
ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>> analyzersPerReference,
ImmutableHashSet<object> referenceIdsToRedirectAsProjectAnalyzers)
{
if (referenceIdsToRedirectAsProjectAnalyzers.IsEmpty)
{
return ([.. analyzersPerReference.Values], []);
}

using var _1 = ArrayBuilder<ImmutableArray<DiagnosticAnalyzer>>.GetInstance(out var hostAnalyzerCollection);
using var _2 = ArrayBuilder<ImmutableArray<DiagnosticAnalyzer>>.GetInstance(out var projectAnalyzerCollection);

foreach (var (referenceId, analyzers) in analyzersPerReference)
{
if (referenceIdsToRedirectAsProjectAnalyzers.Contains(referenceId))
{
projectAnalyzerCollection.Add(analyzers);
}
else
{
hostAnalyzerCollection.Add(analyzers);
}
}

return (hostAnalyzerCollection.ToImmutableAndClear(), projectAnalyzerCollection.ToImmutableAndClear());
}
}

private static (ImmutableHashSet<DiagnosticAnalyzer> hostAnalyzers, ImmutableHashSet<DiagnosticAnalyzer> allAnalyzers) PartitionAnalyzers(
ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> projectAnalyzerCollection,
ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> hostAnalyzerCollection,
bool includeWorkspacePlaceholderAnalyzers)
{
using var _1 = PooledHashSet<DiagnosticAnalyzer>.GetInstance(out var hostAnalyzers);
using var _2 = PooledHashSet<DiagnosticAnalyzer>.GetInstance(out var allAnalyzers);

if (includeWorkspacePlaceholderAnalyzers)
{
hostAnalyzers.Add(FileContentLoadAnalyzer.Instance);
hostAnalyzers.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
allAnalyzers.Add(FileContentLoadAnalyzer.Instance);
allAnalyzers.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
}

foreach (var analyzers in projectAnalyzerCollection)
{
foreach (var analyzer in analyzers)
{
Debug.Assert(analyzer != FileContentLoadAnalyzer.Instance && analyzer != GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
allAnalyzers.Add(analyzer);
}
}

foreach (var analyzers in hostAnalyzerCollection)
{
foreach (var analyzer in analyzers)
{
Debug.Assert(analyzer != FileContentLoadAnalyzer.Instance && analyzer != GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
allAnalyzers.Add(analyzer);
hostAnalyzers.Add(analyzer);
}
}

return (hostAnalyzers.ToImmutableHashSet(), allAnalyzers.ToImmutableHashSet());
}

private static ImmutableHashSet<object> GetReferenceIdsToRedirectAsProjectAnalyzers(Project project)
{
if (project.State.HasSdkCodeStyleAnalyzers)
{
// When a project uses CodeStyle analyzers added by the SDK, we remove them in favor of the
// Features analyzers. We need to then treat the Features analyzers as Project analyzers so
// they do not get access to the Host fallback options.
return GetFeaturesAnalyzerReferenceIds(project.Solution.SolutionState.Analyzers);
}

return [];

static ImmutableHashSet<object> GetFeaturesAnalyzerReferenceIds(HostDiagnosticAnalyzers hostAnalyzers)
{
var builder = ImmutableHashSet.CreateBuilder<object>();

foreach (var analyzerReference in hostAnalyzers.HostAnalyzerReferences)
{
if (analyzerReference.IsFeaturesAnalyzer())
builder.Add(analyzerReference.Id);
}

return builder.ToImmutable();
}
}

private readonly record struct HostAnalyzerInfoKey(
string Language, bool HasSdkCodeStyleAnalyzers, IReadOnlyList<AnalyzerReference> AnalyzerReferences);
}
Loading
Loading