From cac232fa96f5c10d14f9f9e8edc4cfc11882764c Mon Sep 17 00:00:00 2001 From: StephanyBatista Date: Sun, 23 Nov 2014 13:42:53 -0300 Subject: [PATCH] Analyze and fix for use of StringBuilder. The Analyzer will alert when have more than two concatenation in initialization Test: (in) var teste = "A" + "B" + "C"; (out) var teste = new StringBuilder().Append("A").Append("B").Append("C").ToString(); --- src/CodeCracker/CodeCracker.csproj | 2 + ...UseStringBuilderToConcatenationAnalyzer.cs | 76 +++++++++ ...ngBuilderToConcatenationCodeFixProvider.cs | 122 ++++++++++++++ test/CodeCracker.Test/CodeCracker.Test.csproj | 9 ++ .../UseStringBuilderToConcatenationTests.cs | 153 ++++++++++++++++++ test/CodeCracker.Test/packages.config | 1 + 6 files changed, 363 insertions(+) create mode 100644 src/CodeCracker/UseStringBuilderToConcatenationAnalyzer.cs create mode 100644 src/CodeCracker/UseStringBuilderToConcatenationCodeFixProvider.cs create mode 100644 test/CodeCracker.Test/UseStringBuilderToConcatenationTests.cs diff --git a/src/CodeCracker/CodeCracker.csproj b/src/CodeCracker/CodeCracker.csproj index 9e2742921..4db1bb1de 100644 --- a/src/CodeCracker/CodeCracker.csproj +++ b/src/CodeCracker/CodeCracker.csproj @@ -60,6 +60,8 @@ + + diff --git a/src/CodeCracker/UseStringBuilderToConcatenationAnalyzer.cs b/src/CodeCracker/UseStringBuilderToConcatenationAnalyzer.cs new file mode 100644 index 000000000..b429fb8b1 --- /dev/null +++ b/src/CodeCracker/UseStringBuilderToConcatenationAnalyzer.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CodeCracker +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class UseStringBuilderToConcatenationAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "CC0019"; + internal const string Title = "Use StringBuilder To Concatenations"; + internal const string MessageFormat = "Use 'StringBuilder' instead of concatenation."; + internal const string Category = "Syntax"; + internal const int NumberMaxConcatenations = 2; + internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } + + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var localDeclaration = (LocalDeclarationStatementSyntax)context.Node; + + if (localDeclaration == null) return; + if (!IsString(context, localDeclaration)) return; + + var variableDeclaration = localDeclaration.ChildNodes() + .OfType() + .FirstOrDefault(); + + var variableDeclarator = variableDeclaration.ChildNodes() + .OfType() + .FirstOrDefault(); + + var equalsValueClause = variableDeclarator.ChildNodes() + .OfType() + .FirstOrDefault(); + + if(NumberOfConcatenations(equalsValueClause.ChildNodes()) > NumberMaxConcatenations) + { + var diagnostic = Diagnostic.Create(Rule, variableDeclaration.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + + private bool IsString(SyntaxNodeAnalysisContext context, LocalDeclarationStatementSyntax localDeclaration) + { + var semanticModel = context.SemanticModel; + var variableTypeName = localDeclaration.Declaration.Type; + var variableType = semanticModel.GetTypeInfo(variableTypeName).ConvertedType; + return variableType.SpecialType == SpecialType.System_String; + } + + private int NumberOfConcatenations(IEnumerable nodes) + { + const int concatenationCurrent = 1; + + var addExpression = nodes + .OfType() + .FirstOrDefault(); + + return addExpression?.ChildNodes() != null ? + concatenationCurrent + NumberOfConcatenations(addExpression.ChildNodes()) : + concatenationCurrent; + } + } +} diff --git a/src/CodeCracker/UseStringBuilderToConcatenationCodeFixProvider.cs b/src/CodeCracker/UseStringBuilderToConcatenationCodeFixProvider.cs new file mode 100644 index 000000000..5b6c753ec --- /dev/null +++ b/src/CodeCracker/UseStringBuilderToConcatenationCodeFixProvider.cs @@ -0,0 +1,122 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CodeCracker +{ + [ExportCodeFixProvider("UseStringBuilderToConcatenationCodeFixProvider", LanguageNames.CSharp), Shared] + public class UseStringBuilderToConcatenationCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray GetFixableDiagnosticIds() + { + return ImmutableArray.Create(UseStringBuilderToConcatenationAnalyzer.DiagnosticId); + } + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task ComputeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + var localDeclaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); + context.RegisterFix(CodeAction.Create("Use 'StringBuilder'", c => UseStringBuilderAsync(context.Document, localDeclaration, c)), diagnostic); + } + + private async Task UseStringBuilderAsync(Document document, LocalDeclarationStatementSyntax localDeclaration, CancellationToken cancellationToken) + { + var variableDeclaration = localDeclaration.ChildNodes() + .OfType() + .FirstOrDefault(); + + var variableDeclarator = variableDeclaration.ChildNodes() + .OfType() + .FirstOrDefault(); + + var initialization = variableDeclarator.ChildNodes() + .OfType() + .FirstOrDefault(); + + var appends = GetAppends(initialization.ChildNodes()); + + var root = await document.GetSyntaxRootAsync(); + + var newInitialization = SyntaxFactory.EqualsValueClause( + initialization.EqualsToken, + SyntaxFactory.ParseExpression(NewInitializationWithStrigBuilder(appends))); + + var newRoot = root.ReplaceNode(initialization, newInitialization); + var newDocument = document.WithSyntaxRoot(newRoot); + return newDocument; + } + + private IEnumerable> GetAppends(IEnumerable nodes) + { + BinaryExpressionSyntax addExpression = null; + var appends = new Dictionary(); + + do + { + nodes = addExpression?.ChildNodes() != null ? addExpression.ChildNodes() : nodes; + + addExpression = nodes + .OfType() + .FirstOrDefault(); + + InsertItensToAppends(addExpression != null ? addExpression.ChildNodes() : nodes, appends); + + } while (addExpression != null); + + return appends.OrderBy(c => c.Value); + } + + private void InsertItensToAppends(IEnumerable nodes, Dictionary appends) + { + foreach (var node in nodes.AsEnumerable()) + { + var literal = node as LiteralExpressionSyntax; + var spanStart = 0; + string append = null; + + if (literal != null) + { + append = literal.Token.Text; + spanStart = literal.Token.SpanStart; + } + else + { + var variable = node as IdentifierNameSyntax; + if (variable != null) + { + append = variable.Identifier.Value.ToString(); + spanStart = variable.Identifier.SpanStart; + } + } + + if (!string.IsNullOrEmpty(append) && !appends.ContainsKey(append)) + appends.Add(append, spanStart); + } + } + + private string NewInitializationWithStrigBuilder(IEnumerable> itensAppend) + { + var sintax = new StringBuilder("new StringBuilder()"); + foreach (var item in itensAppend) + sintax.Append(string.Format(".Append({0})", item.Key)); + sintax.Append(".ToString()"); + return sintax.ToString(); + } + } +} diff --git a/test/CodeCracker.Test/CodeCracker.Test.csproj b/test/CodeCracker.Test/CodeCracker.Test.csproj index fb88d87c4..8ea4ad548 100644 --- a/test/CodeCracker.Test/CodeCracker.Test.csproj +++ b/test/CodeCracker.Test/CodeCracker.Test.csproj @@ -1,5 +1,6 @@  + Debug AnyCPU @@ -13,6 +14,7 @@ v4.5 512 + 553674c9 true @@ -113,6 +115,7 @@ + @@ -140,6 +143,12 @@ + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + +