CSharpInlineDeclarationCodeFixProvider.cs 19.8 KB
Newer Older
1 2 3
// 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;
4
using System.Collections.Generic;
5 6 7 8 9 10 11
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
12
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
13
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
14
using Microsoft.CodeAnalysis.CSharp.Extensions;
15 16 17 18
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
19
using Microsoft.CodeAnalysis.Options;
A
Andrew Hall (METAL) 已提交
20
using Microsoft.CodeAnalysis.PooledObjects;
21
using Microsoft.CodeAnalysis.Shared.Extensions;
22
using Microsoft.CodeAnalysis.Shared.Utilities;
23
using Microsoft.CodeAnalysis.Text;
24 25 26 27 28
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration
{
    [ExportCodeFixProvider(LanguageNames.CSharp), Shared]
29
    internal partial class CSharpInlineDeclarationCodeFixProvider : SyntaxEditorBasedCodeFixProvider
30
    {
31 32 33 34 35
        [ImportingConstructor]
        public CSharpInlineDeclarationCodeFixProvider()
        {
        }

36 37 38
        public override ImmutableArray<string> FixableDiagnosticIds
            => ImmutableArray.Create(IDEDiagnosticIds.InlineDeclarationDiagnosticId);

39 40
        internal sealed override CodeFixCategory CodeFixCategory => CodeFixCategory.CodeStyle;

41 42 43 44 45
        public override Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            context.RegisterCodeFix(new MyCodeAction(
                c => FixAsync(context.Document, context.Diagnostics.First(), c)),
                context.Diagnostics);
46
            return Task.CompletedTask;
47 48
        }

49
        protected override async Task FixAllAsync(
50
            Document document, ImmutableArray<Diagnostic> diagnostics,
51
            SyntaxEditor editor, CancellationToken cancellationToken)
52
        {
53
            var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
C
CyrusNajmabadi 已提交
54

55 56 57 58 59 60 61 62
            // Gather all statements to be removed
            // We need this to find the statements we can safely attach trivia to
            var declarationsToRemove = new HashSet<StatementSyntax>();
            foreach (var diagnostic in diagnostics)
            {
                declarationsToRemove.Add((LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken).Parent.Parent);
            }

63 64
            // Attempt to use an out-var declaration if that's the style the user prefers.
            // Note: if using 'var' would cause a problem, we will use the actual type
C
CyrusNajmabadi 已提交
65
            // of the local.  This is necessary in some cases (for example, when the
66
            // type of the out-var-decl affects overload resolution or generic instantiation).
67
            var originalRoot = editor.OriginalRoot;
68

A
Andrew Hall (METAL) 已提交
69
            var originalNodes = diagnostics.SelectAsArray(diagnostic => FindDiagnosticNodes(document, diagnostic, options, cancellationToken));
70 71 72

            await editor.ApplyExpressionLevelSemanticEditsAsync(
                document,
A
Andrew Hall (METAL) 已提交
73
                originalNodes,
A
Andrew Hall (METAL) 已提交
74 75
                t =>
                {
76
                    using var additionalNodesToTrack = ArrayBuilder<SyntaxNode>.GetInstance(2);
A
Andrew Hall (METAL) 已提交
77 78 79
                    additionalNodesToTrack.Add(t.identifier);
                    additionalNodesToTrack.Add(t.declarator);

80
                    return (t.invocationOrCreation, additionalNodesToTrack.ToImmutable());
A
Andrew Hall (METAL) 已提交
81
                },
82
                (_1, _2, _3) => true,
T
Tom Needham 已提交
83
                (semanticModel, currentRoot, t, currentNode)
A
Andrew Hall (METAL) 已提交
84
                    => ReplaceIdentifierWithInlineDeclaration(
T
Tom Needham 已提交
85
                        options, semanticModel, currentRoot, t.declarator,
A
Andrew Hall (METAL) 已提交
86
                        t.identifier, t.invocationOrCreation, currentNode, declarationsToRemove),
87
                cancellationToken).ConfigureAwait(false);
88 89
        }

A
Andrew Hall (METAL) 已提交
90
        private (VariableDeclaratorSyntax declarator, IdentifierNameSyntax identifier, SyntaxNode invocationOrCreation) FindDiagnosticNodes(
A
Andrew Hall (METAL) 已提交
91 92
                    Document document, Diagnostic diagnostic,
                    OptionSet options, CancellationToken cancellationToken)
93
        {
94 95 96 97 98
            // Recover the nodes we care about.
            var declaratorLocation = diagnostic.AdditionalLocations[0];
            var identifierLocation = diagnostic.AdditionalLocations[1];
            var invocationOrCreationLocation = diagnostic.AdditionalLocations[2];
            var outArgumentContainingStatementLocation = diagnostic.AdditionalLocations[3];
C
CyrusNajmabadi 已提交
99

100 101 102 103
            var declarator = (VariableDeclaratorSyntax)declaratorLocation.FindNode(cancellationToken);
            var identifier = (IdentifierNameSyntax)identifierLocation.FindNode(cancellationToken);
            var invocationOrCreation = (ExpressionSyntax)invocationOrCreationLocation.FindNode(
                getInnermostNodeForTie: true, cancellationToken: cancellationToken);
A
Andrew Hall (METAL) 已提交
104 105 106 107

            return (declarator, identifier, invocationOrCreation);
        }

108
        private SyntaxNode ReplaceIdentifierWithInlineDeclaration(
A
Andrew Hall (METAL) 已提交
109 110 111
            OptionSet options, SemanticModel semanticModel,
            SyntaxNode currentRoot, VariableDeclaratorSyntax declarator,
            IdentifierNameSyntax identifier, SyntaxNode invocationOrCreation,
A
Andrew Hall (METAL) 已提交
112
            SyntaxNode currentNode, HashSet<StatementSyntax> declarationsToRemove)
A
Andrew Hall (METAL) 已提交
113 114 115 116 117 118
        {
            declarator = currentRoot.GetCurrentNode(declarator);
            identifier = currentRoot.GetCurrentNode(identifier);

            var editor = new SyntaxEditor(currentRoot, CSharpSyntaxGenerator.Instance);
            var sourceText = currentRoot.GetText();
119

120 121
            var declaration = (VariableDeclarationSyntax)declarator.Parent;
            var singleDeclarator = declaration.Variables.Count == 1;
122

123 124
            if (singleDeclarator)
            {
125 126
                // This was a local statement with a single variable in it.  Just Remove
                // the entire local declaration statement. Note that comments belonging to
127
                // this local statement will be moved to be above the statement containing
128
                // the out-var.
129 130 131 132
                var localDeclarationStatement = (LocalDeclarationStatementSyntax)declaration.Parent;
                var block = (BlockSyntax)localDeclarationStatement.Parent;
                var declarationIndex = block.Statements.IndexOf(localDeclarationStatement);

133 134 135
                // Try to find a predecessor Statement on the same line that isn't going to be removed
                StatementSyntax priorStatementSyntax = null;
                var localDeclarationToken = localDeclarationStatement.GetFirstToken();
C
Cyrus Najmabadi 已提交
136
                for (var i = declarationIndex - 1; i >= 0; i--)
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
                {
                    var statementSyntax = block.Statements[i];
                    if (declarationsToRemove.Contains(statementSyntax))
                    {
                        continue;
                    }

                    if (sourceText.AreOnSameLine(statementSyntax.GetLastToken(), localDeclarationToken))
                    {
                        priorStatementSyntax = statementSyntax;
                    }

                    break;
                }

                if (priorStatementSyntax != null)
153
                {
154 155 156 157 158 159
                    // There's another statement on the same line as this declaration statement.
                    // i.e.   int a; int b;
                    //
                    // Just move all trivia from our statement to be trailing trivia of the previous
                    // statement
                    editor.ReplaceNode(
160
                        priorStatementSyntax,
161
                        (s, g) => s.WithAppendedTrailingTrivia(localDeclarationStatement.GetTrailingTrivia()));
162 163 164
                }
                else
                {
165
                    // Trivia on the local declaration will move to the next statement.
166
                    // use the callback form as the next statement may be the place where we're
167
                    // inlining the declaration, and thus need to see the effects of that change.
168 169 170 171 172

                    // Find the next Statement that isn't going to be removed.
                    // We initialize this to null here but we must see at least the statement
                    // into which the declaration is going to be inlined so this will be not null
                    StatementSyntax nextStatementSyntax = null;
C
Cyrus Najmabadi 已提交
173
                    for (var i = declarationIndex + 1; i < block.Statements.Count; i++)
174 175 176 177 178 179 180 181 182
                    {
                        var statement = block.Statements[i];
                        if (!declarationsToRemove.Contains(statement))
                        {
                            nextStatementSyntax = statement;
                            break;
                        }
                    }

183
                    editor.ReplaceNode(
184
                        nextStatementSyntax,
185 186 187 188 189 190 191 192 193 194 195 196 197
                        (s, g) => s.WithPrependedNonIndentationTriviaFrom(localDeclarationStatement));
                }

                editor.RemoveNode(localDeclarationStatement, SyntaxRemoveOptions.KeepUnbalancedDirectives);
            }
            else
            {
                // Otherwise, just remove the single declarator. Note: we'll move the comments
                // 'on' the declarator to the out-var location.  This is a little bit trickier
                // than normal due to how our comment-association rules work.  i.e. if you have:
                //
                //      var /*c1*/ i /*c2*/, /*c3*/ j /*c4*/;
                //
198 199
                // In this case 'c1' is owned by the 'var' token, not 'i', and 'c3' is owned by
                // the comment token not 'j'.
200

201 202 203 204 205 206 207 208 209 210 211
                editor.RemoveNode(declarator);
                if (declarator == declaration.Variables[0])
                {
                    // If we're removing the first declarator, and it's on the same line
                    // as the previous token, then we want to remove all the trivia belonging
                    // to the previous token.  We're going to move it along with this declarator.
                    // If we don't, then the comment will stay with the previous token.
                    //
                    // Note that the moving of the comment happens later on when we make the
                    // declaration expression.
                    if (sourceText.AreOnSameLine(declarator.GetFirstToken(), declarator.GetFirstToken().GetPreviousToken(includeSkipped: true)))
212
                    {
213 214 215
                        editor.ReplaceNode(
                            declaration.Type,
                            (t, g) => t.WithTrailingTrivia(SyntaxFactory.ElasticSpace).WithoutAnnotations(Formatter.Annotation));
216 217
                    }
                }
218
            }
219

220 221 222 223 224 225 226
            // get the type that we want to put in the out-var-decl based on the user's options.
            // i.e. prefer 'out var' if that is what the user wants.  Note: if we have:
            //
            //      Method(out var x)
            //
            // Then the type is not-apparent, and we should not use var if the user only wants
            // it for apparent types
227

228 229
            var local = (ILocalSymbol)semanticModel.GetDeclaredSymbol(declarator);
            var newType = GenerateTypeSyntaxOrVar(local.Type, options);
230

231 232
            var declarationExpression = GetDeclarationExpression(
                sourceText, identifier, newType, singleDeclarator ? null : declarator);
233

234
            // Check if using out-var changed problem semantics.
A
Andrew Hall (METAL) 已提交
235
            var semanticsChanged = SemanticsChanged(semanticModel, currentRoot, currentNode, identifier, declarationExpression);
236
            if (semanticsChanged)
237
            {
238 239
                // Switching to 'var' changed semantics.  Just use the original type of the local.

240 241 242
                // If the user originally wrote it something other than 'var', then use what they
                // wrote.  Otherwise, synthesize the actual type of the local.
                var explicitType = declaration.Type.IsVar ? local.Type?.GenerateTypeSyntax() : declaration.Type;
A
Andrew Hall (METAL) 已提交
243
                declarationExpression = SyntaxFactory.DeclarationExpression(explicitType, declarationExpression.Designation);
244
            }
245

A
Andrew Hall (METAL) 已提交
246
            editor.ReplaceNode(identifier, declarationExpression);
247 248

            return editor.GetChangedRoot();
249 250
        }

251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        public static TypeSyntax GenerateTypeSyntaxOrVar(
           ITypeSymbol symbol, OptionSet options)
        {
            var useVar = IsVarDesired(symbol, options);

            // Note: we cannot use ".GenerateTypeSyntax()" only here.  that's because we're
            // actually creating a DeclarationExpression and currently the Simplifier cannot
            // analyze those due to limitations between how it uses Speculative SemanticModels
            // and how those don't handle new declarations well.
            return useVar
                ? SyntaxFactory.IdentifierName("var")
                : symbol.GenerateTypeSyntax();
        }

        private static bool IsVarDesired(ITypeSymbol type, OptionSet options)
        {
            // If they want it for intrinsics, and this is an intrinsic, then use var.
            if (type.IsSpecialType() == true)
            {
C
Cyrus Najmabadi 已提交
270
                return options.GetOption(CSharpCodeStyleOptions.VarForBuiltInTypes).Value;
271 272 273
            }

            // If they want "var" whenever possible, then use "var".
C
Cyrus Najmabadi 已提交
274
            return options.GetOption(CSharpCodeStyleOptions.VarElsewhere).Value;
275 276
        }

277
        private static DeclarationExpressionSyntax GetDeclarationExpression(
278 279
            SourceText sourceText, IdentifierNameSyntax identifier,
            TypeSyntax newType, VariableDeclaratorSyntax declaratorOpt)
280 281
        {
            newType = newType.WithoutTrivia().WithAdditionalAnnotations(Formatter.Annotation);
282 283 284 285
            var designation = SyntaxFactory.SingleVariableDesignation(identifier.Identifier);

            if (declaratorOpt != null)
            {
286 287
                // We're removing a single declarator.  Copy any comments it has to the out-var.
                //
C
CyrusNajmabadi 已提交
288
                // Note: this is tricky due to comment ownership.  We want the comments that logically
289
                // belong to the declarator, even if our syntax model attaches them to other tokens.
290 291 292 293
                var precedingTrivia = declaratorOpt.GetAllPrecedingTriviaToPreviousToken(
                    sourceText, includePreviousTokenTrailingTriviaOnlyIfOnSameLine: true);
                if (precedingTrivia.Any(t => t.IsSingleOrMultiLineComment()))
                {
294
                    designation = designation.WithPrependedLeadingTrivia(MassageTrivia(precedingTrivia));
295 296 297 298
                }

                if (declaratorOpt.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment()))
                {
299
                    designation = designation.WithAppendedTrailingTrivia(MassageTrivia(declaratorOpt.GetTrailingTrivia()));
300 301 302
                }
            }

303
            return SyntaxFactory.DeclarationExpression(newType, designation);
304
        }
305

306 307 308 309 310 311 312 313 314 315 316
        private static IEnumerable<SyntaxTrivia> MassageTrivia(IEnumerable<SyntaxTrivia> triviaList)
        {
            foreach (var trivia in triviaList)
            {
                if (trivia.IsSingleOrMultiLineComment())
                {
                    yield return trivia;
                }
                else if (trivia.IsWhitespace())
                {
                    // Condense whitespace down to single spaces. We don't want things like
C
CyrusNajmabadi 已提交
317
                    // indentation spaces to be inserted in the out-var location.  It is appropriate
318 319 320 321 322 323 324
                    // though to have single spaces to help separate out things like comments and
                    // tokens though.
                    yield return SyntaxFactory.Space;
                }
            }
        }

325 326 327 328 329
        private bool SemanticsChanged(
            SemanticModel semanticModel,
            SyntaxNode root,
            SyntaxNode nodeToReplace,
            IdentifierNameSyntax identifier,
A
Andrew Hall (METAL) 已提交
330
            DeclarationExpressionSyntax declarationExpression)
331
        {
332
            if (declarationExpression.Type.IsVar)
333 334
            {
                // Options want us to use 'var' if we can.  Make sure we didn't change
C
CyrusNajmabadi 已提交
335
                // the semantics of the call by doing this.
336 337

                // Find the symbol that the existing invocation points to.
338
                var previousSymbol = semanticModel.GetSymbolInfo(nodeToReplace).Symbol;
339

340 341 342
                // Now, create a speculative model in which we make the change.  Make sure
                // we still point to the same symbol afterwards.

343
                var topmostContainer = GetTopmostContainer(nodeToReplace);
344 345 346 347 348 349 350 351
                if (topmostContainer == null)
                {
                    // Couldn't figure out what we were contained in.  Have to assume that semantics
                    // Are changing.
                    return true;
                }

                var annotation = new SyntaxAnnotation();
352 353
                var updatedTopmostContainer = topmostContainer.ReplaceNode(
                    nodeToReplace, nodeToReplace.ReplaceNode(identifier, declarationExpression)
354 355 356 357 358 359 360 361 362 363 364
                                                              .WithAdditionalAnnotations(annotation));

                if (!TryGetSpeculativeSemanticModel(semanticModel,
                        topmostContainer.SpanStart, updatedTopmostContainer, out var speculativeModel))
                {
                    // Couldn't figure out the new semantics.  Assume semantics changed.
                    return true;
                }

                var updatedInvocationOrCreation = updatedTopmostContainer.GetAnnotatedNodes(annotation).Single();
                var updatedSymbolInfo = speculativeModel.GetSymbolInfo(updatedInvocationOrCreation);
365

366
                if (!SymbolEquivalenceComparer.Instance.Equals(previousSymbol, updatedSymbolInfo.Symbol))
367 368 369 370 371 372 373
                {
                    // We're pointing at a new symbol now.  Semantic have changed.
                    return true;
                }
            }

            return false;
374 375
        }

376
        private SyntaxNode GetTopmostContainer(SyntaxNode expression)
377 378 379 380 381 382 383 384 385
        {
            return expression.GetAncestorsOrThis(
                a => a is StatementSyntax ||
                     a is EqualsValueClauseSyntax ||
                     a is ArrowExpressionClauseSyntax ||
                     a is ConstructorInitializerSyntax).LastOrDefault();
        }

        private bool TryGetSpeculativeSemanticModel(
386
            SemanticModel semanticModel,
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
            int position, SyntaxNode topmostContainer,
            out SemanticModel speculativeModel)
        {
            switch (topmostContainer)
            {
                case StatementSyntax statement:
                    return semanticModel.TryGetSpeculativeSemanticModel(position, statement, out speculativeModel);
                case EqualsValueClauseSyntax equalsValue:
                    return semanticModel.TryGetSpeculativeSemanticModel(position, equalsValue, out speculativeModel);
                case ArrowExpressionClauseSyntax arrowExpression:
                    return semanticModel.TryGetSpeculativeSemanticModel(position, arrowExpression, out speculativeModel);
                case ConstructorInitializerSyntax constructorInitializer:
                    return semanticModel.TryGetSpeculativeSemanticModel(position, constructorInitializer, out speculativeModel);
            }

            speculativeModel = null;
            return false;
        }

406 407
        private class MyCodeAction : CodeAction.DocumentChangeAction
        {
408
            public MyCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument)
409 410 411
                : base(FeaturesResources.Inline_variable_declaration,
                       createChangedDocument,
                       FeaturesResources.Inline_variable_declaration)
412 413 414 415
            {
            }
        }
    }
S
Sam Harwell 已提交
416
}