File: src\Analyzers\Core\CodeFixes\AddParameter\AddParameterService.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.GenerateMember.GenerateConstructor;
using Microsoft.CodeAnalysis.InitializeParameter;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.AddParameter;
 
internal static class AddParameterService
{
    private static readonly SyntaxAnnotation s_annotation = new();
 
    /// <summary>
    /// Checks if there are indications that there might be more than one declarations that need to be fixed.
    /// The check does not look-up if there are other declarations (this is done later in the CodeAction).
    /// </summary>
    public static bool HasCascadingDeclarations(IMethodSymbol method)
    {
        // Don't cascade constructors
        if (method.IsConstructor())
        {
            return false;
        }
 
        // Virtual methods of all kinds might have overrides somewhere else that need to be fixed.
        if (method.IsVirtual || method.IsOverride || method.IsAbstract)
        {
            return true;
        }
 
        // If interfaces are involved we will fix those too
        // Explicit interface implementations are easy to detect
        if (method.ExplicitInterfaceImplementations.Length > 0)
        {
            return true;
        }
 
        // For implicit interface implementations lets check if the characteristic of the method
        // allows it to implicit implement an interface member.
        if (method.DeclaredAccessibility != Accessibility.Public)
        {
            return false;
        }
 
        if (method.IsStatic)
        {
            return false;
        }
 
        // Now check if the method does implement an interface member
        if (method.ExplicitOrImplicitInterfaceImplementations().Length > 0)
        {
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    /// Adds a parameter to a method.
    /// </summary>
    /// <param name="newParameterIndex"><see langword="null"/> to add as the final parameter</param>
    /// <returns></returns>
    public static async Task<Solution> AddParameterAsync<TExpressionSyntax>(
        Document invocationDocument,
        IMethodSymbol method,
        ITypeSymbol newParameterType,
        RefKind refKind,
        ParameterName parameterName,
        Argument<TExpressionSyntax>? argument,
        int? newParameterIndex,
        bool fixAllReferences,
        CancellationToken cancellationToken)
        where TExpressionSyntax : SyntaxNode
    {
        var solution = invocationDocument.Project.Solution;
 
        var referencedSymbols = fixAllReferences
            ? await FindMethodDeclarationReferencesAsync(invocationDocument, method, cancellationToken).ConfigureAwait(false)
            : method.GetAllMethodSymbolsOfPartialParts();
 
        var anySymbolReferencesNotInSource = referencedSymbols.Any(static symbol => !symbol.IsFromSource());
        var locationsInSource = referencedSymbols.Where(symbol => symbol.IsFromSource());
 
        // Indexing Locations[0] is valid because IMethodSymbols have one location at most
        // and IsFromSource() tests if there is at least one location.
        var locationsByDocument = locationsInSource.ToLookup(
            declarationLocation => solution.GetRequiredDocument(declarationLocation.Locations[0].SourceTree!));
 
        foreach (var documentLookup in locationsByDocument)
        {
            var document = documentLookup.Key;
 
            // May not have syntax facts for a different language in CodeStyle layer.
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            if (syntaxFacts is null)
                continue;
 
            var semanticDocument = await SemanticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
            var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var editor = new SyntaxEditor(syntaxRoot, solution.Services);
            var generator = editor.Generator;
            foreach (var currentMethodToUpdate in documentLookup)
            {
                var methodNode = syntaxRoot.FindNode(currentMethodToUpdate.Locations[0].SourceSpan, getInnermostNodeForTie: true);
                var existingParameters = generator.GetParameters(methodNode);
                var insertionIndex = newParameterIndex ?? existingParameters.Count;
 
                // if the preceding parameter is optional, the new parameter must also be optional 
                // see also BC30202 and CS1737
                var parameterMustBeOptional = insertionIndex > 0 &&
                    syntaxFacts.GetDefaultOfParameter(existingParameters[insertionIndex - 1]) != null;
 
                var parameterSymbol = CreateParameterSymbol(
                    currentMethodToUpdate, newParameterType, refKind, parameterMustBeOptional, parameterName.BestNameForParameter);
 
                var argumentInitializer = parameterMustBeOptional ? generator.DefaultExpression(newParameterType) : null;
                var parameterDeclaration = generator
                    .ParameterDeclaration(parameterSymbol, argumentInitializer)
                    .WithAdditionalAnnotations(Formatter.Annotation, s_annotation);
 
                if (anySymbolReferencesNotInSource && currentMethodToUpdate == method)
                {
                    parameterDeclaration = parameterDeclaration.WithAdditionalAnnotations(
                        ConflictAnnotation.Create(CodeFixesResources.Related_method_signatures_found_in_metadata_will_not_be_updated));
                }
 
                if (method.MethodKind == MethodKind.ReducedExtension && insertionIndex < existingParameters.Count)
                    insertionIndex++;
 
                AddParameterEditor.AddParameter(
                    syntaxFacts, editor, methodNode, insertionIndex, parameterDeclaration, cancellationToken);
            }
 
            var newRoot = editor.GetChangedRoot();
            solution = solution.WithDocumentSyntaxRoot(document.Id, newRoot);
        }
 
        // Now that we've added the parameter to the method, see if we added to a constructor that we then want to
        // assign that parameter to a field/property to as well.
        solution = await AddConstructorAssignmentsAsync(solution).ConfigureAwait(false);
 
        return solution;
 
        async Task<Solution> AddConstructorAssignmentsAsync(Solution rewrittenSolution)
        {
            var finalSolution = await TryAddConstructorAssignmentsAsync(rewrittenSolution).ConfigureAwait(false);
            return finalSolution ?? rewrittenSolution;
        }
 
        async Task<Solution?> TryAddConstructorAssignmentsAsync(Solution rewrittenSolution)
        {
            // If we weren't adding a parameter to a constructor, we have nothing to do here.
            if (method.MethodKind != MethodKind.Constructor)
                return null;
 
            // If we didn't have an argument indicating what was passed to the constructor, then we have nothing to do.
            if (argument is null)
                return null;
 
            // Only want to do this if we updated a single constructor in a single document.
            var documentsUpdated = locationsByDocument.Select(g => g.Key).ToSet();
            if (documentsUpdated.Count != 1)
                return null;
 
            var documentId = documentsUpdated.Single().Id;
 
            // Now go find the constructor after the parameter was added to it.
            var rewrittenDocument = rewrittenSolution.GetRequiredDocument(documentId);
            var rewrittenSyntaxRoot = await rewrittenDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
            var parameterDeclaration = rewrittenSyntaxRoot.GetAnnotatedNodes(s_annotation).SingleOrDefault();
            if (parameterDeclaration is null)
                return null;
 
            var semanticDocument = await SemanticDocument.CreateAsync(rewrittenDocument, cancellationToken).ConfigureAwait(false);
            if (semanticDocument.SemanticModel.GetDeclaredSymbol(parameterDeclaration, cancellationToken) is not IParameterSymbol parameter)
                return null;
 
            if (parameter.ContainingSymbol is not IMethodSymbol { MethodKind: MethodKind.Constructor, DeclaringSyntaxReferences: [var reference] } constructor)
                return null;
 
            var (_, parameterToExistingMember, _, _) = await GenerateConstructorHelpers.GetParametersAsync(
                semanticDocument,
                constructor.ContainingType,
                [argument.Value],
                [parameter.Type],
                [new ParameterName(parameter.Name, isFixed: true)],
                cancellationToken).ConfigureAwait(false);
 
            var memberToAssignTo = parameterToExistingMember.FirstOrDefault().Value;
            if (memberToAssignTo is null)
                return null;
 
            var initializeParameterService = rewrittenDocument.GetRequiredLanguageService<IInitializeParameterService>();
            return await initializeParameterService.AddAssignmentAsync(
                rewrittenDocument, parameter, memberToAssignTo, cancellationToken).ConfigureAwait(false);
        }
    }
 
    private static async Task<ImmutableArray<IMethodSymbol>> FindMethodDeclarationReferencesAsync(
        Document invocationDocument, IMethodSymbol method, CancellationToken cancellationToken)
    {
        var referencedSymbols = await SymbolFinder.FindReferencesAsync(
            method, invocationDocument.Project.Solution, cancellationToken).ConfigureAwait(false);
 
        return referencedSymbols.Select(referencedSymbol => referencedSymbol.Definition)
                                .OfType<IMethodSymbol>()
                                .Distinct()
                                .ToImmutableArray();
    }
 
    private static IParameterSymbol CreateParameterSymbol(
        IMethodSymbol method,
        ITypeSymbol parameterType,
        RefKind refKind,
        bool isOptional,
        string argumentNameSuggestion)
    {
        var uniqueName = NameGenerator.EnsureUniqueness(argumentNameSuggestion, method.Parameters.Select(p => p.Name));
        var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol(
                attributes: default, refKind: refKind, isOptional: isOptional, isParams: false, type: parameterType, name: uniqueName);
        return newParameterSymbol;
    }
}