AbstractAddParameterCodeFixProvider.cs 21.1 KB
Newer Older
C
CyrusNajmabadi 已提交
1 2 3 4
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
C
CyrusNajmabadi 已提交
5
using System.Collections.Immutable;
C
CyrusNajmabadi 已提交
6
using System.Linq;
7
using System.Threading;
C
CyrusNajmabadi 已提交
8
using System.Threading.Tasks;
9
using Microsoft.CodeAnalysis.CodeActions;
C
CyrusNajmabadi 已提交
10
using Microsoft.CodeAnalysis.CodeFixes;
11 12
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
13
using Microsoft.CodeAnalysis.Formatting;
14
using Microsoft.CodeAnalysis.LanguageServices;
T
Tomas Matousek 已提交
15
using Microsoft.CodeAnalysis.PooledObjects;
16 17 18
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
C
CyrusNajmabadi 已提交
19 20 21

namespace Microsoft.CodeAnalysis.AddParameter
{
22 23 24 25 26 27 28 29 30 31 32 33
    internal abstract class AbstractAddParameterCodeFixProvider<
        TArgumentSyntax,
        TAttributeArgumentSyntax,
        TArgumentListSyntax,
        TAttributeArgumentListSyntax,
        TInvocationExpressionSyntax,
        TObjectCreationExpressionSyntax> : CodeFixProvider
        where TArgumentSyntax : SyntaxNode
        where TArgumentListSyntax : SyntaxNode
        where TAttributeArgumentListSyntax : SyntaxNode
        where TInvocationExpressionSyntax : SyntaxNode
        where TObjectCreationExpressionSyntax : SyntaxNode
C
CyrusNajmabadi 已提交
34
    {
C
CyrusNajmabadi 已提交
35 36
        protected abstract ImmutableArray<string> TooManyArgumentsDiagnosticIds { get; }

37 38 39
        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var cancellationToken = context.CancellationToken;
C
CyrusNajmabadi 已提交
40
            var diagnostic = context.Diagnostics.First();
41 42 43 44

            var document = context.Document;
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

C
CyrusNajmabadi 已提交
45
            var initialNode = root.FindNode(diagnostic.Location.SourceSpan);
46 47

            for (var node = initialNode; node != null; node = node.Parent)
48 49 50
            {
                if (node is TObjectCreationExpressionSyntax objectCreation)
                {
C
CyrusNajmabadi 已提交
51
                    var argumentOpt = TryGetRelevantArgument(initialNode, node, diagnostic);
52
                    await HandleObjectCreationExpressionAsync(context, objectCreation, argumentOpt).ConfigureAwait(false);
53 54 55 56
                    return;
                }
                else if (node is TInvocationExpressionSyntax invocationExpression)
                {
C
CyrusNajmabadi 已提交
57
                    var argumentOpt = TryGetRelevantArgument(initialNode, node, diagnostic);
58
                    await HandleInvocationExpressionAsync(context, invocationExpression, argumentOpt).ConfigureAwait(false);
59 60 61 62 63
                    return;
                }
            }
        }

C
CyrusNajmabadi 已提交
64 65
        private TArgumentSyntax TryGetRelevantArgument(
            SyntaxNode initialNode, SyntaxNode node, Diagnostic diagnostic)
66
        {
C
CyrusNajmabadi 已提交
67 68 69 70 71
            if (this.TooManyArgumentsDiagnosticIds.Contains(diagnostic.Id))
            {
                return null;
            }

72
            return initialNode.GetAncestorsOrThis<TArgumentSyntax>()
73
                              .LastOrDefault(a => a.AncestorsAndSelf().Contains(node));
74 75 76 77
        }

        private Task HandleInvocationExpressionAsync(
            CodeFixContext context, TInvocationExpressionSyntax invocationExpression, TArgumentSyntax argumentOpt)
78
        {
C
CyrusNajmabadi 已提交
79
            // Currently we only support this for 'new obj' calls.
80 81 82 83 84
            return SpecializedTasks.EmptyTask;
        }

        private async Task HandleObjectCreationExpressionAsync(
            CodeFixContext context,
85 86
            TObjectCreationExpressionSyntax objectCreation,
            TArgumentSyntax argumentOpt)
87 88 89 90 91 92
        {
            var document = context.Document;
            var cancellationToken = context.CancellationToken;
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();

C
CyrusNajmabadi 已提交
93
            // Not supported if this is "new { ... }" (as there are no parameters at all.
94 95 96 97 98 99
            var typeNode = syntaxFacts.GetObjectCreationType(objectCreation);
            if (typeNode == null)
            {
                return;
            }

C
CyrusNajmabadi 已提交
100 101
            // If we can't figure out the type being created, or the type isn't in source,
            // then there's nothing we can do.
102
            var type = semanticModel.GetSymbolInfo(typeNode, cancellationToken).GetAnySymbol() as INamedTypeSymbol;
C
CyrusNajmabadi 已提交
103 104 105 106 107 108
            if (type == null)
            {
                return;
            }

            if (!type.IsNonImplicitAndFromSource())
109 110 111 112 113 114
            {
                return;
            }

            var arguments = (SeparatedSyntaxList<TArgumentSyntax>)syntaxFacts.GetArgumentsOfObjectCreationExpression(objectCreation);

115
            var comparer = syntaxFacts.StringComparer;
C
CyrusNajmabadi 已提交
116 117
            var constructorsAndArgumentToAdd = ArrayBuilder<(IMethodSymbol constructor, TArgumentSyntax argument, int index)>.GetInstance();

118 119
            foreach (var constructor in type.InstanceConstructors.OrderBy(m => m.Parameters.Length))
            {
C
CyrusNajmabadi 已提交
120
                if (constructor.IsNonImplicitAndFromSource() &&
121
                    NonParamsParameterCount(constructor) < arguments.Count)
122 123
                {
                    var argumentToAdd = DetermineFirstArgumentToAdd(
124 125
                        semanticModel, syntaxFacts, comparer, constructor, 
                        arguments, argumentOpt);
126 127 128

                    if (argumentToAdd != null)
                    {
129 130 131 132 133 134 135 136 137
                        if (argumentOpt != null && argumentToAdd != argumentOpt)
                        {
                            // We were trying to fix a specific argument, but the argument we want
                            // to fix is something different.  That means there was an error earlier
                            // than this argument.  Which means we're looking at a non-viable 
                            // constructor.  Skip this one.
                            continue;
                        }

C
CyrusNajmabadi 已提交
138 139
                        constructorsAndArgumentToAdd.Add(
                            (constructor, argumentToAdd, arguments.IndexOf(argumentToAdd)));
140 141 142
                    }
                }
            }
C
CyrusNajmabadi 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157

            // Order by the furthest argument index to the nearest argument index.  The ones with
            // larger argument indexes mean that we matched more earlier arguments (and thus are
            // likely to be the correct match).
            foreach (var tuple in constructorsAndArgumentToAdd.OrderByDescending(t => t.index))
            {
                var constructor = tuple.constructor;
                var argumentToAdd = tuple.argument;

                var parameters = constructor.Parameters.Select(p => p.ToDisplayString(SimpleFormat));
                var signature = $"{type.Name}({string.Join(", ", parameters)})";

                var title = string.Format(FeaturesResources.Add_parameter_to_0, signature);

                context.RegisterCodeFix(
C
CyrusNajmabadi 已提交
158 159
                    new MyCodeAction(title, c => FixAsync(document, constructor, argumentToAdd, arguments, c)),
                    context.Diagnostics);
C
CyrusNajmabadi 已提交
160
            }
161 162
        }

163 164 165
        private int NonParamsParameterCount(IMethodSymbol method)
            => method.IsParams() ? method.Parameters.Length - 1 : method.Parameters.Length;

166 167 168 169 170 171 172 173 174
        private async Task<Document> FixAsync(
            Document invocationDocument, 
            IMethodSymbol method,
            TArgumentSyntax argument,
            SeparatedSyntaxList<TArgumentSyntax> argumentList,
            CancellationToken cancellationToken)
        {
            var methodDeclaration = await method.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false);

175 176
            var (parameterSymbol, isNamedArgument) = await CreateParameterSymbolAsync(
                invocationDocument, method, argument, cancellationToken).ConfigureAwait(false);
177 178

            var methodDocument = invocationDocument.Project.Solution.GetDocument(methodDeclaration.SyntaxTree);
179 180 181 182 183 184 185 186 187 188 189 190
            var syntaxFacts = methodDocument.GetLanguageService<ISyntaxFactsService>();
            var methodDeclarationRoot = methodDeclaration.SyntaxTree.GetRoot(cancellationToken);
            var editor = new SyntaxEditor(methodDeclarationRoot, methodDocument.Project.Solution.Workspace);

            var parameterDeclaration = editor.Generator.ParameterDeclaration(parameterSymbol)
                                                       .WithAdditionalAnnotations(Formatter.Annotation);

            AddParameter(
                syntaxFacts, editor, methodDeclaration, isNamedArgument, argument, 
                argumentList, parameterDeclaration, cancellationToken);
            
            var newRoot = editor.GetChangedRoot();
191 192 193 194 195
            var newDocument = methodDocument.WithSyntaxRoot(newRoot);

            return newDocument;
        }

196 197
        private async Task<(IParameterSymbol, bool isNamedArgument)> CreateParameterSymbolAsync(
            Document invocationDocument,
C
CyrusNajmabadi 已提交
198 199
            IMethodSymbol method,
            TArgumentSyntax argument,
200
            CancellationToken cancellationToken)
201
        {
202 203 204 205 206 207 208 209
            var syntaxFacts = invocationDocument.GetLanguageService<ISyntaxFactsService>();
            var semanticFacts = invocationDocument.GetLanguageService<ISemanticFactsService>();
            var argumentName = syntaxFacts.GetNameForArgument(argument);
            var expression = syntaxFacts.GetExpressionOfArgument(argument);

            var semanticModel = await invocationDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var parameterType = semanticModel.GetTypeInfo(expression).Type ?? semanticModel.Compilation.ObjectType;

210 211 212
            if (!string.IsNullOrWhiteSpace(argumentName))
            {
                var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol(
213
                    attributes: default, refKind: RefKind.None, isParams: false, type: parameterType, name: argumentName);
214

215
                return (newParameterSymbol, isNamedArgument: true);
216 217 218
            }
            else
            {
219 220
                var name = semanticFacts.GenerateNameForExpression(
                    semanticModel, expression, capitalize: false, cancellationToken: cancellationToken);
221 222 223
                var uniqueName = NameGenerator.EnsureUniqueness(name, method.Parameters.Select(p => p.Name));

                var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol(
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
                    attributes: default, refKind: RefKind.None, isParams: false, type: parameterType, name: uniqueName);

                return (newParameterSymbol, isNamedArgument: false);
            }
        }

        private static void AddParameter(
            ISyntaxFactsService syntaxFacts,
            SyntaxEditor editor,
            SyntaxNode declaration,
            bool isNamedArgument,
            TArgumentSyntax argument,
            SeparatedSyntaxList<TArgumentSyntax> argumentList,
            SyntaxNode parameterDeclaration,
            CancellationToken cancellationToken)
        {
            var generator = editor.Generator;

            var existingParameters = generator.GetParameters(declaration);
            var placeOnNewLine = ShouldPlaceParametersOnNewLine(existingParameters, cancellationToken);

            var argumentIndex = argumentList.IndexOf(argument);
            if (isNamedArgument || argumentIndex == existingParameters.Count)
            {
                if (placeOnNewLine)
                {
C
CyrusNajmabadi 已提交
250 251
                    // Placing the last parameter on its own line.  Get the indentation of the 
                    // curent last parameter and give the new last parameter the same indentation.
252 253 254 255 256 257 258 259 260 261
                    var leadingIndentation = GetDesiredLeadingIndentation(
                        generator, syntaxFacts, existingParameters.Last(), includeLeadingNewLine: true);
                    parameterDeclaration = parameterDeclaration.WithPrependedLeadingTrivia(leadingIndentation)
                                                               .WithAdditionalAnnotations(Formatter.Annotation);
                }
                
                editor.AddParameter(declaration, parameterDeclaration);
            }
            else
            {
C
CyrusNajmabadi 已提交
262
                // Inserting the parameter somewhere other than the end.
263 264 265 266
                if (placeOnNewLine)
                {
                    if (argumentIndex == 0)
                    {
C
CyrusNajmabadi 已提交
267 268 269 270
                        // We want to insert the parameter at the front of the exsiting parameter
                        // list.  That means we need to move the current first parameter to a new
                        // line.  Give the current first parameter the indentation of the second
                        // parameter in the list.
271 272 273 274 275 276 277 278 279 280 281 282
                        editor.InsertParameter(declaration, argumentIndex, parameterDeclaration);
                        var nextParameter = existingParameters[argumentIndex];

                        var leadingIndentation = GetDesiredLeadingIndentation(
                            generator, syntaxFacts, existingParameters[argumentIndex + 1], includeLeadingNewLine: true);
                        editor.ReplaceNode(
                            nextParameter,
                            nextParameter.WithPrependedLeadingTrivia(leadingIndentation)
                                         .WithAdditionalAnnotations(Formatter.Annotation));
                    }
                    else
                    {
C
CyrusNajmabadi 已提交
283 284 285 286 287 288 289
                        // We're inserting somewhere after the start (but not at the end). Because 
                        // we've set placeOnNewLine, we know that the current comma we'll be placed
                        // after already have a newline following it.  So all we need for this new 
                        // parameter is to get the indentation of the following parameter.
                        // Because we're going to 'steal' the existing comma from that parameter,
                        // ensure that the next parameter has a new-line added to it so that it will
                        // still stay on a new line.
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
                        var nextParameter = existingParameters[argumentIndex];
                        var leadingIndentation = GetDesiredLeadingIndentation(
                            generator, syntaxFacts, existingParameters[argumentIndex], includeLeadingNewLine: false);
                        parameterDeclaration = parameterDeclaration.WithPrependedLeadingTrivia(leadingIndentation);

                        editor.InsertParameter(declaration, argumentIndex, parameterDeclaration);
                        editor.ReplaceNode(
                            nextParameter,
                            nextParameter.WithPrependedLeadingTrivia(generator.ElasticCarriageReturnLineFeed)
                                         .WithAdditionalAnnotations(Formatter.Annotation));
                    }
                }
                else
                {
                    editor.InsertParameter(declaration, argumentIndex, parameterDeclaration);
                }
            }
        }

        private static List<SyntaxTrivia> GetDesiredLeadingIndentation(
            SyntaxGenerator generator, ISyntaxFactsService syntaxFacts, 
            SyntaxNode node, bool includeLeadingNewLine)
        {
            var triviaList = new List<SyntaxTrivia>();
            if (includeLeadingNewLine)
            {
                triviaList.Add(generator.ElasticCarriageReturnLineFeed);
            }

            var lastWhitespace = default(SyntaxTrivia); 
            foreach(var trivia in node.GetLeadingTrivia().Reverse())
            {
                if (syntaxFacts.IsWhitespaceTrivia(trivia))
                {
                    lastWhitespace = trivia;
                }
                else if (syntaxFacts.IsEndOfLineTrivia(trivia))
                {
                    break;
                }
            }

            if (lastWhitespace.RawKind != 0)
            {
                triviaList.Add(lastWhitespace);
            }

            return triviaList;
        }
339

340 341 342 343 344 345
        private static bool ShouldPlaceParametersOnNewLine(
            IReadOnlyList<SyntaxNode> parameters, CancellationToken cancellationToken)
        {
            if (parameters.Count <= 1)
            {
                return false;
346
            }
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361

            var text = parameters[0].SyntaxTree.GetText(cancellationToken);
            for (int i = 1, n = parameters.Count; i < n; i++)
            {
                var lastParameter = parameters[i - 1];
                var thisParameter = parameters[i];

                if (text.AreOnSameLine(lastParameter.GetLastToken(), thisParameter.GetFirstToken()))
                {
                    return false;
                }
            }

            // All parameters are on different lines.  Place the new parameter on a new line as well.
            return true;
362 363 364 365 366 367 368 369 370 371
        }

        private static readonly SymbolDisplayFormat SimpleFormat =
                    new SymbolDisplayFormat(
                        typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
                        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
                        parameterOptions: SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeType,
                        miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes);

        private TArgumentSyntax DetermineFirstArgumentToAdd(
372 373 374 375
            SemanticModel semanticModel,
            ISyntaxFactsService syntaxFacts,
            StringComparer comparer,
            IMethodSymbol method,
376 377
            SeparatedSyntaxList<TArgumentSyntax> arguments,
            TArgumentSyntax argumentOpt)
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
        {
            var methodParameterNames = new HashSet<string>(comparer);
            methodParameterNames.AddRange(method.Parameters.Select(p => p.Name));

            for (int i = 0, n = arguments.Count; i < n; i++)
            {
                var argument = arguments[i];
                var argumentName = syntaxFacts.GetNameForArgument(argument);

                if (!string.IsNullOrWhiteSpace(argumentName))
                {
                    // If the user provided an argument-name and we don't have any parameters that
                    // match, then this is the argument we want to add a parameter for.
                    if (!methodParameterNames.Contains(argumentName))
                    {
                        return argument;
                    }
                }
                else
                {
                    // Positional argument.  If the position is beyond what the method supports,
                    // then this definitely is an argument we could add.
                    if (i >= method.Parameters.Length)
                    {
402 403 404 405 406 407
                        if (method.Parameters.LastOrDefault()?.IsParams == true)
                        {
                            // Last parameter is a params.  We can't place any parameters past it.
                            return null;
                        }

408 409 410
                        return argument;
                    }

C
CyrusNajmabadi 已提交
411 412
                    // Now check the type of the argument versus the type of the parameter.  If they
                    // don't match, then this is the argument we should make the parameter for.
413
                    var argumentTypeInfo = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfArgument(argument));
C
CyrusNajmabadi 已提交
414 415 416 417 418 419 420
                    if (argumentTypeInfo.Type == null && argumentTypeInfo.ConvertedType == null)
                    {
                        // Didn't know the type of the argument.  We shouldn't assume it doesn't
                        // match a parameter. 
                        continue;
                    }

421 422
                    var parameter = method.Parameters[i];

423
                    if (!TypeInfoMatchesType(argumentTypeInfo, parameter.Type))
424
                    {
C
CyrusNajmabadi 已提交
425
                        if (TypeInfoMatchesWithParamsExpansion(argumentTypeInfo, parameter))
426
                        {
C
CyrusNajmabadi 已提交
427 428 429 430
                            // The argument matched if we expanded out the params-parameter.
                            // As the params-parameter has to be last, there's nothing else to 
                            // do here.
                            return null;
431 432
                        }

433 434 435 436 437 438 439
                        return argument;
                    }
                }
            }

            return null;
        }
C
CyrusNajmabadi 已提交
440

C
CyrusNajmabadi 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453
        private bool TypeInfoMatchesWithParamsExpansion(TypeInfo argumentTypeInfo, IParameterSymbol parameter)
        {
            if (parameter.IsParams && parameter.Type is IArrayTypeSymbol arrayType)
            {
                if (TypeInfoMatchesType(argumentTypeInfo, arrayType.ElementType))
                {
                    return true;
                }
            }

            return false;
        }

454 455 456
        private bool TypeInfoMatchesType(TypeInfo argumentTypeInfo, ITypeSymbol type)
            => type.Equals(argumentTypeInfo.Type) || type.Equals(argumentTypeInfo.ConvertedType);

457
        private class MyCodeAction : CodeAction.DocumentChangeAction
C
CyrusNajmabadi 已提交
458
        {
459 460 461 462 463 464
            public MyCodeAction(
                string title,
                Func<CancellationToken, Task<Document>> createChangedDocument)
                : base(title, createChangedDocument)
            {
            }
C
CyrusNajmabadi 已提交
465 466
        }
    }
T
Tomas Matousek 已提交
467
}