@@ -314,11 +314,17 @@ private async Task AppendFixesAsync(
314314
315315 var extensionManager = document . Project . Solution . Workspace . Services . GetService < IExtensionManager > ( ) ;
316316
317- // Run each CodeFixProvider to gather individual CodeFixes for reported diagnostics
318- // Ensure that each diagnostic only has a unique registered code action for any given equivalance key.
317+ // Run each CodeFixProvider to gather individual CodeFixes for reported diagnostics.
318+ // Ensure that no diagnostic has registered code actions from different code fix providers with same equivalance key.
319319 // This prevents duplicate registered code actions from NuGet and VSIX code fix providers.
320320 // See https://github.com/dotnet/roslyn/issues/18818 for details.
321321 var uniqueDiagosticToEquivalenceKeysMap = new Dictionary < Diagnostic , PooledHashSet < string ? > > ( ) ;
322+
323+ // NOTE: For backward compatibility, we allow multiple registered code actions from the same code fix provider
324+ // to have the same equivalence key. See https://github.com/dotnet/roslyn/issues/44553 for details.
325+ // To ensure this, we track the fixer that first registered a code action to fix a diagnostic with a specific equivalence key.
326+ var diagnosticAndEquivalenceKeyToFixersMap = new Dictionary < ( Diagnostic diagnostic , string ? equivalenceKey ) , CodeFixProvider > ( ) ;
327+
322328 try
323329 {
324330 foreach ( var fixer in allFixers . Distinct ( ) )
@@ -338,11 +344,13 @@ await AppendFixesOrConfigurationsAsync(
338344 {
339345 var primaryDiagnostic = dxs . First ( ) ;
340346 return GetCodeFixesAsync ( document , primaryDiagnostic . Location . SourceSpan , fixer , isBlocking ,
341- ImmutableArray . Create ( primaryDiagnostic ) , uniqueDiagosticToEquivalenceKeysMap , cancellationToken ) ;
347+ ImmutableArray . Create ( primaryDiagnostic ) , uniqueDiagosticToEquivalenceKeysMap ,
348+ diagnosticAndEquivalenceKeyToFixersMap , cancellationToken ) ;
342349 }
343350 else
344351 {
345- return GetCodeFixesAsync ( document , span , fixer , isBlocking , dxs , uniqueDiagosticToEquivalenceKeysMap , cancellationToken ) ;
352+ return GetCodeFixesAsync ( document , span , fixer , isBlocking , dxs ,
353+ uniqueDiagosticToEquivalenceKeysMap , diagnosticAndEquivalenceKeyToFixersMap , cancellationToken ) ;
346354 }
347355 }
348356 } ,
@@ -366,6 +374,7 @@ private async Task<ImmutableArray<CodeFix>> GetCodeFixesAsync(
366374 Document document , TextSpan span , CodeFixProvider fixer , bool isBlocking ,
367375 ImmutableArray < Diagnostic > diagnostics ,
368376 Dictionary < Diagnostic , PooledHashSet < string ? > > uniqueDiagosticToEquivalenceKeysMap ,
377+ Dictionary < ( Diagnostic diagnostic , string ? equivalenceKey ) , CodeFixProvider > diagnosticAndEquivalenceKeyToFixersMap ,
369378 CancellationToken cancellationToken )
370379 {
371380 using var fixesDisposer = ArrayBuilder < CodeFix > . GetInstance ( out var fixes ) ;
@@ -377,7 +386,8 @@ private async Task<ImmutableArray<CodeFix>> GetCodeFixesAsync(
377386 lock ( fixes )
378387 {
379388 // Filter out applicable diagnostics which already have a registered code action with same equivalence key.
380- applicableDiagnostics = FilterApplicableDiagnostics ( applicableDiagnostics , action . EquivalenceKey , uniqueDiagosticToEquivalenceKeysMap ) ;
389+ applicableDiagnostics = FilterApplicableDiagnostics ( applicableDiagnostics , action . EquivalenceKey ,
390+ fixer , uniqueDiagosticToEquivalenceKeysMap , diagnosticAndEquivalenceKeyToFixersMap ) ;
381391
382392 if ( ! applicableDiagnostics . IsEmpty )
383393 {
@@ -396,20 +406,33 @@ private async Task<ImmutableArray<CodeFix>> GetCodeFixesAsync(
396406 static ImmutableArray < Diagnostic > FilterApplicableDiagnostics (
397407 ImmutableArray < Diagnostic > applicableDiagnostics ,
398408 string ? equivalenceKey ,
399- Dictionary < Diagnostic , PooledHashSet < string ? > > uniqueDiagosticToEquivalenceKeysMap )
409+ CodeFixProvider fixer ,
410+ Dictionary < Diagnostic , PooledHashSet < string ? > > uniqueDiagosticToEquivalenceKeysMap ,
411+ Dictionary < ( Diagnostic diagnostic , string ? equivalenceKey ) , CodeFixProvider > diagnosticAndEquivalenceKeyToFixersMap )
400412 {
401413 using var disposer = ArrayBuilder < Diagnostic > . GetInstance ( out var newApplicableDiagnostics ) ;
402414 foreach ( var diagnostic in applicableDiagnostics )
403415 {
404416 if ( ! uniqueDiagosticToEquivalenceKeysMap . TryGetValue ( diagnostic , out var equivalenceKeys ) )
405417 {
418+ // First code action registered to fix this diagnostic with any equivalenceKey.
419+ // Record the equivalence key and the fixer that registered this action.
406420 equivalenceKeys = PooledHashSet < string ? > . GetInstance ( ) ;
407421 equivalenceKeys . Add ( equivalenceKey ) ;
408422 uniqueDiagosticToEquivalenceKeysMap [ diagnostic ] = equivalenceKeys ;
423+ diagnosticAndEquivalenceKeyToFixersMap . Add ( ( diagnostic , equivalenceKey ) , fixer ) ;
424+ }
425+ else if ( equivalenceKeys . Add ( equivalenceKey ) )
426+ {
427+ // First code action registered to fix this diagnostic with the given equivalenceKey.
428+ // Record the the fixer that registered this action.
429+ diagnosticAndEquivalenceKeyToFixersMap . Add ( ( diagnostic , equivalenceKey ) , fixer ) ;
409430 }
410- else if ( ! equivalenceKeys . Add ( equivalenceKey ) )
431+ else if ( diagnosticAndEquivalenceKeyToFixersMap [ ( diagnostic , equivalenceKey ) ] != fixer )
411432 {
412- // Diagnostic already has a registered code action with same equivalence key.
433+ // Diagnostic already has a registered code action with same equivalence key from a different fixer.
434+ // Note that we allow same fixer to register multiple such code actions with the same equivalence key
435+ // for backward compatibility. See https://github.com/dotnet/roslyn/issues/44553 for details.
413436 continue ;
414437 }
415438
0 commit comments