AbstractInitializeMemberFromParameterCodeRefactoringProviderMemberCreation.cs 32.4 KB
Newer Older
J
Jonathon Marolf 已提交
1 2 3
// 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.
4

5 6 7
#nullable enable

using System;
C
Cyrus Najmabadi 已提交
8
using System.Collections.Generic;
9
using System.Collections.Immutable;
C
Cyrus Najmabadi 已提交
10
using System.Diagnostics;
11
using System.Linq;
12 13 14
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
15
using Microsoft.CodeAnalysis.CodeGeneration;
A
Allison Chou 已提交
16
using Microsoft.CodeAnalysis.CodeStyle;
17 18
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editing;
19
using Microsoft.CodeAnalysis.Operations;
C
Cyrus Najmabadi 已提交
20
using Microsoft.CodeAnalysis.Options;
21
using Microsoft.CodeAnalysis.PooledObjects;
22
using Microsoft.CodeAnalysis.Shared.Extensions;
23
using Microsoft.CodeAnalysis.Shared.Naming;
24
using Microsoft.CodeAnalysis.Shared.Utilities;
25
using Microsoft.CodeAnalysis.Text;
26
using Roslyn.Utilities;
27 28 29

namespace Microsoft.CodeAnalysis.InitializeParameter
{
30
    internal abstract partial class AbstractInitializeMemberFromParameterCodeRefactoringProvider<
C
Cyrus Najmabadi 已提交
31
        TTypeDeclarationSyntax,
32 33 34
        TParameterSyntax,
        TStatementSyntax,
        TExpressionSyntax> : AbstractInitializeParameterCodeRefactoringProvider<
C
Cyrus Najmabadi 已提交
35
            TTypeDeclarationSyntax,
36 37 38
            TParameterSyntax,
            TStatementSyntax,
            TExpressionSyntax>
C
Cyrus Najmabadi 已提交
39
        where TTypeDeclarationSyntax : SyntaxNode
40 41 42 43
        where TParameterSyntax : SyntaxNode
        where TStatementSyntax : SyntaxNode
        where TExpressionSyntax : SyntaxNode
    {
44
        protected abstract SyntaxNode TryGetLastStatement(IBlockOperation? blockStatementOpt);
C
CyrusNajmabadi 已提交
45

A
Allison Chou 已提交
46
        protected abstract Accessibility DetermineDefaultFieldAccessibility(INamedTypeSymbol containingType);
A
Allison Chou 已提交
47 48 49

        protected abstract Accessibility DetermineDefaultPropertyAccessibility();

50 51 52
        protected override Task<ImmutableArray<CodeAction>> GetRefactoringsForAllParametersAsync(
            Document document, SyntaxNode functionDeclaration, IMethodSymbol method, IBlockOperation? blockStatementOpt,
            ImmutableArray<SyntaxNode> listOfParameterNodes, TextSpan parameterSpan, CancellationToken cancellationToken)
53 54 55 56 57
        {
            return Task.FromResult(ImmutableArray<CodeAction>.Empty);
        }

        protected override async Task<ImmutableArray<CodeAction>> GetRefactoringsForSingleParameterAsync(
C
Cyrus Najmabadi 已提交
58
            Document document, IParameterSymbol parameter, SyntaxNode constructorDeclaration, IMethodSymbol method,
59
            IBlockOperation? blockStatementOpt, CancellationToken cancellationToken)
60
        {
C
CyrusNajmabadi 已提交
61
            // Only supported for constructor parameters.
Š
Šimon Koníček 已提交
62
            if (method.MethodKind != MethodKind.Constructor)
63 64
                return ImmutableArray<CodeAction>.Empty;

65
            var typeDeclaration = constructorDeclaration.GetAncestor<TTypeDeclarationSyntax>();
C
Cyrus Najmabadi 已提交
66
            if (typeDeclaration == null)
C
Cyrus Najmabadi 已提交
67 68 69 70 71
                return ImmutableArray<CodeAction>.Empty;

            // See if we're already assigning this parameter to a field/property in this type. If so, there's nothing
            // more for us to do.
            var assignmentStatement = TryFindFieldOrPropertyAssignmentStatement(parameter, blockStatementOpt);
72 73 74 75 76 77 78
            if (assignmentStatement != null)
                return ImmutableArray<CodeAction>.Empty;

            // Haven't initialized any fields/properties with this parameter.  Offer to assign
            // to an existing matching field/prop if we can find one, or add a new field/prop
            // if we can't.

79
            var rules = await document.GetNamingRulesAsync(cancellationToken).ConfigureAwait(false);
80 81 82 83
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules);
            if (parameterNameParts.BaseName == "")
                return ImmutableArray<CodeAction>.Empty;

C
CyrusNajmabadi 已提交
84
            var fieldOrProperty = await TryFindMatchingUninitializedFieldOrPropertySymbolAsync(
85
                document, parameter, blockStatementOpt, rules, parameterNameParts.BaseNameParts, cancellationToken).ConfigureAwait(false);
86

C
CyrusNajmabadi 已提交
87
            if (fieldOrProperty != null)
88
            {
C
Cyrus Najmabadi 已提交
89
                return HandleExistingFieldOrProperty(
C
Cyrus Najmabadi 已提交
90
                    document, parameter, constructorDeclaration,
91
                    blockStatementOpt, fieldOrProperty);
C
Cyrus Najmabadi 已提交
92 93 94
            }

            return await HandleNoExistingFieldOrPropertyAsync(
C
Cyrus Najmabadi 已提交
95 96
                document, parameter, constructorDeclaration,
                method, blockStatementOpt, rules, cancellationToken).ConfigureAwait(false);
C
Cyrus Najmabadi 已提交
97 98 99 100 101
        }

        private async Task<ImmutableArray<CodeAction>> HandleNoExistingFieldOrPropertyAsync(
            Document document,
            IParameterSymbol parameter,
C
Cyrus Najmabadi 已提交
102
            SyntaxNode constructorDeclaration,
C
Cyrus Najmabadi 已提交
103 104 105 106 107 108 109
            IMethodSymbol method,
            IBlockOperation? blockStatementOpt,
            ImmutableArray<NamingRule> rules,
            CancellationToken cancellationToken)
        {
            // Didn't find a field/prop that this parameter could be assigned to.
            // Offer to create new one and assign to that.
C
Cyrus Najmabadi 已提交
110
            using var _ = ArrayBuilder<CodeAction>.GetInstance(out var allActions);
C
Cyrus Najmabadi 已提交
111 112 113

            var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);

C
Cyrus Najmabadi 已提交
114
            var (fieldAction, propertyAction) = AddSpecificParameterInitializationActions(
C
Cyrus Najmabadi 已提交
115
                document, parameter, constructorDeclaration, blockStatementOpt, rules, options);
116

C
Cyrus Najmabadi 已提交
117 118 119 120 121 122 123
            // Check if the surrounding parameters are assigned to another field in this class.  If so, offer to
            // make this parameter into a field as well.  Otherwise, default to generating a property
            var siblingFieldOrProperty = TryFindSiblingFieldOrProperty(parameter, blockStatementOpt);
            if (siblingFieldOrProperty is IFieldSymbol)
            {
                allActions.Add(fieldAction);
                allActions.Add(propertyAction);
124 125 126
            }
            else
            {
C
Cyrus Najmabadi 已提交
127 128 129
                allActions.Add(propertyAction);
                allActions.Add(fieldAction);
            }
130

C
Cyrus Najmabadi 已提交
131
            var (allFieldsAction, allPropertiesAction) = AddAllParameterInitializationActions(
C
Cyrus Najmabadi 已提交
132
                document, constructorDeclaration, method, blockStatementOpt, rules, options);
C
Cyrus Najmabadi 已提交
133

C
Cyrus Najmabadi 已提交
134
            if (allFieldsAction != null && allPropertiesAction != null)
C
Cyrus Najmabadi 已提交
135 136 137
            {
                if (siblingFieldOrProperty is IFieldSymbol)
                {
C
Cyrus Najmabadi 已提交
138 139
                    allActions.Add(allFieldsAction);
                    allActions.Add(allPropertiesAction);
C
Cyrus Najmabadi 已提交
140 141 142
                }
                else
                {
C
Cyrus Najmabadi 已提交
143 144
                    allActions.Add(allPropertiesAction);
                    allActions.Add(allFieldsAction);
C
Cyrus Najmabadi 已提交
145 146 147 148 149 150
                }
            }

            return allActions.ToImmutable();
        }

C
Cyrus Najmabadi 已提交
151 152
        private (CodeAction? fieldAction, CodeAction? propertyAction) AddAllParameterInitializationActions(
            Document document,
C
Cyrus Najmabadi 已提交
153
            SyntaxNode constructorDeclaration,
C
Cyrus Najmabadi 已提交
154 155 156 157
            IMethodSymbol method,
            IBlockOperation? blockStatementOpt,
            ImmutableArray<NamingRule> rules,
            DocumentOptionSet options)
C
Cyrus Najmabadi 已提交
158
        {
C
Cyrus Najmabadi 已提交
159 160 161 162 163 164 165 166 167 168
            if (blockStatementOpt == null)
                return default;

            var parameters = GetParametersWithoutAssociatedMembers(blockStatementOpt, rules, method);

            if (parameters.Length < 2)
                return default;

            var fields = parameters.SelectAsArray(p => (ISymbol)CreateField(p, options, rules));
            var properties = parameters.SelectAsArray(p => (ISymbol)CreateProperty(p, options, rules));
C
Cyrus Najmabadi 已提交
169

C
Cyrus Najmabadi 已提交
170
            var allFieldsAction = new MyCodeAction(
C
Renames  
Cyrus Najmabadi 已提交
171
                FeaturesResources.Create_and_assign_remaining_as_fields,
C
Cyrus Najmabadi 已提交
172
                c => AddAllSymbolInitializationsAsync(
C
Cyrus Najmabadi 已提交
173
                    document, constructorDeclaration, blockStatementOpt, parameters, fields, c));
C
Cyrus Najmabadi 已提交
174
            var allPropertiesAction = new MyCodeAction(
C
Renames  
Cyrus Najmabadi 已提交
175
                FeaturesResources.Create_and_assign_remaining_as_properties,
C
Cyrus Najmabadi 已提交
176
                c => AddAllSymbolInitializationsAsync(
C
Cyrus Najmabadi 已提交
177
                    document, constructorDeclaration, blockStatementOpt, parameters, properties, c));
C
Cyrus Najmabadi 已提交
178

C
Cyrus Najmabadi 已提交
179
            return (allFieldsAction, allPropertiesAction);
C
Cyrus Najmabadi 已提交
180 181
        }

C
Cyrus Najmabadi 已提交
182
        private (CodeAction fieldAction, CodeAction propertyAction) AddSpecificParameterInitializationActions(
C
Cyrus Najmabadi 已提交
183
            Document document,
C
Cyrus Najmabadi 已提交
184
            IParameterSymbol parameter,
C
Cyrus Najmabadi 已提交
185
            SyntaxNode constructorDeclaration,
C
Cyrus Najmabadi 已提交
186 187
            IBlockOperation? blockStatementOpt,
            ImmutableArray<NamingRule> rules,
C
Cyrus Najmabadi 已提交
188
            DocumentOptionSet options)
C
Cyrus Najmabadi 已提交
189
        {
C
Cyrus Najmabadi 已提交
190 191 192
            var field = CreateField(parameter, options, rules);
            var property = CreateProperty(parameter, options, rules);
            var fieldAction = new MyCodeAction(
C
Renames  
Cyrus Najmabadi 已提交
193
                string.Format(FeaturesResources.Create_and_assign_field_0, field.Name),
C
Cyrus Najmabadi 已提交
194
                c => AddSingleSymbolInitializationAsync(document, constructorDeclaration, blockStatementOpt, parameter, field, c));
C
Cyrus Najmabadi 已提交
195
            var propertyAction = new MyCodeAction(
C
Renames  
Cyrus Najmabadi 已提交
196
                string.Format(FeaturesResources.Create_and_assign_property_0, property.Name),
C
Cyrus Najmabadi 已提交
197
                c => AddSingleSymbolInitializationAsync(document, constructorDeclaration, blockStatementOpt, parameter, property, c));
C
Cyrus Najmabadi 已提交
198

C
Cyrus Najmabadi 已提交
199 200
            return (fieldAction, propertyAction);
        }
C
Cyrus Najmabadi 已提交
201

202
        private static ImmutableArray<IParameterSymbol> GetParametersWithoutAssociatedMembers(
C
Cyrus Najmabadi 已提交
203 204 205 206 207 208 209
            IBlockOperation? blockStatementOpt,
            ImmutableArray<NamingRule> rules,
            IMethodSymbol method)
        {
            using var _ = ArrayBuilder<IParameterSymbol>.GetInstance(out var result);

            foreach (var parameter in method.Parameters)
C
Cyrus Najmabadi 已提交
210 211 212 213 214
            {
                var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules);
                if (parameterNameParts.BaseName == "")
                    continue;

C
Cyrus Najmabadi 已提交
215 216
                var assignmentOp = TryFindFieldOrPropertyAssignmentStatement(parameter, blockStatementOpt);
                if (assignmentOp != null)
C
Cyrus Najmabadi 已提交
217
                    continue;
A
Allison Chou 已提交
218

C
Cyrus Najmabadi 已提交
219
                result.Add(parameter);
220
            }
C
Cyrus Najmabadi 已提交
221

C
Cyrus Najmabadi 已提交
222
            return result.ToImmutable();
C
Cyrus Najmabadi 已提交
223 224
        }

225
        private ImmutableArray<CodeAction> HandleExistingFieldOrProperty(Document document, IParameterSymbol parameter, SyntaxNode functionDeclaration, IBlockOperation? blockStatementOpt, ISymbol fieldOrProperty)
C
Cyrus Najmabadi 已提交
226 227 228 229 230 231 232 233 234 235 236 237
        {
            // Found a field/property that this parameter should be assigned to.
            // Just offer the simple assignment to it.

            var resource = fieldOrProperty.Kind == SymbolKind.Field
                ? FeaturesResources.Initialize_field_0
                : FeaturesResources.Initialize_property_0;

            var title = string.Format(resource, fieldOrProperty.Name);

            return ImmutableArray.Create<CodeAction>(new MyCodeAction(
                title,
C
Cyrus Najmabadi 已提交
238 239
                c => AddSingleSymbolInitializationAsync(
                    document, functionDeclaration, blockStatementOpt, parameter, fieldOrProperty, c)));
240 241
        }

242
        private static ISymbol? TryFindSiblingFieldOrProperty(IParameterSymbol parameter, IBlockOperation? blockStatementOpt)
243 244 245
        {
            foreach (var (siblingParam, _) in GetSiblingParameters(parameter))
            {
C
Cyrus Najmabadi 已提交
246
                TryFindFieldOrPropertyAssignmentStatement(siblingParam, blockStatementOpt, out var sibling);
247 248 249 250 251 252 253
                if (sibling != null)
                    return sibling;
            }

            return null;
        }

C
Cyrus Najmabadi 已提交
254 255 256 257 258
        private IFieldSymbol CreateField(
            IParameterSymbol parameter,
            DocumentOptionSet options,
            ImmutableArray<NamingRule> rules)
        {
259
            var requireAccessibilityModifiers = options.GetOption(CodeStyleOptions2.RequireAccessibilityModifiers);
C
Cyrus Najmabadi 已提交
260 261
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts;

262 263 264 265
            foreach (var rule in rules)
            {
                if (rule.SymbolSpecification.AppliesTo(SymbolKind.Field, Accessibility.Private))
                {
C
CyrusNajmabadi 已提交
266
                    var uniqueName = GenerateUniqueName(parameter, parameterNameParts, rule);
267

A
Allison Chou 已提交
268 269 270
                    var accessibilityLevel = Accessibility.Private;
                    if (requireAccessibilityModifiers.Value == AccessibilityModifiersRequired.Never || requireAccessibilityModifiers.Value == AccessibilityModifiersRequired.OmitIfDefault)
                    {
A
Allison Chou 已提交
271
                        var defaultAccessibility = DetermineDefaultFieldAccessibility(parameter.ContainingType);
A
Allison Chou 已提交
272 273 274 275
                        if (defaultAccessibility == Accessibility.Private)
                        {
                            accessibilityLevel = Accessibility.NotApplicable;
                        }
A
Allison Chou 已提交
276 277
                    }

278
                    return CodeGenerationSymbolFactory.CreateFieldSymbol(
C
CyrusNajmabadi 已提交
279
                        default,
A
Allison Chou 已提交
280
                        accessibilityLevel,
281
                        DeclarationModifiers.ReadOnly,
282
                        parameter.Type, uniqueName);
283 284 285 286 287 288 289 290
                }
            }

            // We place a special rule in s_builtInRules that matches all fields.  So we should 
            // always find a matching rule.
            throw ExceptionUtilities.Unreachable;
        }

C
CyrusNajmabadi 已提交
291
        private static string GenerateUniqueName(IParameterSymbol parameter, ImmutableArray<string> parameterNameParts, NamingRule rule)
C
CyrusNajmabadi 已提交
292 293 294 295 296 297 298 299 300 301 302 303
        {
            // Determine an appropriate name to call the new field.
            var containingType = parameter.ContainingType;
            var baseName = rule.NamingStyle.CreateName(parameterNameParts);

            // Ensure that the name is unique in the containing type so we
            // don't stomp on an existing member.
            var uniqueName = NameGenerator.GenerateUniqueName(
                baseName, n => containingType.GetMembers(n).IsEmpty);
            return uniqueName;
        }

304
        private IPropertySymbol CreateProperty(
A
Allison Chou 已提交
305
            IParameterSymbol parameter,
C
Cyrus Najmabadi 已提交
306 307
            DocumentOptionSet options,
            ImmutableArray<NamingRule> rules)
308
        {
309
            var requireAccessibilityModifiers = options.GetOption(CodeStyleOptions2.RequireAccessibilityModifiers);
C
Cyrus Najmabadi 已提交
310 311
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts;

312 313 314 315
            foreach (var rule in rules)
            {
                if (rule.SymbolSpecification.AppliesTo(SymbolKind.Property, Accessibility.Public))
                {
C
CyrusNajmabadi 已提交
316
                    var uniqueName = GenerateUniqueName(parameter, parameterNameParts, rule);
317

A
Allison Chou 已提交
318 319 320 321 322 323 324 325 326 327
                    var accessibilityLevel = Accessibility.Public;
                    if (requireAccessibilityModifiers.Value == AccessibilityModifiersRequired.Never || requireAccessibilityModifiers.Value == AccessibilityModifiersRequired.OmitIfDefault)
                    {
                        var defaultAccessibility = DetermineDefaultPropertyAccessibility();
                        if (defaultAccessibility == Accessibility.Public)
                        {
                            accessibilityLevel = Accessibility.NotApplicable;
                        }
                    }

328
                    var getMethod = CodeGenerationSymbolFactory.CreateAccessorSymbol(
C
CyrusNajmabadi 已提交
329
                        default,
330
                        Accessibility.Public,
C
CyrusNajmabadi 已提交
331
                        default);
332 333

                    return CodeGenerationSymbolFactory.CreatePropertySymbol(
C
CyrusNajmabadi 已提交
334
                        default,
A
Allison Chou 已提交
335
                        accessibilityLevel,
336
                        new DeclarationModifiers(),
337
                        parameter.Type,
338
                        RefKind.None,
339
                        explicitInterfaceImplementations: default,
340
                        name: uniqueName,
C
CyrusNajmabadi 已提交
341
                        parameters: default,
342 343 344
                        getMethod: getMethod,
                        setMethod: null);
                }
345 346
            }

C
CyrusNajmabadi 已提交
347 348
            // We place a special rule in s_builtInRules that matches all properties.  So we should 
            // always find a matching rule.
349
            throw ExceptionUtilities.Unreachable;
350 351
        }

C
Cyrus Najmabadi 已提交
352 353
        private async Task<Document> AddAllSymbolInitializationsAsync(
            Document document,
C
Cyrus Najmabadi 已提交
354
            SyntaxNode constructorDeclaration,
C
Cyrus Najmabadi 已提交
355 356 357 358 359
            IBlockOperation? blockStatementOpt,
            ImmutableArray<IParameterSymbol> parameters,
            ImmutableArray<ISymbol> fieldsOrProperties,
            CancellationToken cancellationToken)
        {
C
Cyrus Najmabadi 已提交
360
            Debug.Assert(parameters.Length >= 2);
C
Cyrus Najmabadi 已提交
361 362 363
            Debug.Assert(fieldsOrProperties.Length > 0);
            Debug.Assert(parameters.Length == fieldsOrProperties.Length);

C
Cyrus Najmabadi 已提交
364 365 366 367 368 369 370 371 372 373 374
            // Process each param+field/prop in order.  Apply the pair to the document getting the updated document.
            // Then find all the current data in that updated document and move onto the next pair.

            var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var nodesToTrack = new List<SyntaxNode> { constructorDeclaration };
            if (blockStatementOpt != null)
                nodesToTrack.Add(blockStatementOpt.Syntax);

            var trackedRoot = root.TrackNodes(nodesToTrack);
            var currentDocument = document.WithSyntaxRoot(trackedRoot);

375
            for (var i = 0; i < parameters.Length; i++)
C
Cyrus Najmabadi 已提交
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
            {
                var parameter = parameters[i];
                var fieldOrProperty = fieldsOrProperties[i];

                var currentSemanticModel = await currentDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var currentCompilation = currentSemanticModel.Compilation;
                var currentRoot = await currentDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

                var currentConstructorDeclaration = currentRoot.GetCurrentNode(constructorDeclaration);
                if (currentConstructorDeclaration == null)
                    continue;

                IBlockOperation? currentBlockStatementOpt = null;
                if (blockStatementOpt != null)
                {
J
Jonathon Marolf 已提交
391
                    currentBlockStatementOpt = (IBlockOperation?)currentSemanticModel.GetOperation(currentRoot.GetCurrentNode(blockStatementOpt.Syntax), cancellationToken);
C
Cyrus Najmabadi 已提交
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
                    if (currentBlockStatementOpt == null)
                        continue;
                }

                var currentParameter = (IParameterSymbol?)parameter.GetSymbolKey(cancellationToken).Resolve(currentCompilation, cancellationToken: cancellationToken).GetAnySymbol();
                if (currentParameter == null)
                    continue;

                // fieldOrProperty is a new member.  So we don't have to track it to this edit we're making.

                currentDocument = await AddSingleSymbolInitializationAsync(
                    currentDocument,
                    currentConstructorDeclaration,
                    currentBlockStatementOpt,
                    currentParameter,
                    fieldOrProperty,
                    cancellationToken).ConfigureAwait(false);
            }

            return currentDocument;
        }

        private async Task<Document> AddSingleSymbolInitializationAsync(
            Document document,
            SyntaxNode constructorDeclaration,
            IBlockOperation? blockStatementOpt,
            IParameterSymbol parameter,
            ISymbol fieldOrProperty,
            CancellationToken cancellationToken)
        {
422
            var workspace = document.Project.Solution.Workspace;
423
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
424
            var editor = new SyntaxEditor(root, workspace);
425
            var generator = editor.Generator;
C
Cyrus Najmabadi 已提交
426
            var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
427

C
Cyrus Najmabadi 已提交
428
            if (fieldOrProperty.ContainingType == null)
429 430 431
            {
                // We're generating a new field/property.  Place into the containing type,
                // ideally before/after a relevant existing member.
C
CyrusNajmabadi 已提交
432 433
                // First, look for the right containing type (As a type may be partial). 
                // We want the type-block that this constructor is contained within.
C
Cyrus Najmabadi 已提交
434
                var typeDeclaration = constructorDeclaration.GetAncestor<TTypeDeclarationSyntax>()!;
435

C
CyrusNajmabadi 已提交
436 437 438 439 440 441 442 443
                // Now add the field/property to this type.  Use the 'ReplaceNode+callback' form
                // so that nodes will be appropriate tracked and so we can then update the constructor
                // below even after we've replaced the whole type with a new type.
                //
                // Note: We'll pass the appropriate options so that the new field/property 
                // is appropriate placed before/after an existing field/property.  We'll try
                // to preserve the same order for fields/properties that we have for the constructor
                // parameters.
444 445 446 447
                editor.ReplaceNode(
                    typeDeclaration,
                    (currentTypeDecl, _) =>
                    {
C
Cyrus Najmabadi 已提交
448
                        if (fieldOrProperty is IPropertySymbol property)
449
                        {
C
Cyrus Najmabadi 已提交
450 451
                            return CodeGenerator.AddPropertyDeclaration(
                                currentTypeDecl, property, workspace,
C
Cyrus Najmabadi 已提交
452
                                GetAddOptions<IPropertySymbol>(parameter, blockStatementOpt, typeDeclaration, options, cancellationToken));
C
Cyrus Najmabadi 已提交
453 454 455 456 457
                        }
                        else if (fieldOrProperty is IFieldSymbol field)
                        {
                            return CodeGenerator.AddFieldDeclaration(
                                currentTypeDecl, field, workspace,
C
Cyrus Najmabadi 已提交
458
                                GetAddOptions<IFieldSymbol>(parameter, blockStatementOpt, typeDeclaration, options, cancellationToken));
C
Cyrus Najmabadi 已提交
459 460 461 462
                        }
                        else
                        {
                            throw ExceptionUtilities.Unreachable;
463 464 465 466
                        }
                    });
            }

C
Cyrus Najmabadi 已提交
467 468 469 470 471 472 473 474 475 476 477 478 479
            // Now that we've added any potential members, create an assignment between it
            // and the parameter.
            var initializationStatement = (TStatementSyntax)generator.ExpressionStatement(
                generator.AssignmentStatement(
                    generator.MemberAccessExpression(
                        generator.ThisExpression(),
                        generator.IdentifierName(fieldOrProperty.Name)),
                    generator.IdentifierName(parameter.Name)));

            // Attempt to place the initialization in a good location in the constructor
            // We'll want to keep initialization statements in the same order as we see
            // parameters for the constructor.
            var statementToAddAfterOpt = TryGetStatementToAddInitializationAfter(parameter, blockStatementOpt);
480

C
Cyrus Najmabadi 已提交
481
            InsertStatement(editor, constructorDeclaration, returnsVoid: true, statementToAddAfterOpt, initializationStatement);
482 483 484 485

            return document.WithSyntaxRoot(editor.GetChangedRoot());
        }

C
Cyrus Najmabadi 已提交
486
        private static CodeGenerationOptions GetAddOptions<TSymbol>(
487
            IParameterSymbol parameter, IBlockOperation? blockStatementOpt,
C
Cyrus Najmabadi 已提交
488
            SyntaxNode typeDeclaration, OptionSet options, CancellationToken cancellationToken)
489 490
            where TSymbol : ISymbol
        {
491
            foreach (var (sibling, before) in GetSiblingParameters(parameter))
492 493
            {
                var statement = TryFindFieldOrPropertyAssignmentStatement(
494
                    sibling, blockStatementOpt, out var fieldOrProperty);
495 496 497 498

                if (statement != null &&
                    fieldOrProperty is TSymbol symbol)
                {
C
Cyrus Najmabadi 已提交
499 500
                    var symbolSyntax = symbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
                    if (symbolSyntax.Ancestors().Contains(typeDeclaration))
501
                    {
C
Cyrus Najmabadi 已提交
502
                        if (before)
503 504 505
                        {
                            // Found an existing field/property that corresponds to a preceding parameter.
                            // Place ourselves directly after it.
C
Cyrus Najmabadi 已提交
506
                            return new CodeGenerationOptions(afterThisLocation: symbolSyntax.GetLocation(), options: options);
507
                        }
C
Cyrus Najmabadi 已提交
508
                        else
509
                        {
C
Cyrus Najmabadi 已提交
510 511
                            // Found an existing field/property that corresponds to a following parameter.
                            // Place ourselves directly before it.
C
Cyrus Najmabadi 已提交
512
                            return new CodeGenerationOptions(beforeThisLocation: symbolSyntax.GetLocation(), options: options);
513
                        }
514 515 516 517
                    }
                }
            }

C
Cyrus Najmabadi 已提交
518
            return new CodeGenerationOptions(options: options);
519 520
        }

521
        private static ImmutableArray<(IParameterSymbol parameter, bool before)> GetSiblingParameters(IParameterSymbol parameter)
522
        {
C
Cyrus Najmabadi 已提交
523
            using var _ = ArrayBuilder<(IParameterSymbol, bool before)>.GetInstance(out var siblings);
524

525
            if (parameter.ContainingSymbol is IMethodSymbol method)
526
            {
527 528 529 530 531
                var parameterIndex = method.Parameters.IndexOf(parameter);

                // look for an existing assignment for a parameter that comes before us.
                // If we find one, we'll add ourselves after that parameter check.
                for (var i = parameterIndex - 1; i >= 0; i--)
C
Cyrus Najmabadi 已提交
532
                    siblings.Add((method.Parameters[i], before: true));
533 534 535 536

                // look for an existing check for a parameter that comes before us.
                // If we find one, we'll add ourselves after that parameter check.
                for (var i = parameterIndex + 1; i < method.Parameters.Length; i++)
C
Cyrus Najmabadi 已提交
537
                    siblings.Add((method.Parameters[i], before: false));
538
            }
539

540 541 542 543 544 545 546 547 548
            return siblings.ToImmutable();
        }

        private SyntaxNode? TryGetStatementToAddInitializationAfter(
            IParameterSymbol parameter, IBlockOperation? blockStatementOpt)
        {
            // look for an existing assignment for a parameter that comes before/after us.
            // If we find one, we'll add ourselves before/after that parameter check.
            foreach (var (sibling, before) in GetSiblingParameters(parameter))
549
            {
550
                var statement = TryFindFieldOrPropertyAssignmentStatement(sibling, blockStatementOpt);
551 552
                if (statement != null)
                {
553 554 555 556 557 558 559 560 561
                    if (before)
                    {
                        return statement.Syntax;
                    }
                    else
                    {
                        var statementIndex = blockStatementOpt!.Operations.IndexOf(statement);
                        return statementIndex > 0 ? blockStatementOpt.Operations[statementIndex - 1].Syntax : null;
                    }
562 563 564
                }
            }

C
CyrusNajmabadi 已提交
565 566
            // We couldn't find a reasonable location for the new initialization statement.
            // Just place ourselves after the last statement in the constructor.
567
            return TryGetLastStatement(blockStatementOpt);
568 569
        }

570
        private static IOperation? TryFindFieldOrPropertyAssignmentStatement(IParameterSymbol parameter, IBlockOperation? blockStatementOpt)
571
            => TryFindFieldOrPropertyAssignmentStatement(parameter, blockStatementOpt, out _);
572

573
        private static IOperation? TryFindFieldOrPropertyAssignmentStatement(
574
            IParameterSymbol parameter, IBlockOperation? blockStatementOpt, out ISymbol? fieldOrProperty)
575
        {
576
            if (blockStatementOpt != null)
577
            {
578
                var containingType = parameter.ContainingType;
579
                foreach (var statement in blockStatementOpt.Operations)
580
                {
581 582 583 584 585 586
                    // look for something of the form:  "this.s = s" or "this.s = s ?? ..."
                    if (IsFieldOrPropertyAssignment(statement, containingType, out var assignmentExpression, out fieldOrProperty) &&
                        IsParameterReferenceOrCoalesceOfParameterReference(assignmentExpression, parameter))
                    {
                        return statement;
                    }
587 588 589
                }
            }

590
            fieldOrProperty = null;
591 592 593
            return null;
        }

C
CyrusNajmabadi 已提交
594
        private static bool IsParameterReferenceOrCoalesceOfParameterReference(
595
           IAssignmentOperation assignmentExpression, IParameterSymbol parameter)
C
CyrusNajmabadi 已提交
596 597 598 599 600 601 602 603
        {
            if (IsParameterReference(assignmentExpression.Value, parameter))
            {
                // We already have a member initialized with this parameter like:
                //      this.field = parameter
                return true;
            }

604 605
            if (UnwrapImplicitConversion(assignmentExpression.Value) is ICoalesceOperation coalesceExpression &&
                IsParameterReference(coalesceExpression.Value, parameter))
C
CyrusNajmabadi 已提交
606 607 608 609 610 611 612 613 614
            {
                // We already have a member initialized with this parameter like:
                //      this.field = parameter ?? ...
                return true;
            }

            return false;
        }

615 616
        private async Task<ISymbol?> TryFindMatchingUninitializedFieldOrPropertySymbolAsync(
            Document document, IParameterSymbol parameter, IBlockOperation? blockStatementOpt, ImmutableArray<NamingRule> rules, ImmutableArray<string> parameterWords, CancellationToken cancellationToken)
617
        {
C
CyrusNajmabadi 已提交
618 619 620
            // Look for a field/property that really looks like it corresponds to this parameter.
            // Use a variety of heuristics around the name/type to see if this is a match.

621
            var containingType = parameter.ContainingType;
622
            var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
C
CyrusNajmabadi 已提交
623 624 625 626 627 628 629

            // Walk through the naming rules against this parameter's name to see what
            // name the user would like for it as a member in this type.  Note that we
            // have some fallback rules that use the standard conventions around 
            // properties /fields so that can still find things even if the user has no
            // naming preferences set.

630 631 632 633 634
            foreach (var rule in rules)
            {
                var memberName = rule.NamingStyle.CreateName(parameterWords);
                foreach (var memberWithName in containingType.GetMembers(memberName))
                {
C
CyrusNajmabadi 已提交
635 636 637 638
                    // We found members in our type with that name.  If it's a writable
                    // field that we could assign this parameter to, and it's not already
                    // been assigned to, then this field is a good candidate for us to
                    // hook up to.
639 640 641
                    if (memberWithName is IFieldSymbol field &&
                        !field.IsConst &&
                        IsImplicitConversion(compilation, source: parameter.Type, destination: field.Type) &&
642
                        !ContainsMemberAssignment(blockStatementOpt, field))
643 644 645 646
                    {
                        return field;
                    }

C
CyrusNajmabadi 已提交
647 648 649
                    // If it's a writable property that we could assign this parameter to, and it's
                    // not already been assigned to, then this property is a good candidate for us to
                    // hook up to.
650 651 652
                    if (memberWithName is IPropertySymbol property &&
                        property.IsWritableInConstructor() &&
                        IsImplicitConversion(compilation, source: parameter.Type, destination: property.Type) &&
653
                        !ContainsMemberAssignment(blockStatementOpt, property))
654 655 656 657 658 659
                    {
                        return property;
                    }
                }
            }

C
CyrusNajmabadi 已提交
660 661
            // Couldn't find any existing member.  Just return nothing so we can offer to
            // create a member for them.
662 663 664
            return null;
        }

665
        private static bool ContainsMemberAssignment(
666
            IBlockOperation? blockStatementOpt, ISymbol member)
667
        {
668
            if (blockStatementOpt != null)
669
            {
670
                foreach (var statement in blockStatementOpt.Operations)
671
                {
672
                    if (IsFieldOrPropertyAssignment(statement, member.ContainingType, out var assignmentExpression) &&
673
                        UnwrapImplicitConversion(assignmentExpression.Target) is IMemberReferenceOperation memberReference &&
674 675 676 677
                        member.Equals(memberReference.Member))
                    {
                        return true;
                    }
678 679 680 681 682 683
                }
            }

            return false;
        }
    }
S
Sam Harwell 已提交
684
}