Skip to content

Remove CompilationPair abstraction in diagnostic analyzer subsystem#80141

Merged
CyrusNajmabadi merged 15 commits intodotnet:mainfrom
CyrusNajmabadi:noCompilationPairWithApiChange
Sep 16, 2025
Merged

Remove CompilationPair abstraction in diagnostic analyzer subsystem#80141
CyrusNajmabadi merged 15 commits intodotnet:mainfrom
CyrusNajmabadi:noCompilationPairWithApiChange

Conversation

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Sep 4, 2025

@dotnet-policy-service dotnet-policy-service bot added VSCode Needs API Review Needs to be reviewed by the API review council labels Sep 4, 2025

namespace A
{
using System;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is the correct behavior. The test here is testing " 'usings' should be INSIDE the namespace, and it is an ERROR otherwise". So it was a bug this wasn't moving in in CodeCleanup.

Note: this differs from teh tests taht validate that nothing change when the option suggestions the change, but the severity is set to 'hidden'. in that case we do not make any change.

Copy link
Member

@jasonmalinowski jasonmalinowski Sep 12, 2025

Choose a reason for hiding this comment

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

Do you have a sense why this changed? (I agree the behavior looks right, but this means we had a bug somewhere...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When i was debugging through, teh wrong options were coming through, and it meant that the default severity was getting pulled in (which is 'hidden'). Because of this, the codecleanup layer doesn't actually want to make a change due to 'hidden' effectively being equivalent to 'the user didn't actually set anything themselves'.

@CyrusNajmabadi
Copy link
Contributor Author

Draft until API is reviewed and approved.

@CyrusNajmabadi CyrusNajmabadi changed the title No compilation pair with api change Remove CompilationPair abstraction in diagnostic analyzer subsystem (with api change) Sep 4, 2025
{
// No specific options factory. Can use the shared context.
if (_analyzerToCachedOptions is null)
return context;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: this is the common case for those not referencing hte roslyn sdk. So there should be almost no overhead in that common case.

internal SymbolAnalysisContext WithOptions(AnalyzerOptions options)
=> this.Options == options
? this
: new(this.Symbol, this.Compilation, options, _reportDiagnostic, _isSupportedDiagnostic, this.IsGeneratedCode, this.FilterTree, this.FilterSpan, this.CancellationToken);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: these context types are structs. So this is not actually allocating a new instance on the heap. Just returning a tweaked version with the right options to pass along.

/// from the shared instance provided in <see cref="Options"/>. If <see langword="null"/> then <see cref="Options"/>
/// will be used for all analyzers.
/// </summary>
public Func<DiagnosticAnalyzer, AnalyzerOptions>? AnalyzerSpecificOptionsFactory
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: this prop doesn't need to be public (though that matches the reest here). The API review covers if we should expose this (since it was a constructor param) or if we should keep it internally since we hvve no actual public consumers.

bool logAnalyzerExecutionTime,
bool reportSuppressedDiagnostics,
Func<Exception, bool>? analyzerExceptionFilter,
Func<DiagnosticAnalyzer, AnalyzerOptions>? analyzerSpecificOptionsFactory)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

new public API that needs API review.

analyzerExceptionFilter: exceptionFilter,
analyzerSpecificOptionsFactory));

(AnalyzerOptions sharedOptions, Func<DiagnosticAnalyzer, AnalyzerOptions>? analyzerSpecificOptionsFactory) GetOptions()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is hte meat of the change on the IDE side.

var map = new Dictionary<DiagnosticAnalyzer, AnalyzerOptions>(ReferenceEqualityComparer.Instance);

// Deduping map for the distinct AnalyzerConfigOptionsProvider we get back from analyzerSpecificOptionsFactory
var optionsProviderToOptions = new Dictionary<AnalyzerConfigOptionsProvider, AnalyzerOptions>(ReferenceEqualityComparer.Instance);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: this deduping is tested. we make sure that if two analyzers return the same AnalyzerConfigOptionsProvider that they will then get passed the exact same AnalyzerOptions instance in callbacks.

@CyrusNajmabadi
Copy link
Contributor Author

API change was approved. @jasonmalinowski @dotnet/roslyn-compiler for eyes.

diagnostics.Verify(Diagnostic("ID0001", "B").WithLocation(1, 8));
}

private sealed class OptionsOverrideDiagnosticAnalyzer(AnalyzerConfigOptionsProvider customOptions) : DiagnosticAnalyzer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

these tests validate that we see the different options through all the callbacks.

Microsoft.CodeAnalysis.CommandLineResource.LinkedResourceFileName.get -> string?
Microsoft.CodeAnalysis.CommandLineResource.ResourceName.get -> string!
Microsoft.CodeAnalysis.Compilation.EmitDifference(Microsoft.CodeAnalysis.Emit.EmitBaseline! baseline, System.Collections.Generic.IEnumerable<Microsoft.CodeAnalysis.Emit.SemanticEdit>! edits, System.Func<Microsoft.CodeAnalysis.ISymbol!, bool>! isAddedSymbol, System.IO.Stream! metadataStream, System.IO.Stream! ilStream, System.IO.Stream! pdbStream, Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.Emit.EmitDifferenceResult!
Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.CompilationWithAnalyzersOptions(Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions? options, System.Action<System.Exception!, Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer!, Microsoft.CodeAnalysis.Diagnostic!>? onAnalyzerException, bool concurrentAnalysis, bool logAnalyzerExecutionTime, bool reportSuppressedDiagnostics, System.Func<System.Exception!, bool>? analyzerExceptionFilter, System.Func<Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer!, Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider!>? getAnalyzerConfigOptionsProvider) -> void
Copy link
Contributor Author

Choose a reason for hiding this comment

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

only public change is this one constructor.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as draft September 9, 2025 21:11
@CyrusNajmabadi CyrusNajmabadi removed request for a team September 12, 2025 22:06
Copy link
Member

Choose a reason for hiding this comment

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

I assume you'll be renaming the file as a follow-up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup. I'm even working on making the "make filename match type" fixer able to fix-all. So i can do these in bulk.

CancellationToken cancellationToken)
{
var hostAnalyzers = documentAnalysisScope?.HostAnalyzers ?? compilationWithAnalyzers.HostAnalyzers;
var hostAnalyzers = documentAnalysisScope?.Analyzers ?? compilationWithAnalyzers.Analyzers;
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if local needs a rename, because it's suspicious it was 'hostAnalyzers' before?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will do. i agree it's suspicious. can you think of a reason we'd only be using hostanalyzers for suppression analyzers? @mavasani do you perhaps knows?

i'm tempted to leave this as is (with the rename). but if there's a sound reason for only using host analyzers, we could always filter these down to those.

Unfortunately, we don't have any tests indicating if this is intentional or errant.


namespace A
{
using System;
Copy link
Member

@jasonmalinowski jasonmalinowski Sep 12, 2025

Choose a reason for hiding this comment

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

Do you have a sense why this changed? (I agree the behavior looks right, but this means we had a bug somewhere...)

Copy link
Contributor Author

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

comment

RoslynDebug.Assert(AnalysisScope.TextDocument is Document);
Contract.ThrowIfFalse(analyzer.IsCompilerAnalyzer());
Contract.ThrowIfNull(_compilationWithAnalyzers);
Contract.ThrowIfFalse(_compilationBasedAnalyzersInAnalysisScope.Contains(analyzer));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we even already had this assert here. so it massively simplifies the code below.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

I sign off; obviously that question is still pending but I'm not sure I can figure that one out...

filteredHostAnalyzers = filteredHostAnalyzers.AddRange(filteredProjectSuppressors);
// Ensure we filter out DocumentDiagnosticAnalyzers (they're used to get diagnostics, without involving a
// compilation), and also ensure the list has no duplicates.
analyzers = analyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer()).Distinct();
Copy link
Contributor

@ToddGrun ToddGrun Sep 13, 2025

Choose a reason for hiding this comment

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

Distinct

Huh, I would have thought the IA.Distinct extension method would have used a PooledHashSet (in the default comparer case)

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

Labels

Area-Analyzers Needs API Review Needs to be reviewed by the API review council VSCode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants