// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using System.Composition;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;
namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
public sealed class DefineAccessorsForAttributeArgumentsFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DefineAccessorsForAttributeArgumentsAnalyzer.RuleId);
// The rule reports three different problems and offers a different action for each, so the fix-all
// pass has to be told which one the user picked - DocumentBasedFixAllProvider hands over every
// diagnostic it collected without filtering by the equivalence key.
public override FixAllProvider GetFixAllProvider()
=> SyntaxEditorFixAllProvider.Create<string?>(
static fixAllContext => fixAllContext.CodeActionEquivalenceKey,
ApplyFixAsync);
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Document document = context.Document;
SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
SyntaxNode root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
foreach (Diagnostic diagnostic in context.Diagnostics)
{
SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan);
string? title = GetTitle(diagnostic);
// Offer nothing where the fix cannot reach the declaration the diagnostic named, rather than
// registering an action that produces an unchanged document.
if (title == null || GetNodeToFix(generator, node, diagnostic) == null)
{
continue;
}
ImmutableArray<Diagnostic> diagnostics = ImmutableArray.Create(diagnostic);
context.RegisterCodeFix(
CodeAction.Create(
title,
cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync(
document,
diagnostics,
(doc, d, editor, token) => ApplyFixAsync(doc, d, editor, title, token),
cancellationToken),
equivalenceKey: title),
diagnostic);
}
}
private static string? GetTitle(Diagnostic diagnostic)
{
if (!diagnostic.Properties.TryGetValue("case", out string? fixCase))
{
return null;
}
return fixCase switch
{
DefineAccessorsForAttributeArgumentsAnalyzer.AddAccessorCase => MicrosoftCodeQualityAnalyzersResources.CreatePropertyAccessorForParameter,
DefineAccessorsForAttributeArgumentsAnalyzer.MakePublicCase => MicrosoftCodeQualityAnalyzersResources.MakeGetterPublic,
DefineAccessorsForAttributeArgumentsAnalyzer.RemoveSetterCase => MicrosoftCodeQualityAnalyzersResources.MakeSetterNonPublic,
_ => null,
};
}
private static SyntaxNode? GetNodeToFix(SyntaxGenerator generator, SyntaxNode node, Diagnostic diagnostic)
{
if (!diagnostic.Properties.TryGetValue("case", out string? fixCase))
{
return null;
}
return fixCase switch
{
DefineAccessorsForAttributeArgumentsAnalyzer.AddAccessorCase => generator.GetDeclaration(node, DeclarationKind.Parameter),
DefineAccessorsForAttributeArgumentsAnalyzer.MakePublicCase => generator.GetDeclaration(node, DeclarationKind.Property),
DefineAccessorsForAttributeArgumentsAnalyzer.RemoveSetterCase => node,
_ => null,
};
}
private static async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken)
{
if (GetTitle(diagnostic) != equivalenceKey)
{
return;
}
SyntaxNode node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);
SyntaxNode? nodeToFix = GetNodeToFix(editor.Generator, node, diagnostic);
if (nodeToFix == null)
{
return;
}
switch (diagnostic.Properties["case"])
{
case DefineAccessorsForAttributeArgumentsAnalyzer.AddAccessorCase:
await AddAccessorAsync(document, nodeToFix, editor, cancellationToken).ConfigureAwait(false);
break;
case DefineAccessorsForAttributeArgumentsAnalyzer.MakePublicCase:
MakePublic(node, nodeToFix, editor);
break;
case DefineAccessorsForAttributeArgumentsAnalyzer.RemoveSetterCase:
editor.SetAccessibility(nodeToFix, Accessibility.Internal);
break;
}
}
private static async Task AddAccessorAsync(Document document, SyntaxNode parameter, SyntaxEditor editor, CancellationToken cancellationToken)
{
SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (model.GetDeclaredSymbol(parameter, cancellationToken) is not IParameterSymbol parameterSymbol)
{
return;
}
// Make the first character uppercase since we are generating a property.
string propName = char.ToUpper(parameterSymbol.Name[0], CultureInfo.InvariantCulture).ToString() + parameterSymbol.Name[1..];
INamedTypeSymbol typeSymbol = parameterSymbol.ContainingType;
ISymbol? propertySymbol = typeSymbol.GetMembers(propName).FirstOrDefault(m => m.Kind == SymbolKind.Property);
// Add a new property
if (propertySymbol == null)
{
// Add it to the declaration that has this parameter, since a partial type can be declared
// across several documents and the editor only edits this one.
SyntaxNode? typeDeclaration = editor.Generator.GetDeclaration(parameter, DeclarationKind.Class);
if (typeDeclaration is null)
{
return;
}
SyntaxNode newProperty = editor.Generator.PropertyDeclaration(propName,
editor.Generator.TypeExpression(parameterSymbol.Type),
Accessibility.Public,
DeclarationModifiers.ReadOnly);
editor.AddMember(typeDeclaration, newProperty);
}
else
{
SyntaxReference? reference = propertySymbol.DeclaringSyntaxReferences.FirstOrDefault(r => r.SyntaxTree == editor.OriginalRoot.SyntaxTree);
if (reference == null)
{
return;
}
SyntaxNode? propertyDeclaration = editor.Generator.GetDeclaration(await reference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false), DeclarationKind.Property);
if (propertyDeclaration is null)
{
return;
}
editor.SetGetAccessorStatements(propertyDeclaration, editor.Generator.DefaultMethodBody(model.Compilation));
editor.SetModifiers(propertyDeclaration, editor.Generator.GetModifiers(propertyDeclaration) - DeclarationModifiers.WriteOnly);
}
}
private static void MakePublic(SyntaxNode getMethod, SyntaxNode property, SyntaxEditor editor)
{
// Clear the accessibility on the getter.
editor.SetAccessibility(getMethod, Accessibility.NotApplicable);
// If the containing property is not public, make it so
Accessibility propertyAccessibility = editor.Generator.GetAccessibility(property);
if (propertyAccessibility != Accessibility.Public)
{
editor.SetAccessibility(property, Accessibility.Public);
// Having just made the property public, if it has a setter with no Accessibility set, then we've just made the setter public.
// Instead restore the setter's original accessibility so that we don't fire a violation with the generated code.
SyntaxNode? setter = editor.Generator.GetAccessor(property, DeclarationKind.SetAccessor);
if (setter is not null)
{
Accessibility setterAccessibility = editor.Generator.GetAccessibility(setter);
if (setterAccessibility == Accessibility.NotApplicable)
{
editor.SetAccessibility(setter, propertyAccessibility);
}
}
}
}
}
}