Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CodeCracker/CodeCracker.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnnecessaryParenthesisAnalyzer.cs" />
<Compile Include="UnnecessaryParenthesisCodeFixProvider.cs" />
<Compile Include="UseStringBuilderToConcatenationAnalyzer.cs" />
<Compile Include="UseStringBuilderToConcatenationCodeFixProvider.cs" />
</ItemGroup>
<ItemGroup>
<None Include="CodeCracker.nuspec">
Expand Down
76 changes: 76 additions & 0 deletions src/CodeCracker/UseStringBuilderToConcatenationAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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<VariableDeclarationSyntax>()
.FirstOrDefault();

var variableDeclarator = variableDeclaration.ChildNodes()
.OfType<VariableDeclaratorSyntax>()
.FirstOrDefault();

var equalsValueClause = variableDeclarator.ChildNodes()
.OfType<EqualsValueClauseSyntax>()
.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<SyntaxNode> nodes)
{
const int concatenationCurrent = 1;

var addExpression = nodes
.OfType<BinaryExpressionSyntax>()
.FirstOrDefault();

return addExpression?.ChildNodes() != null ?
concatenationCurrent + NumberOfConcatenations(addExpression.ChildNodes()) :
concatenationCurrent;
}
}
}
122 changes: 122 additions & 0 deletions src/CodeCracker/UseStringBuilderToConcatenationCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<LocalDeclarationStatementSyntax>().First();
context.RegisterFix(CodeAction.Create("Use 'StringBuilder'", c => UseStringBuilderAsync(context.Document, localDeclaration, c)), diagnostic);
}

private async Task<Document> UseStringBuilderAsync(Document document, LocalDeclarationStatementSyntax localDeclaration, CancellationToken cancellationToken)
{
var variableDeclaration = localDeclaration.ChildNodes()
.OfType<VariableDeclarationSyntax>()
.FirstOrDefault();

var variableDeclarator = variableDeclaration.ChildNodes()
.OfType<VariableDeclaratorSyntax>()
.FirstOrDefault();

var initialization = variableDeclarator.ChildNodes()
.OfType<EqualsValueClauseSyntax>()
.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<KeyValuePair<string, int>> GetAppends(IEnumerable<SyntaxNode> nodes)
{
BinaryExpressionSyntax addExpression = null;
var appends = new Dictionary<string, int>();

do
{
nodes = addExpression?.ChildNodes() != null ? addExpression.ChildNodes() : nodes;

addExpression = nodes
.OfType<BinaryExpressionSyntax>()
.FirstOrDefault();

InsertItensToAppends(addExpression != null ? addExpression.ChildNodes() : nodes, appends);

} while (addExpression != null);

return appends.OrderBy(c => c.Value);
}

private void InsertItensToAppends(IEnumerable<SyntaxNode> nodes, Dictionary<string, int> 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<KeyValuePair<string, int>> 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();
}
}
}
9 changes: 9 additions & 0 deletions test/CodeCracker.Test/CodeCracker.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.0.99.9-build1021\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.0.99.9-build1021\build\net20\xunit.runner.visualstudio.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
Expand All @@ -13,6 +14,7 @@
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>553674c9</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down Expand Up @@ -113,6 +115,7 @@
<Compile Include="RemoveWhereWhenItIsPossibleTests.cs" />
<Compile Include="TernaryOperatorTests.cs" />
<Compile Include="UnnecessaryParenthesisTests.cs" />
<Compile Include="UseStringBuilderToConcatenationTests.cs" />
<Compile Include="Verifiers\CodeFixTest.cs" />
<Compile Include="Verifiers\CodeFixVerifier.cs" />
<Compile Include="Verifiers\DiagnosticTest.cs" />
Expand Down Expand Up @@ -140,6 +143,12 @@
<Analyzer Include="..\..\packages\codecracker.1.0.0-alpha2-26\tools\analyzers\CodeCracker.dll" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.0.99.9-build1021\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.0.99.9-build1021\build\net20\xunit.runner.visualstudio.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
Expand Down
153 changes: 153 additions & 0 deletions test/CodeCracker.Test/UseStringBuilderToConcatenationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using Microsoft.CodeAnalysis;
using TestHelper;
using Xunit;

namespace CodeCracker.Test
{
public class UseStringBuilderToConcatenationTests : CodeFixTest<UseStringBuilderToConcatenationAnalyzer, UseStringBuilderToConcatenationCodeFixProvider>
{
[Fact]
public void IgnoresConcatenationMinorThanThree()
{
const string test = @"
using System;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var a = ""B"" + ""C"";
}
}
}";

VerifyCSharpHasNoDiagnostics(test);

}

[Fact]
public void IgnoresVariableDifferentOfString()
{
const string test = @"
using System;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var a = 1 + 1 + 1 + 1;
}
}
}";

VerifyCSharpHasNoDiagnostics(test);

}

[Fact]
public void CreateDiagnosticsWhenConcatenationGreaterThanTwo()
{
const string test = @"
using System;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var a = ""A"" + ""B"" + ""C"";
}
}
}";
var expected = new DiagnosticResult
{
Id = UseStringBuilderToConcatenationAnalyzer.DiagnosticId,
Message = "Use 'StringBuilder' instead of concatenation.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 17) }
};

VerifyCSharpDiagnostic(test, expected);

}

[Fact]
public void FixReplacesConcatenationWithoutVariable()
{
const string test = @"
using System;
using System.Text;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var a = ""A"" + ""B"" + ""C"";
}
}
}";
const string expected = @"
using System;
using System.Text;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var a = new StringBuilder().Append(""A"").Append(""B"").Append(""C"").ToString();
}
}
}";

VerifyCSharpFix(test, expected);

}

[Fact]
public void FixReplacesConcatenationWithVariable()
{
const string test = @"
using System;
using System.Text;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var b = ""D"";
var a = ""A"" + ""B"" + ""C"" + b;
}
}
}";
const string expected = @"
using System;
using System.Text;

namespace ConsoleApplication1
{
class TypeName
{
public void Foo()
{
var b = ""D"";
var a = new StringBuilder().Append(""A"").Append(""B"").Append(""C"").Append(b).ToString();
}
}
}";

VerifyCSharpFix(test, expected);

}
}
}
Loading