|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.PopulateSwitch;
internal abstract class AbstractPopulateSwitchCodeFixProvider<
TSwitchOperation,
TSwitchSyntax,
TSwitchArmSyntax,
TMemberAccessExpression>
: SyntaxEditorBasedCodeFixProvider
where TSwitchOperation : IOperation
where TSwitchSyntax : SyntaxNode
where TSwitchArmSyntax : SyntaxNode
where TMemberAccessExpression : SyntaxNode
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; }
protected AbstractPopulateSwitchCodeFixProvider(string diagnosticId)
=> FixableDiagnosticIds = [diagnosticId];
protected abstract ITypeSymbol GetSwitchType(TSwitchOperation switchStatement);
protected abstract ICollection<ISymbol> GetMissingEnumMembers(TSwitchOperation switchOperation);
protected abstract bool HasNullSwitchArm(TSwitchOperation switchOperation);
protected abstract TSwitchArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, TMemberAccessExpression caseLabel);
protected abstract TSwitchArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation);
protected abstract TSwitchArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation);
protected abstract int InsertPosition(TSwitchOperation switchOperation);
protected abstract TSwitchSyntax InsertSwitchArms(SyntaxGenerator generator, TSwitchSyntax switchNode, int insertLocation, List<TSwitchArmSyntax> newArms);
protected abstract void FixOneDiagnostic(
Document document, SyntaxEditor editor, SemanticModel semanticModel,
bool addCases, bool addDefaultCase, bool onlyOneDiagnostic,
bool hasMissingCases, bool hasMissingDefaultCase,
TSwitchSyntax switchNode, TSwitchOperation switchOperation);
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
var properties = diagnostic.Properties;
var missingCases = bool.Parse(properties[PopulateSwitchStatementHelpers.MissingCases]!);
var missingDefaultCase = bool.Parse(properties[PopulateSwitchStatementHelpers.MissingDefaultCase]!);
Debug.Assert(missingCases || missingDefaultCase);
var document = context.Document;
if (missingCases)
{
context.RegisterCodeFix(
CodeAction.Create(
AnalyzersResources.Add_missing_cases,
c => FixAsync(document, diagnostic,
addCases: true, addDefaultCase: false,
cancellationToken: c),
nameof(AnalyzersResources.Add_missing_cases)),
context.Diagnostics);
}
if (missingDefaultCase)
{
context.RegisterCodeFix(
CodeAction.Create(
CodeFixesResources.Add_default_case,
c => FixAsync(document, diagnostic,
addCases: false, addDefaultCase: true,
cancellationToken: c),
nameof(CodeFixesResources.Add_default_case)),
context.Diagnostics);
}
if (missingCases && missingDefaultCase)
{
context.RegisterCodeFix(
CodeAction.Create(
CodeFixesResources.Add_both,
c => FixAsync(document, diagnostic,
addCases: true, addDefaultCase: true,
cancellationToken: c),
nameof(CodeFixesResources.Add_both)),
context.Diagnostics);
}
return Task.CompletedTask;
}
private Task<Document> FixAsync(
Document document, Diagnostic diagnostic,
bool addCases, bool addDefaultCase,
CancellationToken cancellationToken)
{
return FixAllAsync(document, [diagnostic],
addCases, addDefaultCase, cancellationToken);
}
private Task<Document> FixAllAsync(
Document document, ImmutableArray<Diagnostic> diagnostics,
bool addCases, bool addDefaultCase,
CancellationToken cancellationToken)
{
return FixAllWithEditorAsync(document,
editor => FixWithEditorAsync(document, editor, diagnostics, addCases, addDefaultCase, cancellationToken),
cancellationToken);
}
private async Task FixWithEditorAsync(
Document document, SyntaxEditor editor, ImmutableArray<Diagnostic> diagnostics,
bool addCases, bool addDefaultCase,
CancellationToken cancellationToken)
{
foreach (var diagnostic in diagnostics)
{
await FixOneDiagnosticAsync(
document, editor, diagnostic, addCases, addDefaultCase,
diagnostics.Length == 1, cancellationToken).ConfigureAwait(false);
}
}
private async Task FixOneDiagnosticAsync(
Document document, SyntaxEditor editor, Diagnostic diagnostic,
bool addCases, bool addDefaultCase, bool onlyOneDiagnostic,
CancellationToken cancellationToken)
{
var hasMissingCases = bool.Parse(diagnostic.Properties[PopulateSwitchStatementHelpers.MissingCases]!);
var hasMissingDefaultCase = bool.Parse(diagnostic.Properties[PopulateSwitchStatementHelpers.MissingDefaultCase]!);
var switchLocation = diagnostic.AdditionalLocations[0];
var switchNode = switchLocation.FindNode(getInnermostNodeForTie: true, cancellationToken) as TSwitchSyntax;
if (switchNode == null)
return;
var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// https://github.com/dotnet/roslyn/issues/40505
var switchStatement = (TSwitchOperation)model.GetOperation(switchNode, cancellationToken)!;
FixOneDiagnostic(
document, editor, model, addCases, addDefaultCase, onlyOneDiagnostic,
hasMissingCases, hasMissingDefaultCase, switchNode, switchStatement);
}
protected TSwitchSyntax UpdateSwitchNode(
SyntaxEditor editor, SemanticModel semanticModel,
bool addCases, bool addDefaultCase,
bool hasMissingCases, bool hasMissingDefaultCase,
TSwitchSyntax switchNode, TSwitchOperation switchOperation)
{
var enumType = GetSwitchType(switchOperation);
var isNullable = false;
if (enumType.IsNullable(out var underlyingType))
{
isNullable = true;
enumType = underlyingType;
}
var generator = editor.Generator;
var newArms = new List<TSwitchArmSyntax>();
if (hasMissingCases && addCases)
{
var missingArms =
from e in GetMissingEnumMembers(switchOperation)
let caseLabel = (TMemberAccessExpression)generator.MemberAccessExpression(generator.TypeExpression(enumType), e.Name).WithAdditionalAnnotations(Simplifier.Annotation)
select CreateSwitchArm(generator, semanticModel.Compilation, caseLabel);
newArms.AddRange(missingArms);
if (isNullable && !HasNullSwitchArm(switchOperation))
newArms.Add(CreateNullSwitchArm(generator, semanticModel.Compilation));
}
if (hasMissingDefaultCase && addDefaultCase)
{
// Always add the default clause at the end.
newArms.Add(CreateDefaultSwitchArm(generator, semanticModel.Compilation));
}
var insertLocation = InsertPosition(switchOperation);
var newSwitchNode = InsertSwitchArms(generator, switchNode, insertLocation, newArms)
.WithAdditionalAnnotations(Formatter.Annotation);
return newSwitchNode;
}
protected static void AddMissingBraces(
Document document,
ref SyntaxNode root,
ref TSwitchSyntax switchNode)
{
// Parsing of the switch may have caused imbalanced braces. i.e. the switch
// may have consumed a brace that was intended for a higher level construct.
// So balance the tree first, then do the switch replacement.
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
syntaxFacts.AddFirstMissingCloseBrace(
root, switchNode, out var newRoot, out var newSwitchNode);
root = newRoot;
switchNode = newSwitchNode;
}
protected override Task FixAllAsync(
Document document,
ImmutableArray<Diagnostic> diagnostics,
SyntaxEditor editor,
CancellationToken cancellationToken)
{
// If the user is performing a fix-all, then fix up all the issues we see. i.e.
// add missing cases and missing 'default' cases for any switches we reported an
// issue on.
return FixWithEditorAsync(document, editor, diagnostics,
addCases: true, addDefaultCase: true,
cancellationToken: cancellationToken);
}
}
|