diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs index c4f33d130bbb8..906a399c06bd4 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs @@ -656,21 +656,27 @@ await TestNuGetAndVsixAnalyzerCoreAsync( // 1) No duplicate diagnostics // 2) Both NuGet and Vsix analyzers execute // 3) Appropriate diagnostic filtering is done - Nuget suppressor suppresses VSIX analyzer. + // + // 🐛 After splitting fallback options into separate CompilationWithAnalyzers for project and host analyzers, + // NuGet-installed suppressors no longer act on VSIX-installed analyzer diagnostics. Fixing this requires us to + // add NuGet-installed analyzer references to the host CompilationWithAnalyzers, with an additional flag + // indicating that only suppressors should run for these references. + const bool FalseButShouldBeTrue = false; await TestNuGetAndVsixAnalyzerCoreAsync( nugetAnalyzers: ImmutableArray.Create(firstNugetAnalyzer), expectedNugetAnalyzersExecuted: true, vsixAnalyzers: ImmutableArray.Create(vsixAnalyzer), expectedVsixAnalyzersExecuted: true, nugetSuppressors: ImmutableArray.Create(nugetSuppressor), - expectedNugetSuppressorsExecuted: true, + expectedNugetSuppressorsExecuted: FalseButShouldBeTrue, vsixSuppressors: ImmutableArray.Empty, expectedVsixSuppressorsExecuted: false, new[] { (Diagnostic("A", "Class").WithLocation(1, 7), nameof(NuGetAnalyzer)), - (Diagnostic("X", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)), - (Diagnostic("Y", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)), - (Diagnostic("Z", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)) + (Diagnostic("X", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)), + (Diagnostic("Y", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)), + (Diagnostic("Z", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)) }); // Suppressors with duplicate support for VsixAnalyzer, but not 100% overlap. Verify the following: @@ -684,15 +690,15 @@ await TestNuGetAndVsixAnalyzerCoreAsync( vsixAnalyzers: ImmutableArray.Create(vsixAnalyzer), expectedVsixAnalyzersExecuted: true, nugetSuppressors: ImmutableArray.Create(partialNugetSuppressor), - expectedNugetSuppressorsExecuted: true, + expectedNugetSuppressorsExecuted: FalseButShouldBeTrue, vsixSuppressors: ImmutableArray.Create(vsixSuppressor), expectedVsixSuppressorsExecuted: false, new[] { (Diagnostic("A", "Class").WithLocation(1, 7), nameof(NuGetAnalyzer)), (Diagnostic("X", "Class", isSuppressed: false).WithLocation(1, 7), nameof(VsixAnalyzer)), - (Diagnostic("Y", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)), - (Diagnostic("Z", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)) + (Diagnostic("Y", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)), + (Diagnostic("Z", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)) }); // Suppressors with duplicate support for VsixAnalyzer, with 100% overlap. Verify the following: @@ -706,15 +712,15 @@ await TestNuGetAndVsixAnalyzerCoreAsync( vsixAnalyzers: ImmutableArray.Create(vsixAnalyzer), expectedVsixAnalyzersExecuted: true, nugetSuppressors: ImmutableArray.Create(nugetSuppressor), - expectedNugetSuppressorsExecuted: true, + expectedNugetSuppressorsExecuted: FalseButShouldBeTrue, vsixSuppressors: ImmutableArray.Create(vsixSuppressor), expectedVsixSuppressorsExecuted: false, new[] { (Diagnostic("A", "Class").WithLocation(1, 7), nameof(NuGetAnalyzer)), - (Diagnostic("X", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)), - (Diagnostic("Y", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)), - (Diagnostic("Z", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)) + (Diagnostic("X", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)), + (Diagnostic("Y", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)), + (Diagnostic("Z", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)) }); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs index bdb2cc56ec448..0b59e42e310ed 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs @@ -112,10 +112,10 @@ private sealed class CombinedAnalyzerConfigOptions(AnalyzerConfigData fileDirect public override NamingStylePreferences GetNamingStylePreferences() { - var preferences = _fileDirectoryConfigData.ConfigOptions.GetNamingStylePreferences(); + var preferences = _fileDirectoryConfigData.ConfigOptionsWithoutFallback.GetNamingStylePreferences(); if (preferences.IsEmpty && _projectDirectoryConfigData.HasValue) { - preferences = _projectDirectoryConfigData.Value.ConfigOptions.GetNamingStylePreferences(); + preferences = _projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.GetNamingStylePreferences(); } return preferences; @@ -123,7 +123,7 @@ public override NamingStylePreferences GetNamingStylePreferences() public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) { - if (_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out value)) + if (_fileDirectoryConfigData.ConfigOptionsWithoutFallback.TryGetValue(key, out value)) { return true; } @@ -134,7 +134,7 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val return false; } - if (_projectDirectoryConfigData.Value.ConfigOptions.TryGetValue(key, out value)) + if (_projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.TryGetValue(key, out value)) { return true; } @@ -156,23 +156,23 @@ public override IEnumerable Keys { get { - foreach (var key in _fileDirectoryConfigData.ConfigOptions.Keys) + foreach (var key in _fileDirectoryConfigData.ConfigOptionsWithoutFallback.Keys) yield return key; if (!_projectDirectoryConfigData.HasValue) yield break; - foreach (var key in _projectDirectoryConfigData.Value.ConfigOptions.Keys) + foreach (var key in _projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.Keys) { - if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out _)) + if (!_fileDirectoryConfigData.ConfigOptionsWithoutFallback.TryGetValue(key, out _)) yield return key; } foreach (var (key, severity) in _projectDirectoryConfigData.Value.TreeOptions) { var diagnosticKey = "dotnet_diagnostic." + key + ".severity"; - if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(diagnosticKey, out _) && - !_projectDirectoryConfigData.Value.ConfigOptions.TryGetValue(diagnosticKey, out _)) + if (!_fileDirectoryConfigData.ConfigOptionsWithoutFallback.TryGetValue(diagnosticKey, out _) && + !_projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.TryGetValue(diagnosticKey, out _)) { yield return diagnosticKey; } diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs index 2d960247d8df2..6f787d5d6f1a0 100644 --- a/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs @@ -697,13 +697,13 @@ internal async Task TestOnlyRequiredAnalyzerExecutedDuringDiagnosticComputation( var analyzer1 = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false); var analyzer1Id = analyzer1.GetAnalyzerId(); var analyzer2 = new NamedTypeAnalyzer(); - var analyzerIdsToRequestDiagnostics = new[] { analyzer1Id }; + var analyzerIdsToRequestDiagnostics = ImmutableArray.Create(analyzer1Id); var analyzerReference = new AnalyzerImageReference(ImmutableArray.Create(analyzer1, analyzer2)); workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([analyzerReference])); var project = workspace.CurrentSolution.Projects.Single(); var document = documentAnalysis ? project.Documents.Single() : null; var diagnosticsMapResults = await DiagnosticComputer.GetDiagnosticsAsync( - document, project, Checksum.Null, span: null, analyzerIdsToRequestDiagnostics, + document, project, Checksum.Null, span: null, projectAnalyzerIds: [], analyzerIdsToRequestDiagnostics, AnalysisKind.Semantic, new DiagnosticAnalyzerInfoCache(), workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); @@ -742,7 +742,7 @@ void M() var analyzer = new FilterSpanTestAnalyzer(kind); var analyzerId = analyzer.GetAnalyzerId(); - var analyzerIdsToRequestDiagnostics = new[] { analyzerId }; + var analyzerIdsToRequestDiagnostics = ImmutableArray.Create(analyzerId); var analyzerReference = new AnalyzerImageReference(ImmutableArray.Create(analyzer)); project = project.AddAnalyzerReference(analyzerReference); @@ -771,7 +771,7 @@ async Task VerifyCallbackSpanAsync(TextSpan? filterSpan) : AnalysisKind.Semantic; var documentToAnalyze = kind == FilterSpanTestAnalyzer.AnalysisKind.AdditionalFile ? additionalDocument : document; _ = await DiagnosticComputer.GetDiagnosticsAsync( - documentToAnalyze, project, Checksum.Null, filterSpan, analyzerIdsToRequestDiagnostics, + documentToAnalyze, project, Checksum.Null, filterSpan, analyzerIdsToRequestDiagnostics, hostAnalyzerIds: [], analysisKind, new DiagnosticAnalyzerInfoCache(), workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false, logPerformanceInfo: false, getTelemetryInfo: false, CancellationToken.None); @@ -820,14 +820,14 @@ void M() var diagnosticAnalyzerInfoCache = new DiagnosticAnalyzerInfoCache(); var kind = actionKind == AnalyzerRegisterActionKind.SyntaxTree ? AnalysisKind.Syntax : AnalysisKind.Semantic; - var analyzerIds = new[] { analyzer.GetAnalyzerId() }; + var analyzerIds = ImmutableArray.Create(analyzer.GetAnalyzerId()); // First invoke analysis with cancellation token, and verify canceled compilation and no reported diagnostics. Assert.Empty(analyzer.CanceledCompilations); try { _ = await DiagnosticComputer.GetDiagnosticsAsync(document, project, Checksum.Null, span: null, - analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false, + projectAnalyzerIds: [], analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: analyzer.CancellationToken); throw ExceptionUtilities.Unreachable(); @@ -840,7 +840,7 @@ void M() // Then invoke analysis without cancellation token, and verify non-cancelled diagnostic. var diagnosticsMap = await DiagnosticComputer.GetDiagnosticsAsync(document, project, Checksum.Null, span: null, - analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false, + projectAnalyzerIds: [], analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); var builder = diagnosticsMap.Diagnostics.Single().diagnosticMap; var diagnostic = kind == AnalysisKind.Syntax ? builder.Syntax.Single().Item2.Single() : builder.Semantic.Single().Item2.Single(); diff --git a/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs b/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs index fb4d389191d00..7cd038975c260 100644 --- a/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs +++ b/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs @@ -23,5 +23,7 @@ internal sealed class CSharpSyncNamespacesService( { public override AbstractMatchFolderAndNamespaceDiagnosticAnalyzer DiagnosticAnalyzer { get; } = diagnosticAnalyzer; + public override bool IsHostAnalyzer => false; + public override AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; } = codeFixProvider; } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs index 1e59c73a98e08..e1d85cc8844a9 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; @@ -51,7 +52,7 @@ private static VersionStamp GetAnalyzerVersion(string path) public static string GetAnalyzerAssemblyName(this DiagnosticAnalyzer analyzer) => analyzer.GetType().Assembly.GetName().Name ?? throw ExceptionUtilities.Unreachable(); - public static void AppendAnalyzerMap(this Dictionary analyzerMap, IEnumerable analyzers) + public static void AppendAnalyzerMap(this Dictionary analyzerMap, ImmutableArray analyzers) { foreach (var analyzer in analyzers) { diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs index 5907c6c6ed020..c826e93237581 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs @@ -2,6 +2,7 @@ // 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.Immutable; using System.Diagnostics; using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Text; @@ -66,7 +67,13 @@ internal class DiagnosticArguments /// Array of analyzer IDs for analyzers that need to be executed for computing diagnostics. /// [DataMember(Order = 7)] - public string[] AnalyzerIds; + public ImmutableArray ProjectAnalyzerIds; + + /// + /// Array of analyzer IDs for analyzers that need to be executed for computing diagnostics. + /// + [DataMember(Order = 8)] + public ImmutableArray HostAnalyzerIds; /// /// Indicates diagnostic computation for an explicit user-invoked request, @@ -83,14 +90,15 @@ public DiagnosticArguments( TextSpan? documentSpan, AnalysisKind? documentAnalysisKind, ProjectId projectId, - string[] analyzerIds, + ImmutableArray projectAnalyzerIds, + ImmutableArray hostAnalyzerIds, bool isExplicit) { Debug.Assert(documentId != null || documentSpan == null); Debug.Assert(documentId != null || documentAnalysisKind == null); Debug.Assert(documentAnalysisKind is null or (AnalysisKind?)AnalysisKind.Syntax or (AnalysisKind?)AnalysisKind.Semantic); - Debug.Assert(analyzerIds.Length > 0); + Debug.Assert(projectAnalyzerIds.Length > 0 || hostAnalyzerIds.Length > 0); ReportSuppressedDiagnostics = reportSuppressedDiagnostics; LogPerformanceInfo = logPerformanceInfo; @@ -99,7 +107,8 @@ public DiagnosticArguments( DocumentSpan = documentSpan; DocumentAnalysisKind = documentAnalysisKind; ProjectId = projectId; - AnalyzerIds = analyzerIds; + ProjectAnalyzerIds = projectAnalyzerIds; + HostAnalyzerIds = hostAnalyzerIds; IsExplicit = isExplicit; } } diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs index 52f4f042a8e83..4b94601b28b67 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs @@ -439,6 +439,7 @@ private async Task EnqueueProjectConfigurationChangeWorkItemAsync(ProjectChanges !object.Equals(oldProject.AssemblyName, newProject.AssemblyName) || !object.Equals(oldProject.Name, newProject.Name) || !object.Equals(oldProject.AnalyzerOptions, newProject.AnalyzerOptions) || + !object.Equals(oldProject.HostAnalyzerOptions, newProject.HostAnalyzerOptions) || !object.Equals(oldProject.DefaultNamespace, newProject.DefaultNamespace) || !object.Equals(oldProject.OutputFilePath, newProject.OutputFilePath) || !object.Equals(oldProject.OutputRefFilePath, newProject.OutputRefFilePath) || diff --git a/src/Features/Core/Portable/SyncNamespaces/AbstractSyncNamespacesService.cs b/src/Features/Core/Portable/SyncNamespaces/AbstractSyncNamespacesService.cs index 5e5d63aaa56a4..d1b89c768d7b1 100644 --- a/src/Features/Core/Portable/SyncNamespaces/AbstractSyncNamespacesService.cs +++ b/src/Features/Core/Portable/SyncNamespaces/AbstractSyncNamespacesService.cs @@ -25,6 +25,7 @@ internal abstract class AbstractSyncNamespacesService DiagnosticAnalyzer { get; } + public abstract bool IsHostAnalyzer { get; } public abstract AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; } /// @@ -38,7 +39,7 @@ public async Task SyncNamespacesAsync( var solution = projects[0].Solution; var diagnosticAnalyzers = ImmutableArray.Create(DiagnosticAnalyzer); - var diagnosticsByProject = await GetDiagnosticsByProjectAsync(projects, diagnosticAnalyzers, cancellationToken).ConfigureAwait(false); + var diagnosticsByProject = await GetDiagnosticsByProjectAsync(projects, diagnosticAnalyzers, IsHostAnalyzer, cancellationToken).ConfigureAwait(false); // If no diagnostics are reported, then there is nothing to fix. if (diagnosticsByProject.Values.All(diagnostics => diagnostics.IsEmpty)) @@ -57,13 +58,14 @@ public async Task SyncNamespacesAsync( private static async Task>> GetDiagnosticsByProjectAsync( ImmutableArray projects, ImmutableArray diagnosticAnalyzers, + bool isHostAnalyzer, CancellationToken cancellationToken) { var builder = ImmutableDictionary.CreateBuilder>(); foreach (var project in projects) { - var diagnostics = await GetDiagnosticsAsync(project, diagnosticAnalyzers, cancellationToken).ConfigureAwait(false); + var diagnostics = await GetDiagnosticsAsync(project, diagnosticAnalyzers, isHostAnalyzer, cancellationToken).ConfigureAwait(false); builder.Add(project, diagnostics); } @@ -73,13 +75,14 @@ private static async Task> GetDiagnosticsAsync( Project project, ImmutableArray diagnosticAnalyzers, + bool isHostAnalyzer, CancellationToken cancellationToken) { var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); RoslynDebug.AssertNotNull(compilation); var analyzerOptions = new CompilationWithAnalyzersOptions( - project.AnalyzerOptions, + isHostAnalyzer ? project.HostAnalyzerOptions : project.AnalyzerOptions, onAnalyzerException: null, concurrentAnalysis: true, logAnalyzerExecutionTime: false, diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor.cs b/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor.cs index d5de93548ef16..2c6a45b00c25d 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor.cs @@ -25,20 +25,21 @@ namespace Microsoft.CodeAnalysis.Diagnostics /// internal sealed partial class DocumentAnalysisExecutor { - private readonly CompilationWithAnalyzers? _compilationWithAnalyzers; + private readonly CompilationWithAnalyzersPair? _compilationWithAnalyzers; private readonly InProcOrRemoteHostAnalyzerRunner _diagnosticAnalyzerRunner; private readonly bool _isExplicit; private readonly bool _logPerformanceInfo; private readonly Action? _onAnalysisException; - private readonly ImmutableArray _compilationBasedAnalyzersInAnalysisScope; + private readonly ImmutableArray _compilationBasedProjectAnalyzersInAnalysisScope; + private readonly ImmutableArray _compilationBasedHostAnalyzersInAnalysisScope; private ImmutableDictionary? _lazySyntaxDiagnostics; private ImmutableDictionary? _lazySemanticDiagnostics; public DocumentAnalysisExecutor( DocumentAnalysisScope analysisScope, - CompilationWithAnalyzers? compilationWithAnalyzers, + CompilationWithAnalyzersPair? compilationWithAnalyzers, InProcOrRemoteHostAnalyzerRunner diagnosticAnalyzerRunner, bool isExplicit, bool logPerformanceInfo, @@ -51,9 +52,14 @@ public DocumentAnalysisExecutor( _logPerformanceInfo = logPerformanceInfo; _onAnalysisException = onAnalysisException; - var compilationBasedAnalyzers = compilationWithAnalyzers?.Analyzers.ToImmutableHashSet(); - _compilationBasedAnalyzersInAnalysisScope = compilationBasedAnalyzers != null - ? analysisScope.Analyzers.WhereAsArray(compilationBasedAnalyzers.Contains) + var compilationBasedProjectAnalyzers = compilationWithAnalyzers?.ProjectAnalyzers.ToImmutableHashSet(); + _compilationBasedProjectAnalyzersInAnalysisScope = compilationBasedProjectAnalyzers != null + ? analysisScope.ProjectAnalyzers.WhereAsArray(compilationBasedProjectAnalyzers.Contains) + : []; + + var compilationBasedHostAnalyzers = compilationWithAnalyzers?.HostAnalyzers.ToImmutableHashSet(); + _compilationBasedHostAnalyzersInAnalysisScope = compilationBasedHostAnalyzers != null + ? analysisScope.HostAnalyzers.WhereAsArray(compilationBasedHostAnalyzers.Contains) : []; } @@ -67,7 +73,7 @@ public DocumentAnalysisExecutor With(DocumentAnalysisScope analysisScope) /// public async Task> ComputeDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(AnalysisScope.Analyzers.Contains(analyzer)); + Contract.ThrowIfFalse(AnalysisScope.ProjectAnalyzers.Contains(analyzer) || AnalysisScope.HostAnalyzers.Contains(analyzer)); var textDocument = AnalysisScope.TextDocument; var span = AnalysisScope.Span; @@ -107,8 +113,9 @@ public async Task> ComputeDiagnosticsAsync(Diagnosti if (document == null) return []; + // DocumentDiagnosticAnalyzer is a host-only analyzer var documentDiagnostics = await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync( - documentAnalyzer, document, kind, _compilationWithAnalyzers?.Compilation, cancellationToken).ConfigureAwait(false); + documentAnalyzer, document, kind, _compilationWithAnalyzers?.HostCompilation, cancellationToken).ConfigureAwait(false); return ConvertToLocalDiagnostics(documentDiagnostics, document, span); } @@ -163,7 +170,9 @@ public async Task> ComputeDiagnosticsAsync(Diagnosti #if DEBUG var diags = await diagnostics.ToDiagnosticsAsync(textDocument.Project, cancellationToken).ConfigureAwait(false); - Debug.Assert(diags.Length == CompilationWithAnalyzers.GetEffectiveDiagnostics(diags, _compilationWithAnalyzers.Compilation).Count()); + var compilation = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) ? _compilationWithAnalyzers.ProjectCompilation : _compilationWithAnalyzers.HostCompilation; + RoslynDebug.AssertNotNull(compilation); + Debug.Assert(diags.Length == CompilationWithAnalyzers.GetEffectiveDiagnostics(diags, compilation).Count()); Debug.Assert(diagnostics.Length == ConvertToLocalDiagnostics(diags, textDocument, span).Count()); #endif @@ -204,10 +213,12 @@ private async Task> GetCompilerAnalyzerDiagnostic { RoslynDebug.Assert(analyzer.IsCompilerAnalyzer()); RoslynDebug.Assert(_compilationWithAnalyzers != null); - RoslynDebug.Assert(_compilationBasedAnalyzersInAnalysisScope.Contains(analyzer)); + RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); RoslynDebug.Assert(AnalysisScope.TextDocument is Document); - var analysisScope = AnalysisScope.WithAnalyzers([analyzer]).WithSpan(span); + var analysisScope = _compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) + ? AnalysisScope.WithAnalyzers([analyzer], []).WithSpan(span) + : AnalysisScope.WithAnalyzers([], [analyzer]).WithSpan(span); var analysisResult = await GetAnalysisResultAsync(analysisScope, cancellationToken).ConfigureAwait(false); if (!analysisResult.TryGetValue(analyzer, out var result)) { @@ -226,7 +237,7 @@ private async Task> GetSyntaxDiagnosticsAsync(Dia // for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing. RoslynDebug.Assert(_compilationWithAnalyzers != null); - RoslynDebug.Assert(_compilationBasedAnalyzersInAnalysisScope.Contains(analyzer)); + RoslynDebug.Assert(_compilationBasedProjectAnalyzersInAnalysisScope.Contains(analyzer) || _compilationBasedHostAnalyzersInAnalysisScope.Contains(analyzer)); if (isCompilerAnalyzer) { @@ -242,7 +253,7 @@ private async Task> GetSyntaxDiagnosticsAsync(Dia { using var _ = TelemetryLogging.LogBlockTimeAggregated(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSyntaxDiagnosticsAsync)}.{nameof(GetAnalysisResultAsync)}"); - var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedAnalyzersInAnalysisScope); + var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); var syntaxDiagnostics = await GetAnalysisResultAsync(analysisScope, cancellationToken).ConfigureAwait(false); Interlocked.CompareExchange(ref _lazySyntaxDiagnostics, syntaxDiagnostics, null); } @@ -278,7 +289,7 @@ private async Task> GetSemanticDiagnosticsAsync(D { using var _ = TelemetryLogging.LogBlockTimeAggregated(FunctionId.RequestDiagnostics_Summary, $"{nameof(GetSemanticDiagnosticsAsync)}.{nameof(GetAnalysisResultAsync)}"); - var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedAnalyzersInAnalysisScope); + var analysisScope = AnalysisScope.WithAnalyzers(_compilationBasedProjectAnalyzersInAnalysisScope, _compilationBasedHostAnalyzersInAnalysisScope); var semanticDiagnostics = await GetAnalysisResultAsync(analysisScope, cancellationToken).ConfigureAwait(false); Interlocked.CompareExchange(ref _lazySemanticDiagnostics, semanticDiagnostics, null); } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs b/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs index 7a842ed33eca6..12b8d9ceac862 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/DocumentAnalysisExecutor_Helpers.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics.EngineV2; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.SolutionCrawler; @@ -127,9 +128,10 @@ static string GetLanguageSpecificId(string? language, string noLanguageId, strin language: language); } - public static async Task CreateCompilationWithAnalyzersAsync( + public static async Task CreateCompilationWithAnalyzersAsync( Project project, - ImmutableArray analyzers, + ImmutableArray projectAnalyzers, + ImmutableArray hostAnalyzers, bool includeSuppressedDiagnostics, bool crashOnAnalyzerException, CancellationToken cancellationToken) @@ -142,11 +144,12 @@ static string GetLanguageSpecificId(string? language, string noLanguageId, strin } // Create driver that holds onto compilation and associated analyzers - var filteredAnalyzers = analyzers.WhereAsArray(a => !a.IsWorkspaceDiagnosticAnalyzer()); + var filteredProjectAnalyzers = projectAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()); + var filteredHostAnalyzers = hostAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()); // PERF: there is no analyzers for this compilation. // compilationWithAnalyzer will throw if it is created with no analyzers which is perf optimization. - if (filteredAnalyzers.IsEmpty) + if (filteredProjectAnalyzers.IsEmpty && filteredHostAnalyzers.IsEmpty) { return null; } @@ -156,16 +159,25 @@ static string GetLanguageSpecificId(string? language, string noLanguageId, strin // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to // async being used with synchronous blocking concurrency. - var analyzerOptions = new CompilationWithAnalyzersOptions( + var projectAnalyzerOptions = new CompilationWithAnalyzersOptions( options: project.AnalyzerOptions, onAnalyzerException: null, analyzerExceptionFilter: GetAnalyzerExceptionFilter(), concurrentAnalysis: false, logAnalyzerExecutionTime: true, reportSuppressedDiagnostics: includeSuppressedDiagnostics); + var hostAnalyzerOptions = new CompilationWithAnalyzersOptions( + options: project.HostAnalyzerOptions, + onAnalyzerException: null, + analyzerExceptionFilter: GetAnalyzerExceptionFilter(), + concurrentAnalysis: false, + logAnalyzerExecutionTime: true, + reportSuppressedDiagnostics: includeSuppressedDiagnostics); // Create driver that holds onto compilation and associated analyzers - return compilation.WithAnalyzers(filteredAnalyzers, analyzerOptions); + return new CompilationWithAnalyzersPair( + filteredProjectAnalyzers.Any() ? compilation.WithAnalyzers(filteredProjectAnalyzers, projectAnalyzerOptions) : null, + filteredHostAnalyzers.Any() ? compilation.WithAnalyzers(filteredHostAnalyzers, hostAnalyzerOptions) : null); Func GetAnalyzerExceptionFilter() { diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs index 8f8068b4bf479..7ff7a1c34e532 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs @@ -2,16 +2,15 @@ // 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.Threading; using System.Threading.Tasks; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { internal partial class DiagnosticIncrementalAnalyzer { - private static Task CreateCompilationWithAnalyzersAsync(Project project, IEnumerable stateSets, bool includeSuppressedDiagnostics, bool crashOnAnalyzerException, CancellationToken cancellationToken) - => DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync(project, stateSets.SelectAsArray(s => s.Analyzer), includeSuppressedDiagnostics, crashOnAnalyzerException, cancellationToken); + private static Task CreateCompilationWithAnalyzersAsync(Project project, ImmutableArray stateSets, bool includeSuppressedDiagnostics, bool crashOnAnalyzerException, CancellationToken cancellationToken) + => DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync(project, stateSets.SelectAsArray(s => !s.IsHostAnalyzer, s => s.Analyzer), stateSets.SelectAsArray(s => s.IsHostAnalyzer, s => s.Analyzer), includeSuppressedDiagnostics, crashOnAnalyzerException, cancellationToken); } } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs index 2826a734a0209..dd0d6746da3da 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,7 @@ internal partial class DiagnosticIncrementalAnalyzer /// Return all diagnostics that belong to given project for the given StateSets (analyzers) either from cache or by calculating them /// private async Task GetProjectAnalysisDataAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray stateSets, CancellationToken cancellationToken) + CompilationWithAnalyzersPair? compilationWithAnalyzers, Project project, ImmutableArray stateSets, CancellationToken cancellationToken) { using (Logger.LogBlock(FunctionId.Diagnostics_ProjectDiagnostic, GetProjectLogMessage, project, stateSets, cancellationToken)) { @@ -88,14 +89,15 @@ private static async Task private async Task> ComputeDiagnosticsAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray ideAnalyzers, CancellationToken cancellationToken) + CompilationWithAnalyzersPair? compilationWithAnalyzers, Project project, ImmutableArray ideAnalyzers, CancellationToken cancellationToken) { try { var result = ImmutableDictionary.Empty; // can be null if given project doesn't support compilation. - if (compilationWithAnalyzers?.Analyzers.Length > 0) + if (compilationWithAnalyzers?.ProjectAnalyzers.Length > 0 + || compilationWithAnalyzers?.HostAnalyzers.Length > 0) { // calculate regular diagnostic analyzers diagnostics var resultMap = await _diagnosticAnalyzerRunner.AnalyzeProjectAsync( @@ -108,7 +110,8 @@ private async Task a is ProjectDiagnosticAnalyzer or DocumentDiagnosticAnalyzer)); + return await MergeProjectDiagnosticAnalyzerDiagnosticsAsync(project, ideAnalyzers, compilationWithAnalyzers?.HostCompilation, result, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { @@ -117,7 +120,7 @@ private async Task> ComputeDiagnosticsAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray stateSets, + CompilationWithAnalyzersPair? compilationWithAnalyzers, Project project, ImmutableArray stateSets, ImmutableDictionary existing, CancellationToken cancellationToken) { try @@ -129,18 +132,19 @@ private async Task s.Analyzer).Where(a => a is ProjectDiagnosticAnalyzer or DocumentDiagnosticAnalyzer).ToImmutableArrayOrEmpty(); - if (compilationWithAnalyzers != null && TryReduceAnalyzersToRun(compilationWithAnalyzers, version, existing, out var analyzersToRun)) + if (compilationWithAnalyzers != null && TryReduceAnalyzersToRun(compilationWithAnalyzers, version, existing, out var projectAnalyzersToRun, out var hostAnalyzersToRun)) { // it looks like we can reduce the set. create new CompilationWithAnalyzer. // if we reduced to 0, we just pass in null for analyzer drvier. it could be reduced to 0 // since we might have up to date results for analyzers from compiler but not for // workspace analyzers. - var compilationWithReducedAnalyzers = (analyzersToRun.Length == 0) ? null : + var compilationWithReducedAnalyzers = (projectAnalyzersToRun.Length == 0 && hostAnalyzersToRun.Length == 0) ? null : await DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync( project, - analyzersToRun, - compilationWithAnalyzers.AnalysisOptions.ReportSuppressedDiagnostics, + projectAnalyzersToRun, + hostAnalyzersToRun, + compilationWithAnalyzers.ReportSuppressedDiagnostics, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); @@ -180,35 +184,52 @@ private static ImmutableDictionary } private static bool TryReduceAnalyzersToRun( - CompilationWithAnalyzers compilationWithAnalyzers, VersionStamp version, + CompilationWithAnalyzersPair compilationWithAnalyzers, VersionStamp version, ImmutableDictionary existing, - out ImmutableArray analyzers) + out ImmutableArray projectAnalyzers, + out ImmutableArray hostAnalyzers) { - analyzers = default; + projectAnalyzers = compilationWithAnalyzers.ProjectAnalyzers.WhereAsArray( + static (analyzer, arg) => + { + if (arg.existing.TryGetValue(analyzer, out var analysisResult) && + analysisResult.Version == arg.version) + { + // we already have up to date result. + return false; + } - var existingAnalyzers = compilationWithAnalyzers.Analyzers; - var builder = ImmutableArray.CreateBuilder(); - foreach (var analyzer in existingAnalyzers) - { - if (existing.TryGetValue(analyzer, out var analysisResult) && - analysisResult.Version == version) + // analyzer that is out of date. + // open file only analyzer is always out of date for project wide data + return true; + }, + (existing, version)); + + hostAnalyzers = compilationWithAnalyzers.HostAnalyzers.WhereAsArray( + static (analyzer, arg) => { - // we already have up to date result. - continue; - } + if (arg.existing.TryGetValue(analyzer, out var analysisResult) && + analysisResult.Version == arg.version) + { + // we already have up to date result. + return false; + } - // analyzer that is out of date. - // open file only analyzer is always out of date for project wide data - builder.Add(analyzer); - } + // analyzer that is out of date. + // open file only analyzer is always out of date for project wide data + return true; + }, + (existing, version)); - // all of analyzers are out of date. - if (builder.Count == existingAnalyzers.Length) + if (projectAnalyzers.Length == compilationWithAnalyzers.ProjectAnalyzers.Length + && hostAnalyzers.Length == compilationWithAnalyzers.HostAnalyzers.Length) { + // all of analyzers are out of date. + projectAnalyzers = default; + hostAnalyzers = default; return false; } - analyzers = builder.ToImmutable(); return true; } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs index 96f122afbe980..9523e3dd6c3d8 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs @@ -39,7 +39,7 @@ public InProcOrRemoteHostAnalyzerRunner( public Task> AnalyzeDocumentAsync( DocumentAnalysisScope documentAnalysisScope, - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzersPair compilationWithAnalyzers, bool isExplicit, bool logPerformanceInfo, bool getTelemetryInfo, @@ -49,7 +49,7 @@ public Task> AnalyzeProjectAsync( Project project, - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzersPair compilationWithAnalyzers, bool logPerformanceInfo, bool getTelemetryInfo, CancellationToken cancellationToken) @@ -59,7 +59,7 @@ public Task> AnalyzeAsync( DocumentAnalysisScope? documentAnalysisScope, Project project, - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzersPair compilationWithAnalyzers, bool isExplicit, bool logPerformanceInfo, bool getTelemetryInfo, @@ -74,7 +74,7 @@ private async Task> AnalyzeCoreAsync() { - Contract.ThrowIfFalse(!compilationWithAnalyzers.Analyzers.IsEmpty); + Contract.ThrowIfFalse(compilationWithAnalyzers.HasAnalyzers); var remoteHostClient = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false); if (remoteHostClient != null) @@ -114,7 +114,7 @@ public async Task> GetSourceGeneratorDiagnosticsAsync private async Task> AnalyzeInProcAsync( DocumentAnalysisScope? documentAnalysisScope, Project project, - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzersPair compilationWithAnalyzers, RemoteHostClient? client, bool logPerformanceInfo, bool getTelemetryInfo, @@ -122,29 +122,55 @@ private async Task.Empty; + if (projectAnalysisResult is not null) + { + var map = await projectAnalysisResult.ToResultBuilderMapAsync( + additionalPragmaSuppressionDiagnostics, documentAnalysisScope, project, version, + compilationWithAnalyzers.ProjectCompilation!, projectAnalyzers, skippedAnalyzersInfo, + compilationWithAnalyzers.ReportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + builderMap = builderMap.AddRange(map); + } + + if (hostAnalysisResult is not null) + { + var map = await hostAnalysisResult.ToResultBuilderMapAsync( + additionalPragmaSuppressionDiagnostics, documentAnalysisScope, project, version, + compilationWithAnalyzers.HostCompilation!, hostAnalyzers, skippedAnalyzersInfo, + compilationWithAnalyzers.ReportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + builderMap = builderMap.AddRange(map); + } var result = builderMap.ToImmutableDictionary(kv => kv.Key, kv => DiagnosticAnalysisResult.CreateFromBuilder(kv.Value)); - var telemetry = getTelemetryInfo - ? analysisResult.AnalyzerTelemetryInfo - : ImmutableDictionary.Empty; + var telemetry = ImmutableDictionary.Empty; + if (getTelemetryInfo) + { + if (projectAnalysisResult is not null) + { + telemetry = telemetry.AddRange(projectAnalysisResult.AnalyzerTelemetryInfo); + } + + if (hostAnalysisResult is not null) + { + telemetry = telemetry.AddRange(hostAnalysisResult.AnalyzerTelemetryInfo); + } + } + return DiagnosticAnalysisResultMap.Create(result, telemetry); } @@ -152,7 +178,8 @@ private async Task FireAndForgetReportAnalyzerPerformanceAsync( DocumentAnalysisScope? documentAnalysisScope, Project project, RemoteHostClient? client, - AnalysisResult analysisResult, + AnalysisResult? projectAnalysisResult, + AnalysisResult? hostAnalysisResult, CancellationToken cancellationToken) { if (client == null) @@ -166,7 +193,16 @@ private async Task FireAndForgetReportAnalyzerPerformanceAsync( var count = documentAnalysisScope != null ? 1 : project.DocumentIds.Count + 1; var forSpanAnalysis = documentAnalysisScope?.Span.HasValue ?? false; - var performanceInfo = analysisResult.AnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(AnalyzerInfoCache).ToImmutableArray(); + ImmutableArray performanceInfo = []; + if (projectAnalysisResult is not null) + { + performanceInfo = performanceInfo.AddRange(projectAnalysisResult.AnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(AnalyzerInfoCache)); + } + + if (hostAnalysisResult is not null) + { + performanceInfo = performanceInfo.AddRange(hostAnalysisResult.AnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(AnalyzerInfoCache)); + } _ = await client.TryInvokeAsync( (service, cancellationToken) => service.ReportAnalyzerPerformanceAsync(performanceInfo, count, forSpanAnalysis, cancellationToken), @@ -181,34 +217,39 @@ private async Task FireAndForgetReportAnalyzerPerformanceAsync( private static async Task> AnalyzeOutOfProcAsync( DocumentAnalysisScope? documentAnalysisScope, Project project, - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzersPair compilationWithAnalyzers, RemoteHostClient client, bool isExplicit, bool logPerformanceInfo, bool getTelemetryInfo, CancellationToken cancellationToken) { - using var pooledObject = SharedPools.Default>().GetPooledObject(); - var analyzerMap = pooledObject.Object; + using var pooledObject1 = SharedPools.Default>().GetPooledObject(); + using var pooledObject2 = SharedPools.Default>().GetPooledObject(); + var projectAnalyzerMap = pooledObject1.Object; + var hostAnalyzerMap = pooledObject2.Object; - var analyzers = documentAnalysisScope?.Analyzers ?? compilationWithAnalyzers.Analyzers; + var projectAnalyzers = documentAnalysisScope?.ProjectAnalyzers ?? compilationWithAnalyzers.ProjectAnalyzers; + var hostAnalyzers = documentAnalysisScope?.HostAnalyzers ?? compilationWithAnalyzers.HostAnalyzers; - analyzerMap.AppendAnalyzerMap(analyzers); + projectAnalyzerMap.AppendAnalyzerMap(projectAnalyzers); + hostAnalyzerMap.AppendAnalyzerMap(hostAnalyzers); - if (analyzerMap.Count == 0) + if (projectAnalyzerMap.Count == 0 && hostAnalyzerMap.Count == 0) { return DiagnosticAnalysisResultMap.Empty; } var argument = new DiagnosticArguments( - compilationWithAnalyzers.AnalysisOptions.ReportSuppressedDiagnostics, + compilationWithAnalyzers.ReportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, documentAnalysisScope?.TextDocument.Id, documentAnalysisScope?.Span, documentAnalysisScope?.Kind, project.Id, - [.. analyzerMap.Keys], + [.. projectAnalyzerMap.Keys], + [.. hostAnalyzerMap.Keys], isExplicit); var result = await client.TryInvokeAsync( @@ -228,7 +269,7 @@ private static async Task( result.Value.Diagnostics.ToImmutableDictionary( - entry => analyzerMap[entry.analyzerId], + entry => IReadOnlyDictionaryExtensions.GetValueOrDefault(projectAnalyzerMap, entry.analyzerId) ?? hostAnalyzerMap[entry.analyzerId], entry => DiagnosticAnalysisResult.Create( project, version, @@ -237,7 +278,9 @@ private static async Task analyzerMap[entry.analyzerId], entry => entry.telemetry)); + result.Value.Telemetry.ToImmutableDictionary( + entry => IReadOnlyDictionaryExtensions.GetValueOrDefault(projectAnalyzerMap, entry.analyzerId) ?? hostAnalyzerMap[entry.analyzerId], + entry => entry.telemetry)); } // TODO: filter in OOP https://github.com/dotnet/roslyn/issues/47859 diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.IncrementalMemberEditAnalyzer.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.IncrementalMemberEditAnalyzer.cs index 98e0dc4c68628..de59475e77e05 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.IncrementalMemberEditAnalyzer.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.IncrementalMemberEditAnalyzer.cs @@ -94,7 +94,7 @@ public async Task CreateStateSetMap( string language, - IEnumerable> analyzerCollection, + IEnumerable> projectAnalyzerCollection, + IEnumerable> hostAnalyzerCollection, bool includeWorkspacePlaceholderAnalyzers) { var builder = ImmutableDictionary.CreateBuilder(); if (includeWorkspacePlaceholderAnalyzers) { - builder.Add(FileContentLoadAnalyzer.Instance, new StateSet(language, FileContentLoadAnalyzer.Instance)); - builder.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance, new StateSet(language, GeneratorDiagnosticsPlaceholderAnalyzer.Instance)); + builder.Add(FileContentLoadAnalyzer.Instance, new StateSet(language, FileContentLoadAnalyzer.Instance, isHostAnalyzer: true)); + builder.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance, new StateSet(language, GeneratorDiagnosticsPlaceholderAnalyzer.Instance, isHostAnalyzer: true)); } - foreach (var analyzers in analyzerCollection) + foreach (var analyzers in projectAnalyzerCollection) { foreach (var analyzer in analyzers) { @@ -158,7 +159,26 @@ private static ImmutableDictionary CreateStateSetM continue; } - builder.Add(analyzer, new StateSet(language, analyzer)); + builder.Add(analyzer, new StateSet(language, analyzer, isHostAnalyzer: false)); + } + } + + foreach (var analyzers in hostAnalyzerCollection) + { + foreach (var analyzer in analyzers) + { + Debug.Assert(analyzer != FileContentLoadAnalyzer.Instance && analyzer != GeneratorDiagnosticsPlaceholderAnalyzer.Instance); + + // TODO: + // #1, all de-duplication should move to DiagnosticAnalyzerInfoCache + // #2, not sure whether de-duplication of analyzer itself makes sense. this can only happen + // if user deliberately put same analyzer twice. + if (builder.ContainsKey(analyzer)) + { + continue; + } + + builder.Add(analyzer, new StateSet(language, analyzer, isHostAnalyzer: true)); } } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs index c7e696eb8d421..3a6afd3e5633a 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs @@ -23,14 +23,16 @@ private sealed class StateSet { public readonly string Language; public readonly DiagnosticAnalyzer Analyzer; + public readonly bool IsHostAnalyzer; private readonly ConcurrentDictionary _activeFileStates; private readonly ConcurrentDictionary _projectStates; - public StateSet(string language, DiagnosticAnalyzer analyzer) + public StateSet(string language, DiagnosticAnalyzer analyzer, bool isHostAnalyzer) { Language = language; Analyzer = analyzer; + IsHostAnalyzer = isHostAnalyzer; _activeFileStates = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 10); _projectStates = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 1); diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs index dd86515b9604f..df33398ea07c7 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.ProjectAndCompilationWithAnalyzers.cs @@ -9,9 +9,9 @@ internal partial class DiagnosticIncrementalAnalyzer private sealed class ProjectAndCompilationWithAnalyzers { public Project Project { get; } - public CompilationWithAnalyzers? CompilationWithAnalyzers { get; } + public CompilationWithAnalyzersPair? CompilationWithAnalyzers { get; } - public ProjectAndCompilationWithAnalyzers(Project project, CompilationWithAnalyzers? compilationWithAnalyzers) + public ProjectAndCompilationWithAnalyzers(Project project, CompilationWithAnalyzersPair? compilationWithAnalyzers) { Project = project; CompilationWithAnalyzers = compilationWithAnalyzers; diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index 11e74419a40a6..2347422e98e39 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -58,7 +58,7 @@ private sealed class LatestDiagnosticsForSpanGetter private readonly SourceText _text; private readonly ImmutableArray _stateSets; - private readonly CompilationWithAnalyzers? _compilationWithAnalyzers; + private readonly CompilationWithAnalyzersPair? _compilationWithAnalyzers; private readonly TextSpan? _range; private readonly bool _includeSuppressedDiagnostics; @@ -112,7 +112,7 @@ public static async Task CreateAsync( isExplicit, logPerformanceInfo, incrementalAnalysis, diagnosticKinds); } - private static async Task GetOrCreateCompilationWithAnalyzersAsync( + private static async Task GetOrCreateCompilationWithAnalyzersAsync( Project project, ImmutableArray stateSets, bool includeSuppressedDiagnostics, @@ -137,11 +137,13 @@ public static async Task CreateAsync( s_lastProjectAndCompilationWithAnalyzers.SetTarget(new ProjectAndCompilationWithAnalyzers(project, compilationWithAnalyzers)); return compilationWithAnalyzers; - static bool HasAllAnalyzers(IEnumerable stateSets, CompilationWithAnalyzers compilationWithAnalyzers) + static bool HasAllAnalyzers(IEnumerable stateSets, CompilationWithAnalyzersPair compilationWithAnalyzers) { foreach (var stateSet in stateSets) { - if (!compilationWithAnalyzers.Analyzers.Contains(stateSet.Analyzer)) + if (stateSet.IsHostAnalyzer && !compilationWithAnalyzers.HostAnalyzers.Contains(stateSet.Analyzer)) + return false; + else if (!stateSet.IsHostAnalyzer && !compilationWithAnalyzers.ProjectAnalyzers.Contains(stateSet.Analyzer)) return false; } @@ -151,7 +153,7 @@ static bool HasAllAnalyzers(IEnumerable stateSets, CompilationWithAnal private LatestDiagnosticsForSpanGetter( DiagnosticIncrementalAnalyzer owner, - CompilationWithAnalyzers? compilationWithAnalyzers, + CompilationWithAnalyzersPair? compilationWithAnalyzers, TextDocument document, SourceText text, ImmutableArray stateSets, @@ -227,7 +229,7 @@ public async Task GetAsync(ArrayBuilder list, CancellationToken { var existingData = state.GetAnalysisData(AnalysisKind.Syntax); if (!await TryAddCachedDocumentDiagnosticsAsync(stateSet.Analyzer, AnalysisKind.Syntax, existingData, list, cancellationToken).ConfigureAwait(false)) - syntaxAnalyzers.Add(new AnalyzerWithState(stateSet.Analyzer, state, existingData)); + syntaxAnalyzers.Add(new AnalyzerWithState(stateSet.Analyzer, stateSet.IsHostAnalyzer, state, existingData)); } if (includeSemantic) @@ -239,7 +241,7 @@ public async Task GetAsync(ArrayBuilder list, CancellationToken stateSet.Analyzer, _incrementalAnalysis, semanticSpanBasedAnalyzers, semanticDocumentBasedAnalyzers); - stateSets.Add(new AnalyzerWithState(stateSet.Analyzer, state, existingData)); + stateSets.Add(new AnalyzerWithState(stateSet.Analyzer, stateSet.IsHostAnalyzer, state, existingData)); } } } @@ -375,8 +377,9 @@ private async Task ComputeDocumentDiagnosticsAsync( analyzersWithState = filteredAnalyzersWithStateBuilder.ToImmutable(); - var analyzers = analyzersWithState.SelectAsArray(stateSet => stateSet.Analyzer); - var analysisScope = new DocumentAnalysisScope(_document, span, analyzers, kind); + var projectAnalyzers = analyzersWithState.SelectAsArray(stateSet => !stateSet.IsHostAnalyzer, stateSet => stateSet.Analyzer); + var hostAnalyzers = analyzersWithState.SelectAsArray(stateSet => stateSet.IsHostAnalyzer, stateSet => stateSet.Analyzer); + var analysisScope = new DocumentAnalysisScope(_document, span, projectAnalyzers, hostAnalyzers, kind); var executor = new DocumentAnalysisExecutor(analysisScope, _compilationWithAnalyzers, _owner._diagnosticAnalyzerRunner, _isExplicit, _logPerformanceInfo); var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false); @@ -504,7 +507,7 @@ private async Task>.GetInstance(out var builder); - foreach (var analyzer in executor.AnalysisScope.Analyzers) + foreach (var analyzer in executor.AnalysisScope.ProjectAnalyzers.ConcatFast(executor.AnalysisScope.HostAnalyzers)) { var diagnostics = await ComputeDocumentDiagnosticsForAnalyzerCoreAsync(analyzer, executor, cancellationToken).ConfigureAwait(false); builder.Add(analyzer, diagnostics); @@ -534,6 +537,6 @@ private bool ShouldInclude(DiagnosticData diagnostic) } } - private sealed record class AnalyzerWithState(DiagnosticAnalyzer Analyzer, ActiveFileState State, DocumentAnalysisData ExistingData); + private sealed record class AnalyzerWithState(DiagnosticAnalyzer Analyzer, bool IsHostAnalyzer, ActiveFileState State, DocumentAnalysisData ExistingData); } } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index d5f0ef0a644d7..dbba786e56472 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -26,12 +26,13 @@ public async Task> ForceAnalyzeProjectAsync(Proje // PERF: get analyzers that are not suppressed and marked as open file only // this is perf optimization. we cache these result since we know the result. (no diagnostics) - var activeAnalyzers = stateSets.SelectAsArray(s => s.Analyzer); + var activeProjectAnalyzers = stateSets.SelectAsArray(s => !s.IsHostAnalyzer, s => s.Analyzer); + var activeHostAnalyzers = stateSets.SelectAsArray(s => s.IsHostAnalyzer, s => s.Analyzer); - CompilationWithAnalyzers? compilationWithAnalyzers = null; + CompilationWithAnalyzersPair? compilationWithAnalyzers = null; compilationWithAnalyzers = await DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync( - project, activeAnalyzers, includeSuppressedDiagnostics: true, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); + project, activeProjectAnalyzers, activeHostAnalyzers, includeSuppressedDiagnostics: true, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); var result = await GetProjectAnalysisDataAsync(compilationWithAnalyzers, project, stateSets, cancellationToken).ConfigureAwait(false); @@ -100,10 +101,10 @@ private ImmutableArray GetStateSetsForFullSolutionAnalysis(ImmutableAr // Include only analyzers we want to run for full solution analysis. // Analyzers not included here will never be saved because result is unknown. - return stateSets.WhereAsArray(s => IsCandidateForFullSolutionAnalysis(s.Analyzer, project)); + return stateSets.WhereAsArray(static (s, arg) => arg.self.IsCandidateForFullSolutionAnalysis(s.Analyzer, s.IsHostAnalyzer, arg.project), (self: this, project)); } - private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Project project) + private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, bool isHostAnalyzer, Project project) { // PERF: Don't query descriptors for compiler analyzer or workspace load analyzer, always execute them. if (analyzer == FileContentLoadAnalyzer.Instance || @@ -143,7 +144,7 @@ private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Pro var descriptors = DiagnosticAnalyzerInfoCache.GetDiagnosticDescriptors(analyzer); var analyzerConfigOptions = project.GetAnalyzerConfigOptions(); - return descriptors.Any(static (d, arg) => d.GetEffectiveSeverity(arg.CompilationOptions, arg.analyzerConfigOptions?.ConfigOptions, arg.analyzerConfigOptions?.TreeOptions) != ReportDiagnostic.Hidden, (project.CompilationOptions, analyzerConfigOptions)); + return descriptors.Any(static (d, arg) => d.GetEffectiveSeverity(arg.CompilationOptions, arg.isHostAnalyzer ? arg.analyzerConfigOptions?.ConfigOptionsWithFallback : arg.analyzerConfigOptions?.ConfigOptionsWithoutFallback, arg.analyzerConfigOptions?.TreeOptions) != ReportDiagnostic.Hidden, (project.CompilationOptions, isHostAnalyzer, analyzerConfigOptions)); } public TestAccessor GetTestAccessor() diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs index 0c4a6ceebc712..674772daf8bf4 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs @@ -287,7 +287,8 @@ private void UpdateSeverityMenuItemsChecked() foreach (var diagnosticItem in group) { - var severity = diagnosticItem.Descriptor.GetEffectiveSeverity(project.CompilationOptions, analyzerConfigOptions?.ConfigOptions, analyzerConfigOptions?.TreeOptions); + // Currently only project analyzers show in Solution Explorer, so we never need to consider fallback options. + var severity = diagnosticItem.Descriptor.GetEffectiveSeverity(project.CompilationOptions, analyzerConfigOptions?.ConfigOptionsWithoutFallback, analyzerConfigOptions?.TreeOptions); selectedItemSeverities.Add(severity); } } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs index b90add845e4e1..09c14500e946e 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs @@ -116,7 +116,8 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) return; } - var newDiagnosticItems = GenerateDiagnosticItems(project, analyzerReference); + // Currently only project analyzers show in Solution Explorer, so isHostAnalyzer is always false. + var newDiagnosticItems = GenerateDiagnosticItems(project, analyzerReference, isHostAnalyzer: false); var newSourceGeneratorItems = await GenerateSourceGeneratorItemsAsync( project, analyzerReference).ConfigureAwait(false); @@ -143,7 +144,8 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) ImmutableArray GenerateDiagnosticItems( Project project, - AnalyzerReference analyzerReference) + AnalyzerReference analyzerReference, + bool isHostAnalyzer) { var generalDiagnosticOption = project.CompilationOptions!.GeneralDiagnosticOption; var specificDiagnosticOptions = project.CompilationOptions!.SpecificDiagnosticOptions; @@ -158,7 +160,7 @@ ImmutableArray GenerateDiagnosticItems( var selectedDiagnostic = g.OrderBy(d => d, s_comparer).First(); var effectiveSeverity = selectedDiagnostic.GetEffectiveSeverity( project.CompilationOptions!, - analyzerConfigOptions?.ConfigOptions, + isHostAnalyzer ? analyzerConfigOptions?.ConfigOptionsWithFallback : analyzerConfigOptions?.ConfigOptionsWithoutFallback, analyzerConfigOptions?.TreeOptions); return (BaseItem)new DiagnosticItem(project.Id, analyzerReference, selectedDiagnostic, effectiveSeverity, CommandHandler); }); diff --git a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs index 4eedc603d04e6..e916795d62c0e 100644 --- a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs @@ -33,8 +33,9 @@ namespace Roslyn.VisualStudio.Next.UnitTests.Remote [Trait(Traits.Feature, Traits.Features.RemoteHost)] public class VisualStudioDiagnosticAnalyzerExecutorTests { - [Fact] - public async Task TestCSharpAnalyzerOptions() + [Theory] + [CombinatorialData] + public async Task TestCSharpAnalyzerOptions(bool isHostAnalyzer) { var code = @"class Test { @@ -46,7 +47,7 @@ void Method() using var workspace = CreateWorkspace(LanguageNames.CSharp, code); var analyzerType = typeof(CSharpUseExplicitTypeDiagnosticAnalyzer); - var analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType); + var analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, isHostAnalyzer); var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic); Assert.Equal(IDEDiagnosticIds.UseExplicitTypeDiagnosticId, diagnostics[0].Id); @@ -54,15 +55,16 @@ void Method() workspace.SetAnalyzerFallbackOptions(LanguageNames.CSharp, ("csharp_style_var_when_type_is_apparent", "false:suggestion")); - analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType); + analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, isHostAnalyzer); diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic); Assert.Equal(IDEDiagnosticIds.UseExplicitTypeDiagnosticId, diagnostics[0].Id); - Assert.Equal(DiagnosticSeverity.Info, diagnostics[0].Severity); + Assert.Equal(isHostAnalyzer ? DiagnosticSeverity.Info : DiagnosticSeverity.Hidden, diagnostics[0].Severity); } - [Fact] - public async Task TestVisualBasicAnalyzerOptions() + [Theory] + [CombinatorialData] + public async Task TestVisualBasicAnalyzerOptions(bool isHostAnalyzer) { var code = @"Class Test Sub Method() @@ -76,16 +78,27 @@ End Sub workspace.SetAnalyzerFallbackOptions(LanguageNames.VisualBasic, ("dotnet_style_null_propagation", "false:silent")); var analyzerType = typeof(VisualBasicUseNullPropagationDiagnosticAnalyzer); - var analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType); + var analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, isHostAnalyzer); - Assert.True(analyzerResult.IsEmpty); + ImmutableArray diagnostics; + if (isHostAnalyzer) + { + Assert.True(analyzerResult.IsEmpty); + } + else + { + diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic); + Assert.Equal(IDEDiagnosticIds.UseNullPropagationDiagnosticId, diagnostics[0].Id); + Assert.Equal(DiagnosticSeverity.Info, diagnostics[0].Severity); + } workspace.SetAnalyzerFallbackOptions(LanguageNames.VisualBasic, ("dotnet_style_null_propagation", "true:error")); - analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType); + analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, isHostAnalyzer); - var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic); + diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic); Assert.Equal(IDEDiagnosticIds.UseNullPropagationDiagnosticId, diagnostics[0].Id); + Assert.Equal(isHostAnalyzer ? DiagnosticSeverity.Error : DiagnosticSeverity.Info, diagnostics[0].Severity); } [Fact] @@ -103,7 +116,7 @@ public async Task TestCancellation() try { - var task = Task.Run(() => AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, source.Token)); + var task = Task.Run(() => AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, isHostAnalyzer: false, source.Token)); // wait random milli-second var random = new Random(Environment.TickCount); @@ -146,12 +159,14 @@ void Method() var runner = CreateAnalyzerRunner(); - var compilationWithAnalyzers = (await project.GetCompilationAsync()).WithAnalyzers( - analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), - project.AnalyzerOptions); + var compilationWithAnalyzers = new CompilationWithAnalyzersPair( + projectCompilationWithAnalyzers: null, + (await project.GetCompilationAsync()).WithAnalyzers( + analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), + project.AnalyzerOptions)); var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); - var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]]; + var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.HostAnalyzers[0]]; // check result var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic); @@ -183,13 +198,14 @@ void Method() var runner = CreateAnalyzerRunner(); var analyzers = analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(); - var compilationWithAnalyzers = (await project.GetCompilationAsync()) - .WithAnalyzers(analyzers, project.AnalyzerOptions); + var compilationWithAnalyzers = new CompilationWithAnalyzersPair( + (await project.GetCompilationAsync()).WithAnalyzers(analyzers, project.AnalyzerOptions), + hostCompilationWithAnalyzers: null); var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); - var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]]; + var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.ProjectAnalyzers[0]]; // check result var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Syntax); @@ -199,21 +215,34 @@ void Method() private static InProcOrRemoteHostAnalyzerRunner CreateAnalyzerRunner() => new(enabled: true, new DiagnosticAnalyzerInfoCache()); - private static async Task AnalyzeAsync(TestWorkspace workspace, ProjectId projectId, Type analyzerType, CancellationToken cancellationToken = default) + private static async Task AnalyzeAsync(TestWorkspace workspace, ProjectId projectId, Type analyzerType, bool isHostAnalyzer, CancellationToken cancellationToken = default) { var executor = CreateAnalyzerRunner(); var analyzerReference = new AnalyzerFileReference(analyzerType.Assembly.Location, new TestAnalyzerAssemblyLoader()); - var project = workspace.CurrentSolution.GetProject(projectId).AddAnalyzerReference(analyzerReference); + var solution = workspace.CurrentSolution; + if (isHostAnalyzer) + { + solution = solution.AddAnalyzerReference(analyzerReference); + } - var analyzerDriver = (await project.GetCompilationAsync()).WithAnalyzers( + var project = solution.GetProject(projectId); + if (!isHostAnalyzer) + { + project = project.AddAnalyzerReference(analyzerReference); + } + + var compilationWithAnalyzers = (await project.GetCompilationAsync()).WithAnalyzers( analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), project.AnalyzerOptions); + var analyzerDriver = isHostAnalyzer + ? new CompilationWithAnalyzersPair(projectCompilationWithAnalyzers: null, compilationWithAnalyzers) + : new CompilationWithAnalyzersPair(compilationWithAnalyzers, hostCompilationWithAnalyzers: null); var result = await executor.AnalyzeProjectAsync( project, analyzerDriver, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken); - return result.AnalysisResult[analyzerDriver.Analyzers[0]]; + return result.AnalysisResult[(isHostAnalyzer ? analyzerDriver.HostAnalyzers : analyzerDriver.ProjectAnalyzers)[0]]; } private static TestWorkspace CreateWorkspace(string language, string code, ParseOptions options = null) diff --git a/src/Workspaces/Core/Portable/Diagnostics/CompilationWithAnalyzersPair.cs b/src/Workspaces/Core/Portable/Diagnostics/CompilationWithAnalyzersPair.cs new file mode 100644 index 0000000000000..3c34a9b9da17a --- /dev/null +++ b/src/Workspaces/Core/Portable/Diagnostics/CompilationWithAnalyzersPair.cs @@ -0,0 +1,116 @@ +// 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.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics.Telemetry; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal sealed class CompilationWithAnalyzersPair +{ + private readonly CompilationWithAnalyzers? _projectCompilationWithAnalyzers; + private readonly CompilationWithAnalyzers? _hostCompilationWithAnalyzers; + + public CompilationWithAnalyzersPair(CompilationWithAnalyzers? projectCompilationWithAnalyzers, CompilationWithAnalyzers? hostCompilationWithAnalyzers) + { + if (projectCompilationWithAnalyzers is not null && hostCompilationWithAnalyzers is not null) + { + Contract.ThrowIfFalse(projectCompilationWithAnalyzers.AnalysisOptions.ReportSuppressedDiagnostics == hostCompilationWithAnalyzers.AnalysisOptions.ReportSuppressedDiagnostics); + Contract.ThrowIfFalse(projectCompilationWithAnalyzers.AnalysisOptions.ConcurrentAnalysis == hostCompilationWithAnalyzers.AnalysisOptions.ConcurrentAnalysis); + } + else + { + Contract.ThrowIfTrue(projectCompilationWithAnalyzers is null && hostCompilationWithAnalyzers is null); + } + + _projectCompilationWithAnalyzers = projectCompilationWithAnalyzers; + _hostCompilationWithAnalyzers = hostCompilationWithAnalyzers; + } + + public Compilation? ProjectCompilation => _projectCompilationWithAnalyzers?.Compilation; + + public Compilation? HostCompilation => _hostCompilationWithAnalyzers?.Compilation; + + public CompilationWithAnalyzers? ProjectCompilationWithAnalyzers => _projectCompilationWithAnalyzers; + + public CompilationWithAnalyzers? HostCompilationWithAnalyzers => _hostCompilationWithAnalyzers; + + public bool ReportSuppressedDiagnostics => _projectCompilationWithAnalyzers?.AnalysisOptions.ReportSuppressedDiagnostics ?? _hostCompilationWithAnalyzers!.AnalysisOptions.ReportSuppressedDiagnostics; + + public bool ConcurrentAnalysis => _projectCompilationWithAnalyzers?.AnalysisOptions.ConcurrentAnalysis ?? _hostCompilationWithAnalyzers!.AnalysisOptions.ConcurrentAnalysis; + + public bool HasAnalyzers => ProjectAnalyzers.Any() || HostAnalyzers.Any(); + + public ImmutableArray ProjectAnalyzers => _projectCompilationWithAnalyzers?.Analyzers ?? []; + + public ImmutableArray HostAnalyzers => _hostCompilationWithAnalyzers?.Analyzers ?? []; + + public Task GetAnalyzerTelemetryInfoAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + { + if (ProjectAnalyzers.Contains(analyzer)) + { + return ProjectCompilationWithAnalyzers!.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken); + } + else + { + Debug.Assert(HostAnalyzers.Contains(analyzer)); + return HostCompilationWithAnalyzers!.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken); + } + } + + public async Task<(AnalysisResult? projectAnalysisResult, AnalysisResult? hostAnalysisResult)> GetAnalysisResultAsync(CancellationToken cancellationToken) + { + var projectAnalysisResult = ProjectCompilationWithAnalyzers is not null + ? await ProjectCompilationWithAnalyzers.GetAnalysisResultAsync(cancellationToken).ConfigureAwait(false) + : null; + var hostAnalysisResult = HostCompilationWithAnalyzers is not null + ? await HostCompilationWithAnalyzers.GetAnalysisResultAsync(cancellationToken).ConfigureAwait(false) + : null; + + return (projectAnalysisResult, hostAnalysisResult); + } + + public async Task<(AnalysisResult? projectAnalysisResult, AnalysisResult? hostAnalysisResult)> GetAnalysisResultAsync(SyntaxTree tree, TextSpan? filterSpan, ImmutableArray projectAnalyzers, ImmutableArray hostAnalyzers, CancellationToken cancellationToken) + { + var projectAnalysisResult = projectAnalyzers.Any() + ? await ProjectCompilationWithAnalyzers!.GetAnalysisResultAsync(tree, filterSpan, projectAnalyzers, cancellationToken).ConfigureAwait(false) + : null; + var hostAnalysisResult = hostAnalyzers.Any() + ? await HostCompilationWithAnalyzers!.GetAnalysisResultAsync(tree, filterSpan, hostAnalyzers, cancellationToken).ConfigureAwait(false) + : null; + + return (projectAnalysisResult, hostAnalysisResult); + } + + public async Task<(AnalysisResult? projectAnalysisResult, AnalysisResult? hostAnalysisResult)> GetAnalysisResultAsync(AdditionalText file, TextSpan? filterSpan, ImmutableArray projectAnalyzers, ImmutableArray hostAnalyzers, CancellationToken cancellationToken) + { + var projectAnalysisResult = projectAnalyzers.Any() + ? await ProjectCompilationWithAnalyzers!.GetAnalysisResultAsync(file, filterSpan, projectAnalyzers, cancellationToken).ConfigureAwait(false) + : null; + var hostAnalysisResult = hostAnalyzers.Any() + ? await HostCompilationWithAnalyzers!.GetAnalysisResultAsync(file, filterSpan, hostAnalyzers, cancellationToken).ConfigureAwait(false) + : null; + + return (projectAnalysisResult, hostAnalysisResult); + } + + public async Task<(AnalysisResult? projectAnalysisResult, AnalysisResult? hostAnalysisResult)> GetAnalysisResultAsync(SemanticModel model, TextSpan? filterSpan, ImmutableArray projectAnalyzers, ImmutableArray hostAnalyzers, CancellationToken cancellationToken) + { + var projectAnalysisResult = projectAnalyzers.Any() + ? await ProjectCompilationWithAnalyzers!.GetAnalysisResultAsync(model, filterSpan, projectAnalyzers, cancellationToken).ConfigureAwait(false) + : null; + var hostAnalysisResult = hostAnalyzers.Any() + ? await HostCompilationWithAnalyzers!.GetAnalysisResultAsync(model, filterSpan, hostAnalyzers, cancellationToken).ConfigureAwait(false) + : null; + + return (projectAnalysisResult, hostAnalysisResult); + } +} diff --git a/src/Workspaces/Core/Portable/Diagnostics/DocumentAnalysisScope.cs b/src/Workspaces/Core/Portable/Diagnostics/DocumentAnalysisScope.cs index 59832a12f8294..a0a4bfae6cff5 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/DocumentAnalysisScope.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/DocumentAnalysisScope.cs @@ -21,15 +21,19 @@ internal sealed class DocumentAnalysisScope public DocumentAnalysisScope( TextDocument document, TextSpan? span, - ImmutableArray analyzers, + ImmutableArray projectAnalyzers, + ImmutableArray hostAnalyzers, AnalysisKind kind) { Debug.Assert(kind is AnalysisKind.Syntax or AnalysisKind.Semantic); - Debug.Assert(!analyzers.IsDefaultOrEmpty); + Debug.Assert(!projectAnalyzers.IsDefault); + Debug.Assert(!hostAnalyzers.IsDefault); + Debug.Assert(!projectAnalyzers.IsEmpty || !hostAnalyzers.IsEmpty); TextDocument = document; Span = span; - Analyzers = analyzers; + ProjectAnalyzers = projectAnalyzers; + HostAnalyzers = hostAnalyzers; Kind = kind; _lazyAdditionalFile = new Lazy(ComputeAdditionalFile); @@ -37,7 +41,8 @@ public DocumentAnalysisScope( public TextDocument TextDocument { get; } public TextSpan? Span { get; } - public ImmutableArray Analyzers { get; } + public ImmutableArray ProjectAnalyzers { get; } + public ImmutableArray HostAnalyzers { get; } public AnalysisKind Kind { get; } /// @@ -55,8 +60,8 @@ private AdditionalText ComputeAdditionalFile() } public DocumentAnalysisScope WithSpan(TextSpan? span) - => new(TextDocument, span, Analyzers, Kind); + => new(TextDocument, span, ProjectAnalyzers, HostAnalyzers, Kind); - public DocumentAnalysisScope WithAnalyzers(ImmutableArray analyzers) - => new(TextDocument, Span, analyzers, Kind); + public DocumentAnalysisScope WithAnalyzers(ImmutableArray projectAnalyzers, ImmutableArray hostAnalyzers) + => new(TextDocument, Span, projectAnalyzers, hostAnalyzers, Kind); } diff --git a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs index 24d7aad61aa83..b88f6abf6da55 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs @@ -96,7 +96,7 @@ public static async Task analyzers, + ImmutableArray analyzers, SkippedHostAnalyzersInfo skippedAnalyzersInfo, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) @@ -205,7 +205,7 @@ public static async Task d.Location.SourceTree == treeToAnalyze); AddDiagnosticsToResult(diagnostics, ref result, compilation, treeToAnalyze, additionalDocumentId: null, - documentAnalysisScope!.Span, AnalysisKind.Semantic, diagnosticIdsToFilter, includeSuppressedDiagnostics); + documentAnalysisScope.Span, AnalysisKind.Semantic, diagnosticIdsToFilter, includeSuppressedDiagnostics); } } else @@ -313,8 +313,8 @@ public static ImmutableArray Filter( filterSpan.HasValue && !filterSpan.Value.IntersectsWith(diagnostic.Location.SourceSpan)); } - public static async Task<(AnalysisResult result, ImmutableArray additionalDiagnostics)> GetAnalysisResultAsync( - this CompilationWithAnalyzers compilationWithAnalyzers, + public static async Task<(AnalysisResult? projectAnalysisResult, AnalysisResult? hostAnalysisResult, ImmutableArray additionalDiagnostics)> GetAnalysisResultAsync( + this CompilationWithAnalyzersPair compilationWithAnalyzers, DocumentAnalysisScope? documentAnalysisScope, Project project, DiagnosticAnalyzerInfoCache analyzerInfoCache, @@ -323,11 +323,11 @@ public static ImmutableArray Filter( var result = await GetAnalysisResultAsync(compilationWithAnalyzers, documentAnalysisScope, cancellationToken).ConfigureAwait(false); var additionalDiagnostics = await compilationWithAnalyzers.GetPragmaSuppressionAnalyzerDiagnosticsAsync( documentAnalysisScope, project, analyzerInfoCache, cancellationToken).ConfigureAwait(false); - return (result, additionalDiagnostics); + return (result.projectAnalysisResult, result.hostAnalysisResult, additionalDiagnostics); } - private static async Task GetAnalysisResultAsync( - CompilationWithAnalyzers compilationWithAnalyzers, + private static async Task<(AnalysisResult? projectAnalysisResult, AnalysisResult? hostAnalysisResult)> GetAnalysisResultAsync( + CompilationWithAnalyzersPair compilationWithAnalyzers, DocumentAnalysisScope? documentAnalysisScope, CancellationToken cancellationToken) { @@ -336,7 +336,8 @@ private static async Task GetAnalysisResultAsync( return await compilationWithAnalyzers.GetAnalysisResultAsync(cancellationToken).ConfigureAwait(false); } - Debug.Assert(documentAnalysisScope.Analyzers.ToSet().IsSubsetOf(compilationWithAnalyzers.Analyzers)); + Debug.Assert(documentAnalysisScope.ProjectAnalyzers.ToSet().IsSubsetOf(compilationWithAnalyzers.ProjectAnalyzers)); + Debug.Assert(documentAnalysisScope.HostAnalyzers.ToSet().IsSubsetOf(compilationWithAnalyzers.HostAnalyzers)); switch (documentAnalysisScope.Kind) { @@ -344,16 +345,16 @@ private static async Task GetAnalysisResultAsync( if (documentAnalysisScope.TextDocument is Document document) { var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - return await compilationWithAnalyzers.GetAnalysisResultAsync(tree, documentAnalysisScope.Span, documentAnalysisScope.Analyzers, cancellationToken).ConfigureAwait(false); + return await compilationWithAnalyzers.GetAnalysisResultAsync(tree, documentAnalysisScope.Span, documentAnalysisScope.ProjectAnalyzers, documentAnalysisScope.HostAnalyzers, cancellationToken).ConfigureAwait(false); } else { - return await compilationWithAnalyzers.GetAnalysisResultAsync(documentAnalysisScope.AdditionalFile, documentAnalysisScope.Span, documentAnalysisScope.Analyzers, cancellationToken).ConfigureAwait(false); + return await compilationWithAnalyzers.GetAnalysisResultAsync(documentAnalysisScope.AdditionalFile, documentAnalysisScope.Span, documentAnalysisScope.ProjectAnalyzers, documentAnalysisScope.HostAnalyzers, cancellationToken).ConfigureAwait(false); } case AnalysisKind.Semantic: var model = await ((Document)documentAnalysisScope.TextDocument).GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return await compilationWithAnalyzers.GetAnalysisResultAsync(model, documentAnalysisScope.Span, documentAnalysisScope.Analyzers, cancellationToken).ConfigureAwait(false); + return await compilationWithAnalyzers.GetAnalysisResultAsync(model, documentAnalysisScope.Span, documentAnalysisScope.ProjectAnalyzers, documentAnalysisScope.HostAnalyzers, cancellationToken).ConfigureAwait(false); default: throw ExceptionUtilities.UnexpectedValue(documentAnalysisScope.Kind); @@ -361,17 +362,19 @@ private static async Task GetAnalysisResultAsync( } private static async Task> GetPragmaSuppressionAnalyzerDiagnosticsAsync( - this CompilationWithAnalyzers compilationWithAnalyzers, + this CompilationWithAnalyzersPair compilationWithAnalyzers, DocumentAnalysisScope? documentAnalysisScope, Project project, DiagnosticAnalyzerInfoCache analyzerInfoCache, CancellationToken cancellationToken) { - var analyzers = documentAnalysisScope?.Analyzers ?? compilationWithAnalyzers.Analyzers; - var suppressionAnalyzer = analyzers.OfType().FirstOrDefault(); + var hostAnalyzers = documentAnalysisScope?.HostAnalyzers ?? compilationWithAnalyzers.HostAnalyzers; + var suppressionAnalyzer = hostAnalyzers.OfType().FirstOrDefault(); if (suppressionAnalyzer == null) return []; + RoslynDebug.AssertNotNull(compilationWithAnalyzers.HostCompilationWithAnalyzers); + if (documentAnalysisScope != null) { if (documentAnalysisScope.TextDocument is not Document document) @@ -379,24 +382,24 @@ private static async Task> GetPragmaSuppressionAnalyz using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); await AnalyzeDocumentAsync( - compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, + compilationWithAnalyzers.HostCompilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, document, documentAnalysisScope.Span, diagnosticsBuilder.Add, cancellationToken).ConfigureAwait(false); return diagnosticsBuilder.ToImmutableAndClear(); } else { - if (compilationWithAnalyzers.AnalysisOptions.ConcurrentAnalysis) + if (compilationWithAnalyzers.ConcurrentAnalysis) { return await ProducerConsumer.RunParallelAsync( source: project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken), produceItems: static async (document, callback, args, cancellationToken) => { - var (compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer) = args; + var (hostCompilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer) = args; await AnalyzeDocumentAsync( - compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, + hostCompilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, document, span: null, callback, cancellationToken).ConfigureAwait(false); }, - args: (compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer), + args: (compilationWithAnalyzers.HostCompilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer), cancellationToken).ConfigureAwait(false); } else @@ -405,7 +408,7 @@ await AnalyzeDocumentAsync( await foreach (var document in project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)) { await AnalyzeDocumentAsync( - compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, + compilationWithAnalyzers.HostCompilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, document, span: null, diagnosticsBuilder.Add, cancellationToken).ConfigureAwait(false); } @@ -414,7 +417,7 @@ await AnalyzeDocumentAsync( } static async Task AnalyzeDocumentAsync( - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzers hostCompilationWithAnalyzers, DiagnosticAnalyzerInfoCache analyzerInfoCache, IPragmaSuppressionsAnalyzer suppressionAnalyzer, Document document, @@ -424,7 +427,7 @@ static async Task AnalyzeDocumentAsync( { var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); await suppressionAnalyzer.AnalyzeAsync( - semanticModel, span, compilationWithAnalyzers, analyzerInfoCache.GetDiagnosticDescriptors, reportDiagnostic, cancellationToken).ConfigureAwait(false); + semanticModel, span, hostCompilationWithAnalyzers, analyzerInfoCache.GetDiagnosticDescriptors, reportDiagnostic, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt b/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt index 2ebc1a3610226..f6105b778979e 100644 --- a/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.CodeAnalysis.Project.HostAnalyzerOptions.get -> Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions Microsoft.CodeAnalysis.ProjectInfo.WithId(Microsoft.CodeAnalysis.ProjectId id) -> Microsoft.CodeAnalysis.ProjectInfo virtual Microsoft.CodeAnalysis.Host.HostLanguageServices.Dispose() -> void virtual Microsoft.CodeAnalysis.Host.HostWorkspaceServices.Dispose() -> void diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs index 952ce33c736e1..0ca3043968817 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs @@ -10,13 +10,24 @@ namespace Microsoft.CodeAnalysis; /// /// Aggregate analyzer config options for a specific path. /// -internal readonly struct AnalyzerConfigData(AnalyzerConfigOptionsResult result, StructuredAnalyzerConfigOptions fallbackOptions) +internal readonly struct AnalyzerConfigData { - public readonly StructuredAnalyzerConfigOptions ConfigOptions = StructuredAnalyzerConfigOptions.Create( - new DictionaryAnalyzerConfigOptions(result.AnalyzerOptions), fallbackOptions); + private readonly AnalyzerConfigOptions _dictionaryConfigOptions; + + public readonly StructuredAnalyzerConfigOptions ConfigOptionsWithoutFallback; + + public readonly StructuredAnalyzerConfigOptions ConfigOptionsWithFallback; /// /// These options do not fall back. /// - public readonly ImmutableDictionary TreeOptions = result.TreeOptions; + public readonly ImmutableDictionary TreeOptions; + + public AnalyzerConfigData(AnalyzerConfigOptionsResult result, StructuredAnalyzerConfigOptions fallbackOptions) + { + _dictionaryConfigOptions = new DictionaryAnalyzerConfigOptions(result.AnalyzerOptions); + ConfigOptionsWithoutFallback = StructuredAnalyzerConfigOptions.Create(_dictionaryConfigOptions, StructuredAnalyzerConfigOptions.Empty); + ConfigOptionsWithFallback = StructuredAnalyzerConfigOptions.Create(_dictionaryConfigOptions, fallbackOptions); + TreeOptions = result.TreeOptions; + } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index 0d7fa9786fed0..068122537e3e3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -546,7 +546,7 @@ private void InitializeCachedOptions(OptionSet solutionOptions) { var newAsyncLazy = AsyncLazy.Create(static async (arg, cancellationToken) => { - var options = await arg.self.GetAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false); + var options = await arg.self.GetHostAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false); return new DocumentOptionSet(options, arg.solutionOptions, arg.self.Project.Language); }, arg: (self: this, solutionOptions)); @@ -555,9 +555,9 @@ private void InitializeCachedOptions(OptionSet solutionOptions) } #pragma warning restore - internal async ValueTask GetAnalyzerConfigOptionsAsync(CancellationToken cancellationToken) + internal async ValueTask GetHostAnalyzerConfigOptionsAsync(CancellationToken cancellationToken) { - var provider = (ProjectState.ProjectAnalyzerConfigOptionsProvider)Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider; + var provider = (ProjectState.ProjectHostAnalyzerConfigOptionsProvider)Project.State.HostAnalyzerOptions.AnalyzerConfigOptionsProvider; return await provider.GetOptionsAsync(DocumentState, cancellationToken).ConfigureAwait(false); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index ea32cc1235445..9941e55b426d7 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -147,7 +147,12 @@ internal Project(Solution solution, ProjectState projectState) /// /// The options used by analyzers for this project. /// - public AnalyzerOptions AnalyzerOptions => State.AnalyzerOptions; + public AnalyzerOptions AnalyzerOptions => State.ProjectAnalyzerOptions; + + /// + /// The options used by analyzers for this project. + /// + public AnalyzerOptions HostAnalyzerOptions => State.HostAnalyzerOptions; /// /// The options used when building the compilation for this project. diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index b6cf790c348d9..dfa05d720a367 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.Host; @@ -58,7 +59,11 @@ internal partial class ProjectState /// private readonly AnalyzerConfigOptionsCache _analyzerConfigOptionsCache; - private AnalyzerOptions? _lazyAnalyzerOptions; + private ImmutableArray _lazyAdditionalFiles; + + private AnalyzerOptions? _lazyProjectAnalyzerOptions; + + private AnalyzerOptions? _lazyHostAnalyzerOptions; private ProjectState( ProjectInfo projectInfo, @@ -291,10 +296,32 @@ internal TDocumentState CreateDocument(DocumentInfo documentInfo typeof(TDocumentState) == typeof(AnalyzerConfigDocumentState) ? new AnalyzerConfigDocumentState(LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(ChecksumAlgorithm)) : throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState))); - public AnalyzerOptions AnalyzerOptions - => _lazyAnalyzerOptions ??= new AnalyzerOptions( - additionalFiles: AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText), - optionsProvider: new ProjectAnalyzerConfigOptionsProvider(this)); + private ImmutableArray AdditionalFiles + { + get + { + return InterlockedOperations.Initialize( + ref _lazyAdditionalFiles, + static self => self.AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText), + this); + } + } + + public AnalyzerOptions ProjectAnalyzerOptions + => InterlockedOperations.Initialize( + ref _lazyProjectAnalyzerOptions, + static self => new AnalyzerOptions( + additionalFiles: self.AdditionalFiles, + optionsProvider: new ProjectAnalyzerConfigOptionsProvider(self)), + this); + + public AnalyzerOptions HostAnalyzerOptions + => InterlockedOperations.Initialize( + ref _lazyHostAnalyzerOptions, + static self => new AnalyzerOptions( + additionalFiles: self.AdditionalFiles, + optionsProvider: new ProjectHostAnalyzerConfigOptionsProvider(self)), + this); public AnalyzerConfigData GetAnalyzerOptionsForPath(string path, CancellationToken cancellationToken) => _analyzerConfigOptionsCache.Lazy.GetValue(cancellationToken).GetOptionsForSourcePath(path); @@ -333,6 +360,76 @@ public AnalyzerConfigData GetAnalyzerOptionsForPath(string path, CancellationTok } internal sealed class ProjectAnalyzerConfigOptionsProvider(ProjectState projectState) : AnalyzerConfigOptionsProvider + { + private AnalyzerConfigOptionsCache.Value GetCache() + => projectState._analyzerConfigOptionsCache.Lazy.GetValue(CancellationToken.None); + + public override AnalyzerConfigOptions GlobalOptions + => GetCache().GlobalConfigOptions.ConfigOptionsWithoutFallback; + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) + { + var documentId = DocumentState.GetDocumentIdForTree(tree); + var cache = GetCache(); + if (documentId != null && projectState.DocumentStates.TryGetState(documentId, out var documentState)) + { + return GetOptions(cache, documentState); + } + + return GetOptionsForSourcePath(cache, tree.FilePath); + } + + internal async ValueTask GetOptionsAsync(DocumentState documentState, CancellationToken cancellationToken) + { + var cache = await projectState._analyzerConfigOptionsCache.Lazy.GetValueAsync(cancellationToken).ConfigureAwait(false); + return GetOptions(cache, documentState); + } + + private StructuredAnalyzerConfigOptions GetOptions(in AnalyzerConfigOptionsCache.Value cache, DocumentState documentState) + { + var filePath = GetEffectiveFilePath(documentState); + return filePath == null + ? StructuredAnalyzerConfigOptions.Empty + : GetOptionsForSourcePath(cache, filePath); + } + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) + { + // TODO: correctly find the file path, since it looks like we give this the document's .Name under the covers if we don't have one + return GetOptionsForSourcePath(GetCache(), textFile.Path); + } + + private static StructuredAnalyzerConfigOptions GetOptionsForSourcePath(in AnalyzerConfigOptionsCache.Value cache, string path) + => cache.GetOptionsForSourcePath(path).ConfigOptionsWithoutFallback; + + private string? GetEffectiveFilePath(DocumentState documentState) + { + if (!string.IsNullOrEmpty(documentState.FilePath)) + { + return documentState.FilePath; + } + + // We need to work out path to this document. Documents may not have a "real" file path if they're something created + // as a part of a code action, but haven't been written to disk yet. + + var projectFilePath = projectState.FilePath; + + if (documentState.Name != null && projectFilePath != null) + { + var projectPath = PathUtilities.GetDirectoryName(projectFilePath); + + if (!RoslynString.IsNullOrEmpty(projectPath) && + PathUtilities.GetDirectoryName(projectFilePath) is string directory) + { + return PathUtilities.CombinePathsUnchecked(directory, documentState.Name); + } + } + + return null; + } + } + + internal sealed class ProjectHostAnalyzerConfigOptionsProvider(ProjectState projectState) : AnalyzerConfigOptionsProvider { private RazorDesignTimeAnalyzerConfigOptions? _lazyRazorDesignTimeOptions = null; @@ -340,7 +437,7 @@ private AnalyzerConfigOptionsCache.Value GetCache() => projectState._analyzerConfigOptionsCache.Lazy.GetValue(CancellationToken.None); public override AnalyzerConfigOptions GlobalOptions - => GetCache().GlobalConfigOptions.ConfigOptions; + => GetCache().GlobalConfigOptions.ConfigOptionsWithoutFallback; public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) { @@ -382,7 +479,7 @@ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) } private static StructuredAnalyzerConfigOptions GetOptionsForSourcePath(in AnalyzerConfigOptionsCache.Value cache, string path) - => cache.GetOptionsForSourcePath(path).ConfigOptions; + => cache.GetOptionsForSourcePath(path).ConfigOptionsWithFallback; private string? GetEffectiveFilePath(DocumentState documentState) { @@ -467,7 +564,7 @@ public override GeneratedKind IsGenerated(SyntaxTree tree, CancellationToken can { var options = _lazyAnalyzerConfigSet.Lazy .GetValue(cancellationToken).GetOptionsForSourcePath(tree.FilePath); - return GeneratedCodeUtilities.GetGeneratedCodeKindFromOptions(options.ConfigOptions); + return GeneratedCodeUtilities.GetGeneratedCodeKindFromOptions(options.ConfigOptionsWithoutFallback); } public override bool TryGetDiagnosticValue(SyntaxTree tree, string diagnosticId, CancellationToken cancellationToken, out ReportDiagnostic severity) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs index 366d9f75aeac4..ddaf3ff5087d5 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs @@ -396,7 +396,7 @@ static GeneratorDriver CreateGeneratorDriver(ProjectState projectState) return compilationFactory.CreateGeneratorDriver( projectState.ParseOptions!, GetSourceGenerators(projectState), - projectState.AnalyzerOptions.AnalyzerConfigOptionsProvider, + projectState.ProjectAnalyzerOptions.AnalyzerConfigOptionsProvider, additionalTexts); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs index 75056cd38afdb..c116568f3a9e6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs @@ -127,7 +127,7 @@ public override Task TransformCompilationAsync(Compilation oldCompi public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true; public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) - => generatorDriver.WithUpdatedAnalyzerConfigOptions(NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider); + => generatorDriver.WithUpdatedAnalyzerConfigOptions(NewProjectState.ProjectAnalyzerOptions.AnalyzerConfigOptionsProvider); } internal sealed class RemoveDocumentsAction( @@ -364,7 +364,7 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver _) var generatorDriver = oldGeneratorDriver .ReplaceAdditionalTexts(this.NewProjectState.AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText)) .WithUpdatedParseOptions(this.NewProjectState.ParseOptions!) - .WithUpdatedAnalyzerConfigOptions(this.NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider) + .WithUpdatedAnalyzerConfigOptions(this.NewProjectState.ProjectAnalyzerOptions.AnalyzerConfigOptionsProvider) .ReplaceGenerators(GetSourceGenerators(this.NewProjectState)); return generatorDriver; diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index 81169657802d4..abe1dbc185fbe 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -2154,8 +2154,8 @@ public void RemoveAnalyzerReference() public void FallbackAnalyzerOptions(bool isRoot) { using var workspace = CreateWorkspaceWithProjectAndDocuments(editorConfig: $""" - [*] {(isRoot ? "root = true" : "")} + [*] optionA = A """); var solution = workspace.CurrentSolution; @@ -2167,17 +2167,17 @@ public void FallbackAnalyzerOptions(bool isRoot) .Add("optionA", "fallbackA") .Add("optionB", "fallbackB"))))); - TestOptionValues(solution2, expectedA: "A", expectedB: "fallbackB"); + TestOptionValues(solution2, expectedA: "A", hasBWithoutFallback: false, expectedB: "fallbackB"); var solution3 = solution2.WithFallbackAnalyzerOptions(ImmutableDictionary.Empty .Add(LanguageNames.CSharp, StructuredAnalyzerConfigOptions.Create(new DictionaryAnalyzerConfigOptions(ImmutableDictionary.Empty .Add("optionA", "fallbackX") .Add("optionB", "fallbackY"))))); - TestOptionValues(solution3, expectedA: "A", expectedB: "fallbackY"); + TestOptionValues(solution3, expectedA: "A", hasBWithoutFallback: false, expectedB: "fallbackY"); Assert.True(workspace.TryApplyChanges(solution3)); - TestOptionValues(workspace.CurrentSolution, expectedA: "A", expectedB: "fallbackY"); + TestOptionValues(workspace.CurrentSolution, expectedA: "A", hasBWithoutFallback: false, expectedB: "fallbackY"); var editorConfigContent = """ [*] @@ -2186,26 +2186,34 @@ public void FallbackAnalyzerOptions(bool isRoot) var editorConfigId = DocumentId.CreateNewId(solution3.Projects.Single().Id); var solution4 = solution3.AddAnalyzerConfigDocument(editorConfigId, "editorconfig2", SourceText.From(editorConfigContent), filePath: Path.Combine(s_projectDir, "editorconfig2")); - TestOptionValues(solution4, expectedA: "A", expectedB: "ec2"); + TestOptionValues(solution4, expectedA: "A", hasBWithoutFallback: true, expectedB: "ec2"); - static void TestOptionValues(Solution solution, string expectedA, string expectedB) + static void TestOptionValues(Solution solution, string expectedA, bool hasBWithoutFallback, string expectedB) { var project2 = solution.Projects.Single(); var projectOptions = project2.GetAnalyzerConfigOptions(); Assert.NotNull(projectOptions); - Assert.True(projectOptions!.Value.ConfigOptions.TryGetValue("optionA", out var value1)); + Assert.True(projectOptions!.Value.ConfigOptionsWithoutFallback.TryGetValue("optionA", out var value1)); + Assert.Equal(expectedA, value1); + Assert.True(projectOptions!.Value.ConfigOptionsWithFallback.TryGetValue("optionA", out value1)); Assert.Equal(expectedA, value1); - Assert.True(projectOptions!.Value.ConfigOptions.TryGetValue("optionB", out var value2)); + Assert.Equal(hasBWithoutFallback, projectOptions!.Value.ConfigOptionsWithoutFallback.TryGetValue("optionB", out var value2)); + Assert.Equal(hasBWithoutFallback ? expectedB : null, value2); + Assert.True(projectOptions!.Value.ConfigOptionsWithFallback.TryGetValue("optionB", out value2)); Assert.Equal(expectedB, value2); var sourcePathOptions = project2.State.GetAnalyzerOptionsForPath(Path.Combine(s_projectDir, "x.cs"), CancellationToken.None); - Assert.True(sourcePathOptions.ConfigOptions.TryGetValue("optionA", out var value3)); + Assert.True(sourcePathOptions.ConfigOptionsWithoutFallback.TryGetValue("optionA", out var value3)); + Assert.Equal(expectedA, value3); + Assert.True(sourcePathOptions.ConfigOptionsWithFallback.TryGetValue("optionA", out value3)); Assert.Equal(expectedA, value3); - Assert.True(sourcePathOptions.ConfigOptions.TryGetValue("optionB", out var value4)); + Assert.Equal(hasBWithoutFallback, sourcePathOptions.ConfigOptionsWithoutFallback.TryGetValue("optionB", out var value4)); + Assert.Equal(hasBWithoutFallback ? expectedB : null, value4); + Assert.True(sourcePathOptions.ConfigOptionsWithFallback.TryGetValue("optionB", out value4)); Assert.Equal(expectedB, value4); } } @@ -5382,11 +5390,12 @@ public async Task EditorConfigOptions(string projectPath, string configPath, str #pragma warning restore var syntaxTree = await document.GetSyntaxTreeAsync(); - var documentOptionsViaSyntaxTree = document.Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree); + var documentOptionsViaSyntaxTree = document.Project.State.ProjectAnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree); Assert.Equal(appliedToDocument, documentOptionsViaSyntaxTree.TryGetValue("indent_style", out var value) == true && value == "tab"); var projectOptions = document.Project.GetAnalyzerConfigOptions(); - Assert.Equal(appliedToEntireProject, projectOptions?.ConfigOptions.TryGetValue("indent_style", out value) == true && value == "tab"); + Assert.Equal(appliedToEntireProject, projectOptions?.ConfigOptionsWithoutFallback.TryGetValue("indent_style", out value) == true && value == "tab"); + Assert.Equal(appliedToEntireProject, projectOptions?.ConfigOptionsWithFallback.TryGetValue("indent_style", out value) == true && value == "tab"); } [Fact] diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs index f6aaf5c220842..dcfca32e3e4d1 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs @@ -111,7 +111,8 @@ public static Task GetDiagnosticsAsync( Project project, Checksum solutionChecksum, TextSpan? span, - IEnumerable analyzerIds, + ImmutableArray projectAnalyzerIds, + ImmutableArray hostAnalyzerIds, AnalysisKind? analysisKind, DiagnosticAnalyzerInfoCache analyzerInfoCache, HostWorkspaceServices hostWorkspaceServices, @@ -145,12 +146,13 @@ public static Task GetDiagnosticsAsync( // from clients such as editor diagnostic tagger to show squiggles, background analysis to populate the error list, etc. var diagnosticsComputer = new DiagnosticComputer(document, project, solutionChecksum, span, analysisKind, analyzerInfoCache, hostWorkspaceServices); return isExplicit - ? diagnosticsComputer.GetHighPriorityDiagnosticsAsync(analyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken) - : diagnosticsComputer.GetNormalPriorityDiagnosticsAsync(analyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken); + ? diagnosticsComputer.GetHighPriorityDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken) + : diagnosticsComputer.GetNormalPriorityDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken); } private async Task GetHighPriorityDiagnosticsAsync( - IEnumerable analyzerIds, + ImmutableArray projectAnalyzerIds, + ImmutableArray hostAnalyzerIds, bool reportSuppressedDiagnostics, bool logPerformanceInfo, bool getTelemetryInfo, @@ -160,7 +162,7 @@ private async Task GetHighPriorityDiagnos // Step 1: // - Create the core 'computeTask' for computing diagnostics. - var computeTask = GetDiagnosticsAsync(analyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken); + var computeTask = GetDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken); // Step 2: // - Add this computeTask to the set of currently executing high priority tasks. @@ -224,7 +226,8 @@ static void CancelNormalPriorityTasks() } private async Task GetNormalPriorityDiagnosticsAsync( - IEnumerable analyzerIds, + ImmutableArray projectAnalyzerIds, + ImmutableArray hostAnalyzerIds, bool reportSuppressedDiagnostics, bool logPerformanceInfo, bool getTelemetryInfo, @@ -254,7 +257,7 @@ private async Task GetNormalPriorityDiagn { // Step 3: // - Execute the core compute task for diagnostic computation. - return await GetDiagnosticsAsync(analyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, + return await GetDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationTokenSource.Token) @@ -315,7 +318,8 @@ static async Task WaitForHighPriorityTasksAsync(CancellationToken cancellationTo } private async Task GetDiagnosticsAsync( - IEnumerable analyzerIds, + ImmutableArray projectAnalyzerIds, + ImmutableArray hostAnalyzerIds, bool reportSuppressedDiagnostics, bool logPerformanceInfo, bool getTelemetryInfo, @@ -327,28 +331,46 @@ private async Task GetDiagnosticsAsync( return SerializableDiagnosticAnalysisResults.Empty; } - var analyzers = GetAnalyzers(analyzerToIdMap, analyzerIds); - if (analyzers.IsEmpty) + var (projectAnalyzers, hostAnalyzers) = GetAnalyzers(analyzerToIdMap, projectAnalyzerIds, hostAnalyzerIds); + if (projectAnalyzers.IsEmpty && hostAnalyzers.IsEmpty) { return SerializableDiagnosticAnalysisResults.Empty; } - if (_document == null && analyzers.Length < compilationWithAnalyzers.Analyzers.Length) + if (_document == null) { - // PERF: Generate a new CompilationWithAnalyzers with trimmed analyzers for non-document analysis case. - compilationWithAnalyzers = compilationWithAnalyzers.Compilation.WithAnalyzers(analyzers, compilationWithAnalyzers.AnalysisOptions); + if (projectAnalyzers.Length < compilationWithAnalyzers.ProjectAnalyzers.Length) + { + Contract.ThrowIfFalse(projectAnalyzers.Length > 0 || compilationWithAnalyzers.HostCompilationWithAnalyzers is not null); + + // PERF: Generate a new CompilationWithAnalyzers with trimmed analyzers for non-document analysis case. + compilationWithAnalyzers = new CompilationWithAnalyzersPair( + projectAnalyzers.Any() ? compilationWithAnalyzers.ProjectCompilation!.WithAnalyzers(projectAnalyzers, compilationWithAnalyzers.ProjectCompilationWithAnalyzers!.AnalysisOptions) : null, + compilationWithAnalyzers.HostCompilationWithAnalyzers); + } + + if (hostAnalyzers.Length < compilationWithAnalyzers.HostAnalyzers.Length) + { + Contract.ThrowIfFalse(hostAnalyzers.Length > 0 || compilationWithAnalyzers.ProjectCompilationWithAnalyzers is not null); + + // PERF: Generate a new CompilationWithAnalyzers with trimmed analyzers for non-document analysis case. + compilationWithAnalyzers = new CompilationWithAnalyzersPair( + compilationWithAnalyzers.ProjectCompilationWithAnalyzers, + hostAnalyzers.Any() ? compilationWithAnalyzers.HostCompilation!.WithAnalyzers(hostAnalyzers, compilationWithAnalyzers.HostCompilationWithAnalyzers!.AnalysisOptions) : null); + } } var skippedAnalyzersInfo = _project.GetSkippedAnalyzersInfo(_analyzerInfoCache); - return await AnalyzeAsync(compilationWithAnalyzers, analyzerToIdMap, analyzers, skippedAnalyzersInfo, + return await AnalyzeAsync(compilationWithAnalyzers, analyzerToIdMap, projectAnalyzers, hostAnalyzers, skippedAnalyzersInfo, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken).ConfigureAwait(false); } private async Task AnalyzeAsync( - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzersPair compilationWithAnalyzers, BidirectionalMap analyzerToIdMap, - ImmutableArray analyzers, + ImmutableArray projectAnalyzers, + ImmutableArray hostAnalyzers, SkippedHostAnalyzersInfo skippedAnalyzersInfo, bool reportSuppressedDiagnostics, bool logPerformanceInfo, @@ -356,10 +378,10 @@ private async Task AnalyzeAsync( CancellationToken cancellationToken) { var documentAnalysisScope = _document != null - ? new DocumentAnalysisScope(_document, _span, analyzers, _analysisKind!.Value) + ? new DocumentAnalysisScope(_document, _span, projectAnalyzers, hostAnalyzers, _analysisKind!.Value) : null; - var (analysisResult, additionalPragmaSuppressionDiagnostics) = await compilationWithAnalyzers.GetAnalysisResultAsync( + var (projectAnalysisResult, hostAnalysisResult, additionalPragmaSuppressionDiagnostics) = await compilationWithAnalyzers.GetAnalysisResultAsync( documentAnalysisScope, _project, _analyzerInfoCache, cancellationToken).ConfigureAwait(false); if (logPerformanceInfo && _performanceTracker != null) @@ -374,17 +396,31 @@ private async Task AnalyzeAsync( if (documentAnalysisScope == null) unitCount += _project.DocumentIds.Count; - _performanceTracker.AddSnapshot(analysisResult.AnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(_analyzerInfoCache), unitCount, forSpanAnalysis: _span.HasValue); + var projectPerformanceInfo = projectAnalysisResult?.AnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(_analyzerInfoCache) ?? []; + var hostPerformanceInfo = hostAnalysisResult?.AnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(_analyzerInfoCache) ?? []; + _performanceTracker.AddSnapshot(projectPerformanceInfo.Concat(hostPerformanceInfo), unitCount, forSpanAnalysis: _span.HasValue); } } - var builderMap = await analysisResult.ToResultBuilderMapAsync( - additionalPragmaSuppressionDiagnostics, documentAnalysisScope, - _project, VersionStamp.Default, compilationWithAnalyzers.Compilation, - analyzers, skippedAnalyzersInfo, reportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + var builderMap = ImmutableDictionary.Empty; + if (projectAnalysisResult is not null) + { + builderMap = builderMap.AddRange(await projectAnalysisResult.ToResultBuilderMapAsync( + additionalPragmaSuppressionDiagnostics, documentAnalysisScope, + _project, VersionStamp.Default, compilationWithAnalyzers.ProjectCompilation!, + projectAnalyzers, skippedAnalyzersInfo, reportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false)); + } + + if (hostAnalysisResult is not null) + { + builderMap = builderMap.AddRange(await hostAnalysisResult.ToResultBuilderMapAsync( + additionalPragmaSuppressionDiagnostics, documentAnalysisScope, + _project, VersionStamp.Default, compilationWithAnalyzers.HostCompilation!, + hostAnalyzers, skippedAnalyzersInfo, reportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false)); + } var telemetry = getTelemetryInfo - ? GetTelemetryInfo(analysisResult, analyzers, analyzerToIdMap) + ? GetTelemetryInfo(projectAnalysisResult, hostAnalysisResult, projectAnalyzers, hostAnalyzers, analyzerToIdMap) : []; return new SerializableDiagnosticAnalysisResults(Dehydrate(builderMap, analyzerToIdMap), telemetry); @@ -412,16 +448,20 @@ private async Task AnalyzeAsync( } private static ImmutableArray<(string analyzerId, AnalyzerTelemetryInfo)> GetTelemetryInfo( - AnalysisResult analysisResult, - ImmutableArray analyzers, + AnalysisResult? projectAnalysisResult, + AnalysisResult? hostAnalysisResult, + ImmutableArray projectAnalyzers, + ImmutableArray hostAnalyzers, BidirectionalMap analyzerToIdMap) { Func shouldInclude; - if (analyzers.Length < analysisResult.AnalyzerTelemetryInfo.Count) + if (projectAnalyzers.Length < (projectAnalysisResult?.AnalyzerTelemetryInfo.Count ?? 0) + || hostAnalyzers.Length < (hostAnalysisResult?.AnalyzerTelemetryInfo.Count ?? 0)) { // Filter the telemetry info to the executed analyzers. using var _1 = PooledHashSet.GetInstance(out var analyzersSet); - analyzersSet.AddRange(analyzers); + analyzersSet.AddRange(projectAnalyzers); + analyzersSet.AddRange(hostAnalyzers); shouldInclude = analyzer => analyzersSet.Contains(analyzer); } @@ -431,12 +471,27 @@ private async Task AnalyzeAsync( } using var _2 = ArrayBuilder<(string analyzerId, AnalyzerTelemetryInfo)>.GetInstance(out var telemetryBuilder); - foreach (var (analyzer, analyzerTelemetry) in analysisResult.AnalyzerTelemetryInfo) + if (projectAnalysisResult is not null) + { + foreach (var (analyzer, analyzerTelemetry) in projectAnalysisResult.AnalyzerTelemetryInfo) + { + if (shouldInclude(analyzer)) + { + var analyzerId = GetAnalyzerId(analyzerToIdMap, analyzer); + telemetryBuilder.Add((analyzerId, analyzerTelemetry)); + } + } + } + + if (hostAnalysisResult is not null) { - if (shouldInclude(analyzer)) + foreach (var (analyzer, analyzerTelemetry) in hostAnalysisResult.AnalyzerTelemetryInfo) { - var analyzerId = GetAnalyzerId(analyzerToIdMap, analyzer); - telemetryBuilder.Add((analyzerId, analyzerTelemetry)); + if (shouldInclude(analyzer)) + { + var analyzerId = GetAnalyzerId(analyzerToIdMap, analyzer); + telemetryBuilder.Add((analyzerId, analyzerTelemetry)); + } } } @@ -451,23 +506,32 @@ private static string GetAnalyzerId(BidirectionalMap return analyzerId; } - private static ImmutableArray GetAnalyzers(BidirectionalMap analyzerMap, IEnumerable analyzerIds) + private static (ImmutableArray projectAnalyzers, ImmutableArray hostAnalyzers) GetAnalyzers(BidirectionalMap analyzerMap, ImmutableArray projectAnalyzerIds, ImmutableArray hostAnalyzerIds) { // TODO: this probably need to be cached as well in analyzer service? - var builder = ImmutableArray.CreateBuilder(); + var projectBuilder = ImmutableArray.CreateBuilder(); + var hostBuilder = ImmutableArray.CreateBuilder(); - foreach (var analyzerId in analyzerIds) + foreach (var analyzerId in projectAnalyzerIds) { if (analyzerMap.TryGetValue(analyzerId, out var analyzer)) { - builder.Add(analyzer); + projectBuilder.Add(analyzer); } } - return builder.ToImmutableAndClear(); + foreach (var analyzerId in hostAnalyzerIds) + { + if (analyzerMap.TryGetValue(analyzerId, out var analyzer)) + { + hostBuilder.Add(analyzer); + } + } + + return (projectBuilder.ToImmutableAndClear(), hostBuilder.ToImmutableAndClear()); } - private async Task<(CompilationWithAnalyzers? compilationWithAnalyzers, BidirectionalMap analyzerToIdMap)> GetOrCreateCompilationWithAnalyzersAsync(CancellationToken cancellationToken) + private async Task<(CompilationWithAnalyzersPair? compilationWithAnalyzers, BidirectionalMap analyzerToIdMap)> GetOrCreateCompilationWithAnalyzersAsync(CancellationToken cancellationToken) { var cacheEntry = await GetOrCreateCacheEntryAsync().ConfigureAwait(false); return (cacheEntry.CompilationWithAnalyzers, cacheEntry.AnalyzerToIdMap); @@ -509,8 +573,9 @@ private async Task CreateCompilationWithAnal var analyzerMapBuilder = pooledMap.Object; // This follows what we do in DiagnosticAnalyzerInfoCache.CheckAnalyzerReferenceIdentity - using var _ = ArrayBuilder.GetInstance(out var analyzerBuilder); - foreach (var reference in _project.Solution.AnalyzerReferences.Concat(_project.AnalyzerReferences)) + using var _1 = ArrayBuilder.GetInstance(out var projectAnalyzerBuilder); + using var _2 = ArrayBuilder.GetInstance(out var hostAnalyzerBuilder); + foreach (var reference in _project.Solution.AnalyzerReferences) { if (!referenceSet.Add(reference.Id)) { @@ -518,21 +583,35 @@ private async Task CreateCompilationWithAnal } var analyzers = reference.GetAnalyzers(_project.Language); - analyzerBuilder.AddRange(analyzers); + hostAnalyzerBuilder.AddRange(analyzers); analyzerMapBuilder.AppendAnalyzerMap(analyzers); } - var compilationWithAnalyzers = analyzerBuilder.Count > 0 - ? await CreateCompilationWithAnalyzerAsync(analyzerBuilder.ToImmutable(), cancellationToken).ConfigureAwait(false) + // Evaluate project analyzers after host analyzers to ensure duplicates in analyzerMapBuilder are + // overwritten with project analyzers if/when applicable. + foreach (var reference in _project.AnalyzerReferences) + { + if (!referenceSet.Add(reference.Id)) + { + continue; + } + + var analyzers = reference.GetAnalyzers(_project.Language); + projectAnalyzerBuilder.AddRange(analyzers); + analyzerMapBuilder.AppendAnalyzerMap(analyzers); + } + + var compilationWithAnalyzers = projectAnalyzerBuilder.Count > 0 || hostAnalyzerBuilder.Count > 0 + ? await CreateCompilationWithAnalyzerAsync(projectAnalyzerBuilder.ToImmutable(), hostAnalyzerBuilder.ToImmutable(), cancellationToken).ConfigureAwait(false) : null; var analyzerToIdMap = new BidirectionalMap(analyzerMapBuilder); return new CompilationWithAnalyzersCacheEntry(_solutionChecksum, _project, compilationWithAnalyzers, analyzerToIdMap); } - private async Task CreateCompilationWithAnalyzerAsync(ImmutableArray analyzers, CancellationToken cancellationToken) + private async Task CreateCompilationWithAnalyzerAsync(ImmutableArray projectAnalyzers, ImmutableArray hostAnalyzers, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(!analyzers.IsEmpty); + Contract.ThrowIfFalse(!projectAnalyzers.IsEmpty || !hostAnalyzers.IsEmpty); // Always run analyzers concurrently in OOP const bool concurrentAnalysis = true; @@ -549,25 +628,34 @@ private async Task CreateCompilationWithAnalyzerAsync( // This allows all client requests with or without performance data and/or suppressed diagnostics to be satisfied. // TODO: can we support analyzerExceptionFilter in remote host? // right now, host doesn't support watson, we might try to use new NonFatal watson API? - var analyzerOptions = new CompilationWithAnalyzersOptions( + var projectAnalyzerOptions = new CompilationWithAnalyzersOptions( options: _project.AnalyzerOptions, onAnalyzerException: null, analyzerExceptionFilter: null, concurrentAnalysis: concurrentAnalysis, logAnalyzerExecutionTime: true, reportSuppressedDiagnostics: true); + var hostAnalyzerOptions = new CompilationWithAnalyzersOptions( + options: _project.HostAnalyzerOptions, + onAnalyzerException: null, + analyzerExceptionFilter: null, + concurrentAnalysis: concurrentAnalysis, + logAnalyzerExecutionTime: true, + reportSuppressedDiagnostics: true); - return compilation.WithAnalyzers(analyzers, analyzerOptions); + return new CompilationWithAnalyzersPair( + projectAnalyzers.Any() ? compilation.WithAnalyzers(projectAnalyzers, projectAnalyzerOptions) : null, + hostAnalyzers.Any() ? compilation.WithAnalyzers(hostAnalyzers, hostAnalyzerOptions) : null); } private sealed class CompilationWithAnalyzersCacheEntry { public Checksum SolutionChecksum { get; } public Project Project { get; } - public CompilationWithAnalyzers? CompilationWithAnalyzers { get; } + public CompilationWithAnalyzersPair? CompilationWithAnalyzers { get; } public BidirectionalMap AnalyzerToIdMap { get; } - public CompilationWithAnalyzersCacheEntry(Checksum solutionChecksum, Project project, CompilationWithAnalyzers? compilationWithAnalyzers, BidirectionalMap analyzerToIdMap) + public CompilationWithAnalyzersCacheEntry(Checksum solutionChecksum, Project project, CompilationWithAnalyzersPair? compilationWithAnalyzers, BidirectionalMap analyzerToIdMap) { SolutionChecksum = solutionChecksum; Project = project; diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs index 3d2715001f177..71c1414b5690c 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs @@ -64,7 +64,7 @@ public async ValueTask CalculateDiagnosti var result = await DiagnosticComputer.GetDiagnosticsAsync( document, project, solutionChecksum, documentSpan, - arguments.AnalyzerIds, documentAnalysisKind, + arguments.ProjectAnalyzerIds, arguments.HostAnalyzerIds, documentAnalysisKind, _analyzerInfoCache, hostWorkspaceServices, isExplicit: arguments.IsExplicit, reportSuppressedDiagnostics: arguments.ReportSuppressedDiagnostics, diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs index 745de21e1831f..12e85c717daac 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs @@ -22,7 +22,7 @@ internal interface IPragmaSuppressionsAnalyzer Task AnalyzeAsync( SemanticModel semanticModel, TextSpan? span, - CompilationWithAnalyzers compilationWithAnalyzers, + CompilationWithAnalyzers hostCompilationWithAnalyzers, Func> getSupportedDiagnostics, Action reportDiagnostic, CancellationToken cancellationToken); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs index b3a38f2bdf1a1..0fc7f4c3b746a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs @@ -228,6 +228,9 @@ public static IEnumerable GetLinkedDocuments(this Document document) public static async ValueTask GetHostAnalyzerConfigOptionsAsync(this Document document, CancellationToken cancellationToken) { var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + + // Code style layer (which is always NuGet-installed) does not use host options, but we keep the method name to + // reduce the number of instances where code needs to be conditionally included. return document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree).GetOptionsReader(); } #endif