From d6b91d3c16bd0840844a675f8ba137dea03a0fe8 Mon Sep 17 00:00:00 2001 From: CyrusNajmabadi Date: Mon, 19 Sep 2016 23:48:24 -0700 Subject: [PATCH] Initial work for the 'Use Object Initializer' analyzer. --- .../CSharp/Portable/CSharpFeatures.csproj | 2 + ...harpUseObjectInitializerCodeFixProvider.cs | 82 ++++++ ...pUseObjectInitializerDiagnosticAnalyzer.cs | 239 ++++++++++++++++++ .../PredefinedCodeFixProviderNames.cs | 1 + .../Diagnostics/Analyzers/IDEDiagnosticIds.cs | 2 + src/Features/Core/Portable/Features.csproj | 1 + .../Portable/FeaturesResources.Designer.cs | 9 + .../Core/Portable/FeaturesResources.resx | 3 + ...tUseObjectInitializerDiagnosticAnalyzer.cs | 56 ++++ .../Portable/CodeStyle/CodeStyleOptions.cs | 8 +- 10 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs create mode 100644 src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerDiagnosticAnalyzer.cs create mode 100644 src/Features/Core/Portable/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index f331aa7b134..fbce40c9b70 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -417,6 +417,8 @@ + + diff --git a/src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs b/src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs new file mode 100644 index 00000000000..3fa50f79e8e --- /dev/null +++ b/src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs @@ -0,0 +1,82 @@ +// 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.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.UseObjectInitializer +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseObjectInitializer), Shared] + internal class CSharpUseObjectInitializerCodeFixProvider : CodeFixProvider + { + 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 = (ObjectCreationExpressionSyntax)root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); + var matches = new Analyzer(objectCreation).Analyze(); + + var editor = new SyntaxEditor(root, document.Project.Solution.Workspace); + + editor.ReplaceNode(objectCreation, GetNewObjectCreation(objectCreation, matches)); + foreach(var match in matches) + { + editor.RemoveNode(match.ExpressionStatement); + } + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); + } + + private ObjectCreationExpressionSyntax GetNewObjectCreation( + ObjectCreationExpressionSyntax objectCreation, + List matches) + { + var initializer = SyntaxFactory.InitializerExpression( + SyntaxKind.ObjectInitializerExpression, + SyntaxFactory.SeparatedList( + matches.Select(CreateAssignmentExpression))); + + return objectCreation.WithInitializer(initializer) + .WithAdditionalAnnotations(Formatter.Annotation); + } + + private ExpressionSyntax CreateAssignmentExpression(Match match) + { + return SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + match.MemberAccessExpression.Name, + match.Initializer).WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + } + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(FeaturesResources.Object_initialization_can_be_simplified, createChangedDocument) + { + } + } + } +} diff --git a/src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerDiagnosticAnalyzer.cs new file mode 100644 index 00000000000..23d161c6315 --- /dev/null +++ b/src/Features/CSharp/Portable/UseObjectInitializer/CSharpUseObjectInitializerDiagnosticAnalyzer.cs @@ -0,0 +1,239 @@ +// 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 Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.UseObjectInitializer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class CSharpUseObjectInitializerDiagnosticAnalyzer : DiagnosticAnalyzer, IBuiltInAnalyzer + { + private static readonly string Id = IDEDiagnosticIds.UseObjectInitializerDiagnosticId; + + private static readonly DiagnosticDescriptor s_descriptor = + CreateDescriptor(Id, DiagnosticSeverity.Hidden); + + private static readonly DiagnosticDescriptor s_unnecessaryWithSuggestionDescriptor = + CreateDescriptor(Id, DiagnosticSeverity.Hidden, DiagnosticCustomTags.Unnecessary); + + private static readonly DiagnosticDescriptor s_unnecessaryWithoutSuggestionDescriptor = + CreateDescriptor(Id + "WithoutSuggestion", + DiagnosticSeverity.Hidden, DiagnosticCustomTags.Unnecessary); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(s_descriptor, s_unnecessaryWithoutSuggestionDescriptor, s_unnecessaryWithSuggestionDescriptor); + + public bool OpenFileOnly(Workspace workspace) => false; + + private static DiagnosticDescriptor CreateDescriptor(string id, DiagnosticSeverity severity, params string[] customTags) + => new DiagnosticDescriptor( + id, + FeaturesResources.Object_initialization_can_be_simplified, + FeaturesResources.Object_initialization_can_be_simplified, + DiagnosticCategory.Style, + severity, + isEnabledByDefault: true, + customTags: customTags); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ObjectCreationExpression); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var optionSet = context.Options.GetOptionSet(); + var option = optionSet.GetOption(CodeStyleOptions.PreferObjectInitializer, LanguageNames.CSharp); + if (!option.Value) + { + // not point in analyzing if the option is off. + return; + } + + var objectCreationExpression = (ObjectCreationExpressionSyntax)context.Node; + + var matches = new Analyzer(objectCreationExpression).Analyze(); + if (matches == null) + { + return; + } + + var locations = ImmutableArray.Create(objectCreationExpression.GetLocation()); + + var severity = option.Notification.Value; + context.ReportDiagnostic(Diagnostic.Create( + CreateDescriptor(Id, severity), + objectCreationExpression.GetLocation(), + additionalLocations: locations)); + + var syntaxTree = objectCreationExpression.SyntaxTree; + + foreach (var match in matches) + { + var location1 = Location.Create(syntaxTree, TextSpan.FromBounds( + match.MemberAccessExpression.SpanStart, match.MemberAccessExpression.OperatorToken.Span.End)); + + context.ReportDiagnostic(Diagnostic.Create( + s_unnecessaryWithSuggestionDescriptor, location1, additionalLocations: locations)); + context.ReportDiagnostic(Diagnostic.Create( + s_unnecessaryWithoutSuggestionDescriptor, + match.ExpressionStatement.SemicolonToken.GetLocation(), + additionalLocations: locations)); + } + } + + public DiagnosticAnalyzerCategory GetAnalyzerCategory() + { + return DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + } + + + } + + internal struct Match + { + public readonly ExpressionStatementSyntax ExpressionStatement; + public readonly MemberAccessExpressionSyntax MemberAccessExpression; + public readonly ExpressionSyntax Initializer; + + public Match(ExpressionStatementSyntax expressionStatement, MemberAccessExpressionSyntax memberAccessExpression, ExpressionSyntax initializer) + { + ExpressionStatement = expressionStatement; + MemberAccessExpression = memberAccessExpression; + Initializer = initializer; + } + } + + internal struct Analyzer + { + private readonly ObjectCreationExpressionSyntax _objectCreationExpression; + private StatementSyntax _containingStatement; + private SyntaxNodeOrToken _valuePattern; + + public Analyzer(ObjectCreationExpressionSyntax objectCreationExpression) : this() + { + _objectCreationExpression = objectCreationExpression; + } + + internal List Analyze() + { + if (_objectCreationExpression.Initializer != null) + { + // Don't bother if this already has an initializer. + return null; + } + + if (!TryInitializeVariableDeclarationCase() && + !TryInitializeAssignmentCase()) + { + return null; + } + + var containingBlock = _containingStatement.Parent as BlockSyntax; + if (containingBlock == null) + { + return null; + } + + List matches = null; + + var statementIndex = containingBlock.Statements.IndexOf(_containingStatement); + for (var i = statementIndex + 1; i < containingBlock.Statements.Count; i++) + { + var expressionStatement = containingBlock.Statements[i] as ExpressionStatementSyntax; + if (expressionStatement == null) + { + break; + } + + var assignExpression = expressionStatement.Expression as AssignmentExpressionSyntax; + if (assignExpression?.Kind() != SyntaxKind.SimpleAssignmentExpression) + { + break; + } + + var leftMemberAccess = assignExpression.Left as MemberAccessExpressionSyntax; + if (leftMemberAccess?.Kind() != SyntaxKind.SimpleMemberAccessExpression) + { + break; + } + + var expression = leftMemberAccess.Expression; + if (!ValuePatternMatches(expression)) + { + break; + } + + // found a match! + matches = matches ?? new List(); + matches.Add(new Match(expressionStatement, leftMemberAccess, assignExpression.Right)); + } + + return matches; + } + + private bool ValuePatternMatches(ExpressionSyntax expression) + { + if (_valuePattern.IsToken) + { + return expression.IsKind(SyntaxKind.IdentifierName) && + SyntaxFactory.AreEquivalent( + _valuePattern.AsToken(), + ((IdentifierNameSyntax)expression).Identifier); + } + else + { + return SyntaxFactory.AreEquivalent( + _valuePattern.AsNode(), + expression); + } + } + + private bool TryInitializeAssignmentCase() + { + if (!IsRightSideOfAssignment()) + { + return false; + } + + _containingStatement = _objectCreationExpression.FirstAncestorOrSelf(); + _valuePattern = ((AssignmentExpressionSyntax)_objectCreationExpression.Parent).Left; + return true; + } + + private bool IsRightSideOfAssignment() + { + return _objectCreationExpression.IsParentKind(SyntaxKind.SimpleAssignmentExpression) && + ((AssignmentExpressionSyntax)_objectCreationExpression.Parent).Right == _objectCreationExpression && + _objectCreationExpression.Parent.IsParentKind(SyntaxKind.ExpressionStatement); + } + + private bool TryInitializeVariableDeclarationCase() + { + if (!IsVariableDeclarationInitializer()) + { + return false; + } + + _containingStatement = _objectCreationExpression.FirstAncestorOrSelf(); + _valuePattern = ((VariableDeclaratorSyntax)_objectCreationExpression.Parent.Parent).Identifier; + return true; + } + + private bool IsVariableDeclarationInitializer() + { + return + _objectCreationExpression.IsParentKind(SyntaxKind.EqualsValueClause) && + _objectCreationExpression.Parent.IsParentKind(SyntaxKind.VariableDeclarator) && + _objectCreationExpression.Parent.Parent.IsParentKind(SyntaxKind.VariableDeclaration) && + _objectCreationExpression.Parent.Parent.Parent.IsParentKind(SyntaxKind.LocalDeclarationStatement); + } + } + +} diff --git a/src/Features/Core/Portable/CodeFixes/PredefinedCodeFixProviderNames.cs b/src/Features/Core/Portable/CodeFixes/PredefinedCodeFixProviderNames.cs index 619d6cbbe63..608a8445fa5 100644 --- a/src/Features/Core/Portable/CodeFixes/PredefinedCodeFixProviderNames.cs +++ b/src/Features/Core/Portable/CodeFixes/PredefinedCodeFixProviderNames.cs @@ -44,6 +44,7 @@ internal static class PredefinedCodeFixProviderNames public const string AddNew = "Add new keyword to member"; public const string UseImplicitType = nameof(UseImplicitType); public const string UseExplicitType = nameof(UseExplicitType); + public const string UseObjectInitializer = nameof(UseObjectInitializer); public const string PreferFrameworkType = nameof(PreferFrameworkType); } } diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs index aa12579908e..50540227fbe 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs @@ -20,6 +20,8 @@ internal static class IDEDiagnosticIds public const string PreferFrameworkTypeInDeclarationsDiagnosticId = "IDE0014"; public const string PreferFrameworkTypeInMemberAccessDiagnosticId = "IDE0015"; + public const string UseObjectInitializerDiagnosticId = "IDE0017"; + // Analyzer error Ids public const string AnalyzerChangedId = "IDE1001"; public const string AnalyzerDependencyConflictId = "IDE1002"; diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 28ed5fc88e0..61c116276b2 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -652,6 +652,7 @@ + diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index b187fb4690a..3f11afcfa2b 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -1948,6 +1948,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Object initialization can be simplified. + /// + internal static string Object_initialization_can_be_simplified { + get { + return ResourceManager.GetString("Object_initialization_can_be_simplified", resourceCulture); + } + } + /// /// Looks up a localized string similar to Only methods with a single argument, which is not an out variable declaration, can be replaced with a property.. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 8efa54bfdfc..89fd32f2522 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1067,4 +1067,7 @@ This version used in: {2} Install package '{0}' + + Object initialization can be simplified + \ No newline at end of file diff --git a/src/Features/Core/Portable/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs b/src/Features/Core/Portable/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs new file mode 100644 index 00000000000..76d96fec133 --- /dev/null +++ b/src/Features/Core/Portable/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs @@ -0,0 +1,56 @@ +//// 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.Text; +//using System.Threading.Tasks; +//using Microsoft.CodeAnalysis.Diagnostics; + +//namespace Microsoft.CodeAnalysis.UseObjectInitializer +//{ +// internal abstract class AbstractUseObjectInitializerDiagnosticAnalyzer< +// TObjectCreationExpression, +// TEqualsValueClause, +// TVariableDeclarator, +// TAssignmentExpression, +// TSyntaxKind> +// : DiagnosticAnalyzer +// where TObjectCreationExpression : SyntaxNode +// where TEqualsValueClause : SyntaxNode +// where TVariableDeclarator : SyntaxNode +// where TAssignmentExpression : SyntaxNode +// where TSyntaxKind : struct +// { +// public override ImmutableArray SupportedDiagnostics +// { +// get +// { +// throw new NotImplementedException(); +// } +// } + +// public override void Initialize(AnalysisContext context) +// { +// context.RegisterSyntaxNodeAction( +// AnalyzeNode, +// ImmutableArray.Create(GetObjectCreationSyntaxKind())); +// } + +// protected abstract TSyntaxKind GetObjectCreationSyntaxKind(); + +// private void AnalyzeNode(SyntaxNodeAnalysisContext obj) +// { +// var objectCreationNode = (TObjectCreationExpression)obj.Node; +// if (objectCreationNode.Parent is TEqualsValueClause && +// objectCreationNode.Parent.Parent is TVariableDeclarator) +// { +// AnalyzeInVariableDeclarator(objectCreationNode); +// return; +// } + +// if( objectCreationNode.Parent is TAssignmentExpression &&) +// } +// } +//} diff --git a/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs b/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs index ac1862b4ab0..1c97ca4332a 100644 --- a/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs +++ b/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs @@ -55,5 +55,11 @@ public class CodeStyleOptions nameof(PreferThrowExpression), defaultValue: trueWithSuggestionEnforcement, storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferThrowExpression")); + + internal static readonly PerLanguageOption> PreferObjectInitializer = new PerLanguageOption>( + nameof(CodeStyleOptions), + nameof(PreferObjectInitializer), + defaultValue: trueWithSuggestionEnforcement, + storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferObjectInitializer")); } -} +} \ No newline at end of file -- GitLab