diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs index ddc1c436a5cda6b3e7f2af0a231d8d9eb3b54eb3..01f16e0c5f395b5d20846e7744be878229d25b7f 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs @@ -24,6 +24,7 @@ internal static class IDEDiagnosticIds public const string InlineDeclarationDiagnosticId = "IDE0018"; public const string InlineAsTypeCheckId = "IDE0019"; public const string InlineIsTypeCheckId = "IDE0020"; + public const string UseCollectionInitializerDiagnosticId = "IDE0021"; public const string UseExpressionBodyForConstructorsDiagnosticId = "IDE0020"; public const string UseExpressionBodyForMethodsDiagnosticId = "IDE0021"; diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 8a64b106dad11f43dcb00094d20466602cc35b6c..8451ff64c12381ecb52d8d034e2c862e83d2c346 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -121,6 +121,8 @@ + + diff --git a/src/Features/Core/Portable/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs b/src/Features/Core/Portable/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..2ba8f78c43ed38d72017afb16b9b24af928b8bce --- /dev/null +++ b/src/Features/Core/Portable/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs @@ -0,0 +1,89 @@ +// 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; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.UseCollectionInitializer +{ + internal abstract class AbstractUseCollectionInitializerCodeFixProvider< + TExpressionSyntax, + TStatementSyntax, + TObjectCreationExpressionSyntax, + TMemberAccessExpressionSyntax, + TAssignmentStatementSyntax, + TVariableDeclarator> + : CodeFixProvider + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TObjectCreationExpressionSyntax : TExpressionSyntax + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TAssignmentStatementSyntax : TStatementSyntax + where TVariableDeclarator : SyntaxNode + { + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(IDEDiagnosticIds.UseObjectInitializerDiagnosticId); + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix( + new MyCodeAction(c => FixAsync(context.Document, context.Diagnostics.First(), c)), + context.Diagnostics); + return SpecializedTasks.EmptyTask; + } + + private async Task FixAsync( + Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var objectCreation = (TObjectCreationExpressionSyntax)root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); + + var syntaxFacts = document.GetLanguageService(); + var analyzer = new Analyzer( + syntaxFacts, objectCreation); + var matches = analyzer.Analyze(); + + var editor = new SyntaxEditor(root, document.Project.Solution.Workspace); + + var statement = objectCreation.FirstAncestorOrSelf(); + var newStatement = statement.ReplaceNode( + objectCreation, + GetNewObjectCreation(objectCreation, matches)).WithAdditionalAnnotations(Formatter.Annotation); + + editor.ReplaceNode(statement, newStatement); + foreach (var match in matches) + { + editor.RemoveNode(match.Statement); + } + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); + } + + protected abstract TObjectCreationExpressionSyntax GetNewObjectCreation( + TObjectCreationExpressionSyntax objectCreation, + List> matches); + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(FeaturesResources.Object_initialization_can_be_simplified, createChangedDocument) + { + } + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs b/src/Features/Core/Portable/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..97320f2840b7d06ec143b8ccfb6f2080f519e86f --- /dev/null +++ b/src/Features/Core/Portable/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs @@ -0,0 +1,324 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.UseCollectionInitializer +{ + internal abstract class AbstractUseCollectionInitializerDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TStatementSyntax, + TObjectCreationExpressionSyntax, + TMemberAccessExpressionSyntax, + TAssignmentStatementSyntax, + TVariableDeclarator> + : AbstractCodeStyleDiagnosticAnalyzer, IBuiltInAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TObjectCreationExpressionSyntax : TExpressionSyntax + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TAssignmentStatementSyntax : TStatementSyntax + where TVariableDeclarator : SyntaxNode + { + protected abstract bool FadeOutOperatorToken { get; } + + public bool OpenFileOnly(Workspace workspace) => false; + + protected AbstractUseCollectionInitializerDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCollectionInitializerDiagnosticId, + new LocalizableResourceString(nameof(FeaturesResources.Object_initialization_can_be_simplified), FeaturesResources.ResourceManager, typeof(FeaturesResources))) + { + } + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, GetObjectCreationSyntaxKind()); + } + + protected abstract TSyntaxKind GetObjectCreationSyntaxKind(); + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var objectCreationExpression = (TObjectCreationExpressionSyntax)context.Node; + var language = objectCreationExpression.Language; + + var optionSet = context.Options.GetOptionSet(); + var option = optionSet.GetOption(CodeStyleOptions.PreferCollectionInitializer, language); + if (!option.Value) + { + // not point in analyzing if the option is off. + return; + } + + var syntaxFacts = GetSyntaxFactsService(); + var analyzer = new Analyzer( + syntaxFacts, + objectCreationExpression); + var matches = analyzer.Analyze(); + if (matches == null) + { + return; + } + + var locations = ImmutableArray.Create(objectCreationExpression.GetLocation()); + + var severity = option.Notification.Value; + context.ReportDiagnostic(Diagnostic.Create( + CreateDescriptor(DescriptorId, severity), + objectCreationExpression.GetLocation(), + additionalLocations: locations)); + + FadeOutCode(context, optionSet, matches, locations); + } + + private void FadeOutCode( + SyntaxNodeAnalysisContext context, + OptionSet optionSet, + List> matches, + ImmutableArray locations) + { + var syntaxTree = context.Node.SyntaxTree; + + var fadeOutCode = optionSet.GetOption( + CodeStyleOptions.PreferCollectionInitializer_FadeOutCode, context.Node.Language); + if (!fadeOutCode) + { + return; + } + + var syntaxFacts = GetSyntaxFactsService(); + + foreach (var match in matches) + { + var end = this.FadeOutOperatorToken + ? syntaxFacts.GetOperatorTokenOfMemberAccessExpression(match.MemberAccessExpression).Span.End + : syntaxFacts.GetExpressionOfMemberAccessExpression(match.MemberAccessExpression).Span.End; + + var location1 = Location.Create(syntaxTree, TextSpan.FromBounds( + match.MemberAccessExpression.SpanStart, end)); + + context.ReportDiagnostic(Diagnostic.Create( + UnnecessaryWithSuggestionDescriptor, location1, additionalLocations: locations)); + + if (match.Statement.Span.End > match.Initializer.FullSpan.End) + { + context.ReportDiagnostic(Diagnostic.Create( + UnnecessaryWithoutSuggestionDescriptor, + Location.Create(syntaxTree, TextSpan.FromBounds( + match.Initializer.FullSpan.End, + match.Statement.Span.End)), + additionalLocations: locations)); + } + } + } + + protected abstract ISyntaxFactsService GetSyntaxFactsService(); + + public DiagnosticAnalyzerCategory GetAnalyzerCategory() + { + return DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + } + } + + internal struct Match + where TExpressionSyntax : SyntaxNode + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TAssignmentStatementSyntax : SyntaxNode + { + public readonly TAssignmentStatementSyntax Statement; + public readonly TMemberAccessExpressionSyntax MemberAccessExpression; + public readonly TExpressionSyntax Initializer; + + public Match( + TAssignmentStatementSyntax statement, + TMemberAccessExpressionSyntax memberAccessExpression, + TExpressionSyntax initializer) + { + Statement = statement; + MemberAccessExpression = memberAccessExpression; + Initializer = initializer; + } + } + + internal struct Analyzer< + TExpressionSyntax, + TStatementSyntax, + TObjectCreationExpressionSyntax, + TMemberAccessExpressionSyntax, + TAssignmentStatementSyntax, + TVariableDeclaratorSyntax> + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TObjectCreationExpressionSyntax : TExpressionSyntax + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TAssignmentStatementSyntax : TStatementSyntax + where TVariableDeclaratorSyntax : SyntaxNode + { + private readonly ISyntaxFactsService _syntaxFacts; + private readonly TObjectCreationExpressionSyntax _objectCreationExpression; + + private TStatementSyntax _containingStatement; + private SyntaxNodeOrToken _valuePattern; + + public Analyzer( + ISyntaxFactsService syntaxFacts, + TObjectCreationExpressionSyntax objectCreationExpression) : this() + { + _syntaxFacts = syntaxFacts; + _objectCreationExpression = objectCreationExpression; + } + + internal List> Analyze() + { + if (_syntaxFacts.GetObjectCreationInitializer(_objectCreationExpression) != null) + { + // Don't bother if this already has an initializer. + return null; + } + + _containingStatement = _objectCreationExpression.FirstAncestorOrSelf(); + if (_containingStatement == null) + { + return null; + } + + if (!TryInitializeVariableDeclarationCase() && + !TryInitializeAssignmentCase()) + { + return null; + } + + var containingBlock = _containingStatement.Parent; + var foundStatement = false; + + List> matches = null; + HashSet seenNames = null; + + foreach (var child in containingBlock.ChildNodesAndTokens()) + { + if (!foundStatement) + { + if (child == _containingStatement) + { + foundStatement = true; + } + + continue; + } + + if (child.IsToken) + { + break; + } + + var statement = child.AsNode() as TAssignmentStatementSyntax; + if (statement == null) + { + break; + } + + if (!_syntaxFacts.IsSimpleAssignmentStatement(statement)) + { + break; + } + + SyntaxNode left, right; + _syntaxFacts.GetPartsOfAssignmentStatement(statement, out left, out right); + + var rightExpression = right as TExpressionSyntax; + var leftMemberAccess = left as TMemberAccessExpressionSyntax; + if (!_syntaxFacts.IsSimpleMemberAccessExpression(leftMemberAccess)) + { + break; + } + + var expression = (TExpressionSyntax)_syntaxFacts.GetExpressionOfMemberAccessExpression(leftMemberAccess); + if (!ValuePatternMatches(expression)) + { + break; + } + + // found a match! + seenNames = seenNames ?? new HashSet(); + matches = matches ?? new List>(); + + // If we see an assignment to the same property/field, we can't convert it + // to an initializer. + var name = _syntaxFacts.GetNameOfMemberAccessExpression(leftMemberAccess); + var identifier = _syntaxFacts.GetIdentifierOfSimpleName(name); + if (!seenNames.Add(identifier.ValueText)) + { + break; + } + + matches.Add(new Match( + statement, leftMemberAccess, rightExpression)); + } + + return matches; + } + + private bool ValuePatternMatches(TExpressionSyntax expression) + { + if (_valuePattern.IsToken) + { + return _syntaxFacts.IsIdentifierName(expression) && + _syntaxFacts.AreEquivalent( + _valuePattern.AsToken(), + _syntaxFacts.GetIdentifierOfSimpleName(expression)); + } + else + { + return _syntaxFacts.AreEquivalent( + _valuePattern.AsNode(), expression); + } + } + + private bool TryInitializeAssignmentCase() + { + if (!_syntaxFacts.IsSimpleAssignmentStatement(_containingStatement)) + { + return false; + } + + SyntaxNode left, right; + _syntaxFacts.GetPartsOfAssignmentStatement(_containingStatement, out left, out right); + if (right != _objectCreationExpression) + { + return false; + } + + _valuePattern = left; + return true; + } + + private bool TryInitializeVariableDeclarationCase() + { + if (!_syntaxFacts.IsLocalDeclarationStatement(_containingStatement)) + { + return false; + } + + var containingDeclarator = _objectCreationExpression.FirstAncestorOrSelf(); + if (containingDeclarator == null) + { + return false; + } + + if (!_syntaxFacts.IsDeclaratorOfLocalDeclarationStatement(containingDeclarator, _containingStatement)) + { + return false; + } + + _valuePattern = _syntaxFacts.GetIdentifierOfVariableDeclarator(containingDeclarator); + return true; + } + } +} \ No newline at end of file diff --git a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs index 2e88b34b02237e3b5b5cc4ac7cbc18e36d970b56..6f102ef25fd0c23afb9baf9f8f2b254bae27f23c 100644 --- a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs +++ b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs @@ -338,6 +338,34 @@ public Customer() //] } } +"; + + private static readonly string s_preferCollectionInitializer = @" +using System.Collections.Generic; + +class Customer +{ + private int Age; + + public Customer() + { +//[ + // Prefer: + var list = new List + { + 1, + 2, + 3 + }; + + // Over: + var list = new List(); + list.Add(1); + list.Add(2); + list.Add(3); +//] + } +} "; private static readonly string s_preferInlinedVariableDeclaration = @" @@ -589,6 +617,7 @@ internal StyleViewModel(OptionSet optionSet, IServiceProvider serviceProvider) : // Expression preferences CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferObjectInitializer, ServicesVSResources.Prefer_object_initializer, s_preferObjectInitializer, s_preferObjectInitializer, this, optionSet, expressionPreferencesGroupTitle)); + CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferCollectionInitializer, ServicesVSResources.Prefer_collection_initializer, s_preferCollectionInitializer, s_preferCollectionInitializer, this, optionSet, expressionPreferencesGroupTitle)); CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck, CSharpVSResources.Prefer_pattern_matching_over_is_with_cast_check, s_preferPatternMatchingOverIsWithCastCheck, s_preferPatternMatchingOverIsWithCastCheck, this, optionSet, expressionPreferencesGroupTitle)); CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferPatternMatchingOverAsWithNullCheck, CSharpVSResources.Prefer_pattern_matching_over_as_with_null_check, s_preferPatternMatchingOverAsWithNullCheck, s_preferPatternMatchingOverAsWithNullCheck, this, optionSet, expressionPreferencesGroupTitle)); diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs index 4f4e8ff05a183ee68a8f25c19d691b3a23af0236..5666bdbd25bbd8a073f9d1af6a51d46a90c36124 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs +++ b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs @@ -1365,6 +1365,15 @@ internal class ServicesVSResources { } } + /// + /// Looks up a localized string similar to Prefer collection initializer. + /// + internal static string Prefer_collection_initializer { + get { + return ResourceManager.GetString("Prefer_collection_initializer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Prefer framework type. /// diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index 417c32725b0106199433b1d8136f3d9c6b9e4ba5..1a5fe2589165591b90fea10b5da21ae3b2e0694e 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -804,4 +804,7 @@ Additional information: {1} This item cannot be deleted because it is used by an existing Naming Rule. + + Prefer collection initializer + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb b/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb index fbe94d07d06f63e99cdf24620f248e204cbd85a5..5475a3c53ff6d27eb45ce9ff487f49ec7db1a177 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb @@ -165,6 +165,30 @@ Class Customer End Sub End Class" + Private Shared ReadOnly s_preferCollectionInitializer As String = " +Imports System.Collections.Generic + +Class Customer + Private Age As Integer + + Sub New() +//[ + ' Prefer: + Dim list = New List(Of Integer) From { + 1, + 2, + 3 + } + + ' Over: + Dim list = New List(Of Integer)() + list.Add(1) + list.Add(2) + list.Add(3) +//] + End Sub +End Class" + #End Region Public Sub New(optionSet As OptionSet, serviceProvider As IServiceProvider) @@ -202,6 +226,7 @@ End Class" ' expression preferences Me.CodeStyleItems.Add(New SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferObjectInitializer, ServicesVSResources.Prefer_object_initializer, s_preferObjectInitializer, s_preferObjectInitializer, Me, optionSet, expressionPreferencesGroupTitle)) + Me.CodeStyleItems.Add(New SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferCollectionInitializer, ServicesVSResources.Prefer_collection_initializer, s_preferCollectionInitializer, s_preferCollectionInitializer, Me, optionSet, expressionPreferencesGroupTitle)) End Sub End Class diff --git a/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs b/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs index 9c27bb56c6d3cce8e209c87798d9fabdc304ba17..ef15537242a9f867b29f5e9ee41ddd4cb5130b70 100644 --- a/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs +++ b/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs @@ -64,11 +64,23 @@ public class CodeStyleOptions defaultValue: TrueWithSuggestionEnforcement, storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferObjectInitializer")); + internal static readonly PerLanguageOption> PreferCollectionInitializer = new PerLanguageOption>( + nameof(CodeStyleOptions), + nameof(PreferCollectionInitializer), + defaultValue: TrueWithSuggestionEnforcement, + storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferCollectionInitializer")); + internal static readonly PerLanguageOption PreferObjectInitializer_FadeOutCode = new PerLanguageOption( nameof(CodeStyleOptions), nameof(PreferObjectInitializer_FadeOutCode), defaultValue: false, - storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferObjectInitializer")); + storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferObjectInitializer_FadeOutCode")); + + internal static readonly PerLanguageOption PreferCollectionInitializer_FadeOutCode = new PerLanguageOption( + nameof(CodeStyleOptions), + nameof(PreferCollectionInitializer_FadeOutCode), + defaultValue: false, + storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferCollectionInitializer_FadeOutCode")); internal static readonly PerLanguageOption> PreferInlinedVariableDeclaration = new PerLanguageOption>( nameof(CodeStyleOptions),