Skip to content

Conversation

@CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Aug 27, 2025

This entrypoint existed so that the "run code analysis" command could work. Specifically, because that is an explicit comamnd kicked off by the user for the exact purpose of analyzing everything, we needed a way to run analyzer and not have our default bail-out logic come into play. =

For example, if the user has set 'run analyzers on the active file' (the default) we clearly don't want that stopping us from getting analyzer diags when teh user explicitly kicks off 'run code analysis'.

This was always very clunky, especially since 'run code analysis' is a separate component that then drives the low level diagnostic analyzer service.

This PR changes things so that the core entrypoints into the diag analyzer service (in this case GetDiagnosticsForIds) is now flexible enough to just handle what 'run code analysis' needs. First, #80084 made it so that the caller can specify the documents they want diags for no matter what. Second, this PR makes it so that the caller has complete control if needed over which analyzers to run, avoiding the default impl which uses heuristics to filter out certain analyzers it doesn't think are necessary.

This is all with the goal of making IDiagAnalyzerService very simple and clear in terms of the semantics it provides and when and how to use its entrypoints.

@CyrusNajmabadi CyrusNajmabadi force-pushed the noForceAnalyze branch 2 times, most recently from 8f17d74 to 58f80cd Compare August 28, 2025 04:02
// document diagnostics and also does not add any delay for pulling workspace diagnostics.
_diagnosticAnalyzerService.RequestDiagnosticRefresh();

bool FilterAnalyzer(DiagnosticAnalyzer analyzer)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a move from teh diag analzyer service itself. i want to take it out of the job of figuring these things out.

using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Implementation.Suggestions;
using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bridging tests to new api by use of an extension method.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review August 29, 2025 17:49
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner August 29, 2025 17:49
@CyrusNajmabadi
Copy link
Member Author

@jasonmalinowski @dibarbet ptal.


internal static class IDiagnosticServiceExtensions
{
public static Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit test only extension for tests that want to validate the diags that CodeAnalysisDiagnosticAnalyzerService gets back. I will do a future pr that has these features literally just test that service instead of diving into one of its helper methosd.

private static Func<DiagnosticAnalyzer, bool> GetDiagnosticAnalyzerFilter(
Project project, DiagnosticAnalyzerInfoCache infoCache)
{
return analyzer =>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic is a move. but it allows CodeAnalysisDiagnosticAnalyzerService to be entire explicit about which analyzers it runs as that is specific to its scenario. the DiagAnalService doesn't need to care or think about it.

using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.SolutionCrawler;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only used for doc comments crefs.

void RequestDiagnosticRefresh();

/// <inheritdoc cref="IRemoteDiagnosticAnalyzerService.ForceAnalyzeProjectAsync"/>
Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this was the primary goal of this PR.

/// </summary>
/// <param name="additionalFilter">An additional filter that can accept or reject analyzers that the default
/// rules have accepted.</param>
Func<DiagnosticAnalyzer, bool> GetDefaultAnalyzerFilter(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codifying what this is, and making it something a caller can override if they do not want it.

var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
if (box.Value.checksum == checksum)
return box.Value.diagnosticAnalysisResults;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically we lose this capability. But that's fine by me. 'run code analysis' isn't a common op, and we're going to be computing diags anyways for documents even in the presence of this having been called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this sort of a subset logic might be generally useful for that question you asked earlier about why we're creating so many CompilationWithAnalyzers for different analyzer sets and whether we can be more generally reusing prior answers. But I agree on removing this code -- having an optimization for specifically after this gesture seems unnecessary.

/// <param name="additionalFilter">An additional filter that can accept or reject analyzers that the default
/// rules have accepted.</param>
Func<DiagnosticAnalyzer, bool> GetDefaultAnalyzerFilter(
Project project, ImmutableHashSet<string>? diagnosticIds, Func<DiagnosticAnalyzer, bool>? additionalFilter = null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does feel a little weird that we pass another filter to retrieve the default filter. Wondering if places that use the additionalFilter should instead be creating their own single filter (which may wrap a call to the default filter), instead of it being passed in?

It might make usages more clear that even though they are getting the default filter, they may not be using the default filter behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreeing here -- my assumption would have been that the additional filter was only used for ones this didn't answer on, but I guess the comment says it's the other way around.

Or maybe just have a pattern that this filter method returns a nullable boolean, and then have a Compose method somewhere that takes a bunch of filters, calls them in order, and has some default true/false to fallback to if nobody answers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrmm... i'll see if there's a better way to do this. Originally, i just returned the filtetr, and then the callers had to combine it with theirs.

In practice, the pattern was always teh same. So having this pattern just helped encode both the idea of:

  1. get the default rules
  2. get the default rules, augmented with some other set.

So with this, and the ability for someone to pass a callback to GetDiagnostics meant we could handle all 3 cases:

  1. get the default rules
  2. get the default rules, augmented with some other set.
  3. use an entirely custom set of rules

But yeah, i agree it's not necessarily stunning here...

// For most of analyzers, the number of diagnostic descriptors is small, so this should be cheap.
var descriptors = infoCache.GetDiagnosticDescriptors(analyzer);
var analyzerConfigOptions = project.GetAnalyzerConfigOptions();
return descriptors.Any(static (d, arg) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit is actually readable now, thanks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good. I'm glad!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants