diff --git a/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests.cs b/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests.cs index c819c4aad6ad3..96306663905c7 100644 --- a/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests.cs +++ b/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.CSharp.UseAutoProperty; using Microsoft.CodeAnalysis.Diagnostics; @@ -1877,7 +1878,7 @@ class Class [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/23216")] [WorkItem("https://github.com/dotnet/roslyn/issues/23215")] - public async Task TestFixAllInDocument() + public async Task TestFixAllInDocument1() { await TestInRegularAndScript1Async( """ @@ -1914,6 +1915,53 @@ class Class """); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/26527")] + public async Task TestFixAllInDocument2() + { + await TestInRegularAndScript1Async( + """ + internal struct StringFormat + { + private readonly object {|FixAllInDocument:_argument1|}; + private readonly object _argument2; + private readonly object _argument3; + private readonly object[] _arguments; + + public object Argument1 + { + get { return _argument1; } + } + + public object Argument2 + { + get { return _argument2; } + } + + public object Argument3 + { + get { return _argument3; } + } + + public object[] Arguments + { + get { return _arguments; } + } + } + """, + """ + internal struct StringFormat + { + public object Argument1 { get; } + + public object Argument2 { get; } + + public object Argument3 { get; } + + public object[] Arguments { get; } + } + """); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/23735")] public async Task ExplicitInterfaceImplementationGetterOnly() { diff --git a/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests_Field.cs b/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests_Field.cs index f738c92812ca5..85687a5791c4b 100644 --- a/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests_Field.cs +++ b/src/Analyzers/CSharp/Tests/UseAutoProperty/UseAutoPropertyTests_Field.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; @@ -68,6 +69,38 @@ string P """, parseOptions: Preview); } + [Fact] + public async Task TestFieldWithInitializer() + { + await TestInRegularAndScriptAsync( + """ + class Class + { + [|string s = ""|]; + + string P + { + get + { + return s.Trim(); + } + } + } + """, + """ + class Class + { + string P + { + get + { + return field.Trim(); + } + } = ""; + } + """, parseOptions: Preview); + } + [Fact] public async Task TestFieldAccessOffOfThis() { @@ -1471,4 +1504,39 @@ void M(ref int i) { } } """, parseOptions: Preview); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/26527")] + public async Task TestFixAllInDocument3() + { + await TestInRegularAndScript1Async( + """ + using System; + + public sealed class SomeViewModel + { + private bool {|FixAllInDocument:a|} = true; + public bool A { get => a; set => Set(ref a, value); } + + private bool b = true; + public bool B { get => b; set => Set(ref b, value); } + + private bool c = true; + public bool C { get => c; set => Set(ref c, value); } + + private void Set(ref T field, T value) => throw new NotImplementedException(); + } + """, + """ + using System; + + public sealed class SomeViewModel + { + public bool A { get; set => Set(ref field, value); } = true; + public bool B { get; set => Set(ref field, value); } = true; + public bool C { get; set => Set(ref field, value); } = true; + + private void Set(ref T field, T value) => throw new NotImplementedException(); + } + """, new TestParameters(parseOptions: Preview)); + } } diff --git a/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests.vb b/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests.vb index 4550fce1c2846..c640397a1ce32 100644 --- a/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests.vb @@ -3147,7 +3147,7 @@ Class C End Function - + Public Async Function TestMatchWithTurkishIWorkaround9() As Task Using New CultureContext(New Globalization.CultureInfo("tr-TR", useUserOverride:=False)) Using state = TestStateFactory.CreateVisualBasicTestState( @@ -3165,13 +3165,16 @@ Class C $$]]>) state.SendTypeChars("IF") Await state.WaitForAsynchronousOperationsAsync() - Await state.AssertSelectedCompletionItem("If") + + ' ICustomFormatter is better than `if` as the user used full-caps, which makes it more likely that + ' they wanted to camel case match + Await state.AssertSelectedCompletionItem("ICustomFormatter") End Using End Using End Function - + Public Async Function TestMatchWithTurkishIWorkaround10() As Task Using New CultureContext(New Globalization.CultureInfo("tr-TR", useUserOverride:=False)) Using state = TestStateFactory.CreateVisualBasicTestState( @@ -3190,7 +3193,10 @@ Class C ]]>) state.SendTypeChars("IF") Await state.WaitForAsynchronousOperationsAsync() - Await state.AssertSelectedCompletionItem("If") + + ' ICustomFormatter is better than `if` as the user used full-caps, which makes it more likely that + ' they wanted to camel case match + Await state.AssertSelectedCompletionItem("ICustomFormatter") End Using End Using End Function diff --git a/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs b/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs index 8a9422c4e0a2a..a14ed3349d2bb 100644 --- a/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs @@ -32,6 +32,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UseAutoProperty; [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] internal sealed partial class CSharpUseAutoPropertyCodeFixProvider() : AbstractUseAutoPropertyCodeFixProvider< + CSharpUseAutoPropertyCodeFixProvider, TypeDeclarationSyntax, PropertyDeclarationSyntax, VariableDeclaratorSyntax, @@ -130,8 +131,8 @@ protected override Task UpdatePropertyAsync( // Move any field initializer over to the property as well. if (fieldInitializer != null) { - updatedProperty = updatedProperty - .WithInitializer(EqualsValueClause(fieldInitializer)) + updatedProperty = updatedProperty.WithoutTrailingTrivia() + .WithInitializer(EqualsValueClause(EqualsToken.WithLeadingTrivia(Space).WithTrailingTrivia(Space), fieldInitializer)) .WithSemicolonToken(SemicolonToken); } diff --git a/src/Features/Core/Portable/Completion/CompletionHelper.cs b/src/Features/Core/Portable/Completion/CompletionHelper.cs index 82cf6c6c95b04..0273fbcd2a23d 100644 --- a/src/Features/Core/Portable/Completion/CompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/CompletionHelper.cs @@ -124,13 +124,13 @@ private static int CompareItems( return expandedDiff; } - // Then see how the two items compare in a case insensitive fashion. Matches that - // are strictly better (ignoring case) should prioritize the item. i.e. if we have - // a prefix match, that should always be better than a substring match. + // Then see how the two items compare in a case insensitive fashion. Matches that are strictly better (ignoring + // case) should prioritize the item. i.e. if we have a prefix match, that should always be better than a + // substring match. // - // The reason we ignore case is that it's very common for people to type expecting - // completion to fix up their casing. i.e. 'false' will be written with the - // expectation that it will get fixed by the completion list to 'False'. + // The reason we ignore case is that it's very common for people to type expecting completion to fix up their + // casing. i.e. 'false' will be written with the expectation that it will get fixed by the completion list to + // 'False'. var caseInsensitiveComparison = match1.CompareTo(match2, ignoreCase: true); if (caseInsensitiveComparison != 0) { @@ -139,17 +139,18 @@ private static int CompareItems( // Now we have two items match in case-insensitive manner, // - // 1. if we are in a case-insensitive language, we'd first check if either item has the MatchPriority set to one of - // the two special values ("Preselect" and "Deprioritize"). If so and these two items have different MatchPriority, - // then we'd select the one of "Preselect", or the one that's not of "Deprioritize". Otherwise we will prefer the one - // matches case-sensitively. This is to make sure common items in VB like "True" and "False" are prioritized for selection - // when user types "t" and "f" (see https://github.com/dotnet/roslyn/issues/4892) + // 1. if we are in a case-insensitive language, we'd first check if either item has the MatchPriority set to one + // of the two special values ("Preselect" and "Deprioritize"). If so and these two items have different + // MatchPriority, then we'd select the one of "Preselect", or the one that's not of "Deprioritize". Otherwise + // we will prefer the one matches case-sensitively. This is to make sure common items in VB like "True" and + // "False" are prioritized for selection when user types "t" and "f" (see + // https://github.com/dotnet/roslyn/issues/4892) // - // 2. or similarly, if the filter text contains only lowercase letters, we want to relax our filtering standard a tiny - // bit to account for the sceanrio that users expect completion to fix the casing. This only happens if one of the item's - // MatchPriority is "Deprioritize". Otherwise we will always prefer the one matches case-sensitively. - // This is to make sure uncommon items like conversion "(short)" are not selected over `Should` when user types `sho` - // (see https://github.com/dotnet/roslyn/issues/55546) + // 2. or similarly, if the filter text contains only lowercase letters, we want to relax our filtering standard + // a tiny bit to account for the scenario that users expect completion to fix the casing. This only happens + // if one of the item's MatchPriority is "Deprioritize". Otherwise we will always prefer the one matches + // case-sensitively. This is to make sure uncommon items like conversion "(short)" are not selected over + // `Should` when user types `sho` (see https://github.com/dotnet/roslyn/issues/55546) var specialMatchPriorityValuesDiff = 0; if (!isCaseSensitive) diff --git a/src/Features/Core/Portable/Completion/MatchResult.cs b/src/Features/Core/Portable/Completion/MatchResult.cs index c358a7e8d6833..7a91624fcbed0 100644 --- a/src/Features/Core/Portable/Completion/MatchResult.cs +++ b/src/Features/Core/Portable/Completion/MatchResult.cs @@ -80,4 +80,7 @@ public int Compare(MatchResult x, MatchResult y) return x.IndexInOriginalSortedOrder - y.IndexInOriginalSortedOrder; } } + + public override string ToString() + => this.CompletionItem.ToString(); } diff --git a/src/Features/Core/Portable/UseAutoProperty/AbstractUseAutoPropertyCodeFixProvider.cs b/src/Features/Core/Portable/UseAutoProperty/AbstractUseAutoPropertyCodeFixProvider.cs index 77ef502baac75..77bc02712d424 100644 --- a/src/Features/Core/Portable/UseAutoProperty/AbstractUseAutoPropertyCodeFixProvider.cs +++ b/src/Features/Core/Portable/UseAutoProperty/AbstractUseAutoPropertyCodeFixProvider.cs @@ -24,7 +24,9 @@ namespace Microsoft.CodeAnalysis.UseAutoProperty; using static UseAutoPropertiesHelpers; -internal abstract class AbstractUseAutoPropertyCodeFixProvider : CodeFixProvider +internal abstract partial class AbstractUseAutoPropertyCodeFixProvider + : CodeFixProvider + where TProvider : AbstractUseAutoPropertyCodeFixProvider where TTypeDeclarationSyntax : SyntaxNode where TPropertyDeclaration : SyntaxNode where TVariableDeclarator : SyntaxNode @@ -36,7 +38,8 @@ internal abstract class AbstractUseAutoPropertyCodeFixProvider FixableDiagnosticIds => [IDEDiagnosticIds.UseAutoPropertyDiagnosticId]; - public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + public sealed override FixAllProvider GetFixAllProvider() + => new UseAutoPropertyFixAllProvider((TProvider)this); protected abstract TPropertyDeclaration GetPropertyDeclaration(SyntaxNode node); protected abstract SyntaxNode GetNodeToRemove(TVariableDeclarator declarator); @@ -60,6 +63,8 @@ protected abstract Task UpdatePropertyAsync( public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { + var solution = context.Document.Project.Solution; + foreach (var diagnostic in context.Diagnostics) { var priority = diagnostic.Severity == DiagnosticSeverity.Hidden @@ -68,7 +73,7 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix(CodeAction.SolutionChangeAction.Create( AnalyzersResources.Use_auto_property, - cancellationToken => ProcessResultAsync(context, diagnostic, cancellationToken), + cancellationToken => ProcessResultAsync(solution, solution, diagnostic, cancellationToken), equivalenceKey: nameof(AnalyzersResources.Use_auto_property), priority), diagnostic); @@ -77,23 +82,19 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) return Task.CompletedTask; } - private async Task ProcessResultAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken) + private async Task ProcessResultAsync( + Solution originalSolution, Solution currentSolution, Diagnostic diagnostic, CancellationToken cancellationToken) { - var locations = diagnostic.AdditionalLocations; + var (field, property) = await MapDiagnosticToCurrentSolutionAsync( + diagnostic, originalSolution, currentSolution, cancellationToken).ConfigureAwait(false); - var propertyLocation = locations[0]; - var declaratorLocation = locations[1]; + if (field == null || property == null) + return currentSolution; - var solution = context.Document.Project.Solution; - var declarator = (TVariableDeclarator)declaratorLocation.FindNode(cancellationToken); - var fieldDocument = solution.GetRequiredDocument(declarator.SyntaxTree); - var fieldSemanticModel = await fieldDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var fieldSymbol = (IFieldSymbol)fieldSemanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); + var locations = diagnostic.AdditionalLocations; - var property = GetPropertyDeclaration(propertyLocation.FindNode(cancellationToken)); - var propertyDocument = solution.GetRequiredDocument(property.SyntaxTree); - var propertySemanticModel = await propertyDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var propertySymbol = (IPropertySymbol)propertySemanticModel.GetRequiredDeclaredSymbol(property, cancellationToken); + var fieldDocument = currentSolution.GetRequiredDocument(field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).SyntaxTree); + var propertyDocument = currentSolution.GetRequiredDocument(property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).SyntaxTree); var isTrivialGetAccessor = diagnostic.Properties.ContainsKey(IsTrivialGetAccessor); var isTrivialSetAccessor = diagnostic.Properties.ContainsKey(IsTrivialSetAccessor); @@ -105,24 +106,27 @@ private async Task ProcessResultAsync(CodeFixContext context, Diagnost var renameOptions = new SymbolRenameOptions(); var fieldLocations = await Renamer.FindRenameLocationsAsync( - solution, fieldSymbol, renameOptions, cancellationToken).ConfigureAwait(false); + currentSolution, field, renameOptions, cancellationToken).ConfigureAwait(false); + + var declarator = (TVariableDeclarator)field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + var propertyDeclaration = GetPropertyDeclaration(property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken)); // First, create the updated property we want to replace the old property with var isWrittenToOutsideOfConstructor = IsWrittenToOutsideOfConstructorOrProperty( - fieldSymbol, fieldLocations, property, cancellationToken); + field, fieldLocations, propertyDeclaration, cancellationToken); if (!isTrivialGetAccessor || - (propertySymbol.SetMethod != null && !isTrivialSetAccessor)) + (property.SetMethod != null && !isTrivialSetAccessor)) { // We have at least a non-trivial getter/setter. Those will not be rewritten to `get;/set;`. As such, we // need to update the property to reference `field` or itself instead of the actual field. - property = RewriteFieldReferencesInProperty(property, fieldLocations, cancellationToken); + propertyDeclaration = RewriteFieldReferencesInProperty(propertyDeclaration, fieldLocations, cancellationToken); } var updatedProperty = await UpdatePropertyAsync( propertyDocument, compilation, - fieldSymbol, propertySymbol, - declarator, property, + field, property, + declarator, propertyDeclaration, isWrittenToOutsideOfConstructor, isTrivialGetAccessor, isTrivialSetAccessor, cancellationToken).ConfigureAwait(false); @@ -161,31 +165,31 @@ private async Task ProcessResultAsync(CodeFixContext context, Diagnost var filteredLocations = fieldLocations.Filter( (documentId, span) => - fieldDocument.Id == documentId ? !span.IntersectsWith(declaratorLocation.SourceSpan) : true && // The span check only makes sense if we are in the same file - CanEditDocument(solution, documentId, linkedFiles, canEdit)); + fieldDocument.Id == documentId ? !span.IntersectsWith(declarator.Span) : true && // The span check only makes sense if we are in the same file + CanEditDocument(currentSolution, documentId, linkedFiles, canEdit)); var resolution = await filteredLocations.ResolveConflictsAsync( - fieldSymbol, propertySymbol.Name, - nonConflictSymbolKeys: [propertySymbol.GetSymbolKey(cancellationToken)], + field, property.Name, + nonConflictSymbolKeys: [property.GetSymbolKey(cancellationToken)], cancellationToken).ConfigureAwait(false); Contract.ThrowIfFalse(resolution.IsSuccessful); - solution = resolution.NewSolution; + currentSolution = resolution.NewSolution; // Now find the field and property again post rename. - fieldDocument = solution.GetRequiredDocument(fieldDocument.Id); - propertyDocument = solution.GetRequiredDocument(propertyDocument.Id); + fieldDocument = currentSolution.GetRequiredDocument(fieldDocument.Id); + propertyDocument = currentSolution.GetRequiredDocument(propertyDocument.Id); Debug.Assert(fieldDocument.Project == propertyDocument.Project); compilation = await fieldDocument.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - fieldSymbol = (IFieldSymbol?)fieldSymbol.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol; - propertySymbol = (IPropertySymbol?)propertySymbol.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol; - Contract.ThrowIfTrue(fieldSymbol == null || propertySymbol == null); + field = (IFieldSymbol?)field.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol; + property = (IPropertySymbol?)property.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol; + Contract.ThrowIfTrue(field == null || property == null); - declarator = (TVariableDeclarator)await fieldSymbol.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - property = GetPropertyDeclaration(await propertySymbol.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false)); + declarator = (TVariableDeclarator)field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + propertyDeclaration = GetPropertyDeclaration(property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken)); var nodeToRemove = GetNodeToRemove(declarator); @@ -222,7 +226,7 @@ private async Task ProcessResultAsync(CodeFixContext context, Diagnost { var syntaxFacts = fieldDocument.GetRequiredLanguageService(); var bannerService = fieldDocument.GetRequiredLanguageService(); - if (WillRemoveFirstFieldInTypeDirectlyAboveProperty(syntaxFacts, property, nodeToRemove) && + if (WillRemoveFirstFieldInTypeDirectlyAboveProperty(syntaxFacts, propertyDeclaration, nodeToRemove) && bannerService.GetLeadingBlankLines(nodeToRemove).Length == 0) { updatedProperty = bannerService.GetNodeWithoutLeadingBlankLines(updatedProperty); @@ -236,13 +240,13 @@ private async Task ProcessResultAsync(CodeFixContext context, Diagnost var declaratorTreeRoot = await fieldDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var editor = new SyntaxEditor(declaratorTreeRoot, fieldDocument.Project.Solution.Services); - editor.ReplaceNode(property, updatedProperty); + editor.ReplaceNode(propertyDeclaration, updatedProperty); editor.RemoveNode(nodeToRemove, syntaxRemoveOptions); var newRoot = editor.GetChangedRoot(); newRoot = await FormatAsync(newRoot, fieldDocument, updatedProperty, cancellationToken).ConfigureAwait(false); - return solution.WithDocumentSyntaxRoot(fieldDocument.Id, newRoot); + return currentSolution.WithDocumentSyntaxRoot(fieldDocument.Id, newRoot); } else { @@ -252,18 +256,56 @@ private async Task ProcessResultAsync(CodeFixContext context, Diagnost var newFieldTreeRoot = fieldTreeRoot.RemoveNode(nodeToRemove, syntaxRemoveOptions); Contract.ThrowIfNull(newFieldTreeRoot); - var newPropertyTreeRoot = propertyTreeRoot.ReplaceNode(property, updatedProperty); + var newPropertyTreeRoot = propertyTreeRoot.ReplaceNode(propertyDeclaration, updatedProperty); newFieldTreeRoot = await FormatAsync(newFieldTreeRoot, fieldDocument, updatedProperty, cancellationToken).ConfigureAwait(false); newPropertyTreeRoot = await FormatAsync(newPropertyTreeRoot, propertyDocument, updatedProperty, cancellationToken).ConfigureAwait(false); - var updatedSolution = solution.WithDocumentSyntaxRoot(fieldDocument.Id, newFieldTreeRoot); + var updatedSolution = currentSolution.WithDocumentSyntaxRoot(fieldDocument.Id, newFieldTreeRoot); updatedSolution = updatedSolution.WithDocumentSyntaxRoot(propertyDocument.Id, newPropertyTreeRoot); return updatedSolution; } } + private async Task<(IFieldSymbol? fieldSymbol, IPropertySymbol? propertySymbol)> MapDiagnosticToCurrentSolutionAsync( + Diagnostic diagnostic, + Solution originalSolution, + Solution currentSolution, + CancellationToken cancellationToken) + { + var locations = diagnostic.AdditionalLocations; + + var propertyLocation = locations[0]; + var declaratorLocation = locations[1]; + + // Look up everything in the original solution. + + var declarator = (TVariableDeclarator)declaratorLocation.FindNode(cancellationToken); + var fieldDocument = originalSolution.GetRequiredDocument(declarator.SyntaxTree); + var fieldSemanticModel = await fieldDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var fieldSymbol = (IFieldSymbol)fieldSemanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); + + var property = GetPropertyDeclaration(propertyLocation.FindNode(cancellationToken)); + var propertyDocument = originalSolution.GetRequiredDocument(property.SyntaxTree); + var propertySemanticModel = await propertyDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var propertySymbol = (IPropertySymbol)propertySemanticModel.GetRequiredDeclaredSymbol(property, cancellationToken); + + Contract.ThrowIfFalse(fieldDocument.Project == propertyDocument.Project); + + // If we're just starting, no need to map anything. + if (originalSolution != currentSolution) + { + var currentProject = currentSolution.GetRequiredProject(fieldDocument.Project.Id); + var currentCompilation = await currentProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + + fieldSymbol = fieldSymbol.GetSymbolKey(cancellationToken).Resolve(currentCompilation, cancellationToken: cancellationToken).GetAnySymbol() as IFieldSymbol; + propertySymbol = propertySymbol.GetSymbolKey(cancellationToken).Resolve(currentCompilation, cancellationToken: cancellationToken).GetAnySymbol() as IPropertySymbol; + } + + return (fieldSymbol, propertySymbol); + } + private static SyntaxRemoveOptions CreateSyntaxRemoveOptions(SyntaxNode nodeToRemove) { var syntaxRemoveOptions = SyntaxGenerator.DefaultRemoveOptions; diff --git a/src/Features/Core/Portable/UseAutoProperty/UseAutoPropertyFixAllProvider.cs b/src/Features/Core/Portable/UseAutoProperty/UseAutoPropertyFixAllProvider.cs new file mode 100644 index 0000000000000..333052f575da3 --- /dev/null +++ b/src/Features/Core/Portable/UseAutoProperty/UseAutoPropertyFixAllProvider.cs @@ -0,0 +1,56 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.UseAutoProperty; + +internal abstract partial class AbstractUseAutoPropertyCodeFixProvider< + TProvider, + TTypeDeclarationSyntax, + TPropertyDeclaration, + TVariableDeclarator, + TConstructorDeclaration, + TExpression> +{ + private sealed class UseAutoPropertyFixAllProvider(TProvider provider) : FixAllProvider + { + public override Task GetFixAsync(FixAllContext fixAllContext) + => DefaultFixAllProviderHelpers.GetFixAsync( + fixAllContext.GetDefaultFixAllTitle(), fixAllContext, FixAllContextsHelperAsync); + + private async Task FixAllContextsHelperAsync(FixAllContext originalContext, ImmutableArray contexts) + { + var cancellationToken = originalContext.CancellationToken; + + // Very slow approach, but the only way we know how to do this correctly and without colliding edits. We + // effectively apply each fix one at a time, moving the solution forward each time. As we process each + // diagnostic, we attempt to re-recover the field/property it was referring to in the original solution to + // the current solution. + var originalSolution = originalContext.Solution; + var currentSolution = originalSolution; + + foreach (var currentContext in contexts) + { + var documentToDiagnostics = await FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(currentContext).ConfigureAwait(false); + foreach (var (_, diagnostics) in documentToDiagnostics) + { + foreach (var diagnostic in diagnostics) + { + currentSolution = await provider.ProcessResultAsync( + originalSolution, currentSolution, diagnostic, cancellationToken).ConfigureAwait(false); + } + } + } + + return currentSolution; + } + } +} diff --git a/src/Features/VisualBasic/Portable/UseAutoProperty/VisualBasicUseAutoPropertyCodeFixProvider.vb b/src/Features/VisualBasic/Portable/UseAutoProperty/VisualBasicUseAutoPropertyCodeFixProvider.vb index 0ba154e235d0f..7bbfd6ed38090 100644 --- a/src/Features/VisualBasic/Portable/UseAutoProperty/VisualBasicUseAutoPropertyCodeFixProvider.vb +++ b/src/Features/VisualBasic/Portable/UseAutoProperty/VisualBasicUseAutoPropertyCodeFixProvider.vb @@ -16,7 +16,7 @@ Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.UseAutoProperty Friend NotInheritable Class VisualBasicUseAutoPropertyCodeFixProvider - Inherits AbstractUseAutoPropertyCodeFixProvider(Of TypeBlockSyntax, PropertyBlockSyntax, ModifiedIdentifierSyntax, ConstructorBlockSyntax, ExpressionSyntax) + Inherits AbstractUseAutoPropertyCodeFixProvider(Of VisualBasicUseAutoPropertyCodeFixProvider, TypeBlockSyntax, PropertyBlockSyntax, ModifiedIdentifierSyntax, ConstructorBlockSyntax, ExpressionSyntax) diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatch.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatch.cs index ad069f7feb922..3894cc984a121 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatch.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatch.cs @@ -81,9 +81,11 @@ public int CompareTo(PatternMatch other, bool ignoreCase) } // Compare types - var comparison = this.Kind - other.Kind; - if (comparison != 0) - return comparison; + { + var comparison = this.Kind - other.Kind; + if (comparison != 0) + return comparison; + } if (!ignoreCase) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs index 9b920f587a42d..2106a867bae00 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs @@ -27,6 +27,7 @@ internal static class CSharpSyntaxTokens public static readonly SyntaxToken DotDotToken = Token(SyntaxKind.DotDotToken); public static readonly SyntaxToken DoubleKeyword = Token(SyntaxKind.DoubleKeyword); public static readonly SyntaxToken EndOfDocumentationCommentToken = Token(SyntaxKind.EndOfDocumentationCommentToken); + public static readonly SyntaxToken EqualsToken = Token(SyntaxKind.EqualsToken); public static readonly SyntaxToken ExplicitKeyword = Token(SyntaxKind.ExplicitKeyword); public static readonly SyntaxToken ExternKeyword = Token(SyntaxKind.ExternKeyword); public static readonly SyntaxToken FileKeyword = Token(SyntaxKind.FileKeyword);