diff --git a/CodeCracker.sln b/CodeCracker.sln index 4a19abe95..a20270f5b 100644 --- a/CodeCracker.sln +++ b/CodeCracker.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.14 +VisualStudioVersion = 15.0.27130.2026 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCracker.Common", "src\Common\CodeCracker.Common\CodeCracker.Common.csproj", "{753D4757-FCBA-43BA-B1BE-89201ACDA192}" EndProject @@ -154,4 +154,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EF50C6AB-1421-41AE-8CB3-927AB24A69EA} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {15B937E4-F880-478B-B387-3438FDD895F7} + EndGlobalSection EndGlobal diff --git a/runTestsCS.ps1 b/runTestsCS.ps1 index 930bdccb4..b3f56a286 100644 --- a/runTestsCS.ps1 +++ b/runTestsCS.ps1 @@ -1,8 +1,10 @@ param([String]$testClass) -$testDllDirPath = "$PSScriptRoot\test\CSharp\CodeCracker.Test\bin\Debug\" +# $testDllDirPath = "$PSScriptRoot\test\CSharp\CodeCracker.Test\bin\Debug\" +$testDllDirPath = "test\CSharp\CodeCracker.Test\bin\Debug\" $testDllFileName = "CodeCracker.Test.CSharp.dll" $testDllFullFileName = "$testDllDirPath$testDllFileName" -$xunitConsole = "$PSScriptRoot\packages\xunit.runner.console.2.2.0\tools\xunit.console.x86.exe" +# $xunitConsole = "$PSScriptRoot\packages\xunit.runner.console.2.2.0\tools\xunit.console.x86.exe" +$xunitConsole = "packages\xunit.runner.console.2.2.0\tools\xunit.console.x86.exe" if (!(gcm nodemon -ErrorAction Ignore)) { Write-Host -ForegroundColor DarkRed 'Nodemon not found, install it with npm: `npm i -g nodemon`' diff --git a/src/CSharp/CodeCracker/CodeCracker.csproj b/src/CSharp/CodeCracker/CodeCracker.csproj index 337211dff..449e7bdff 100644 --- a/src/CSharp/CodeCracker/CodeCracker.csproj +++ b/src/CSharp/CodeCracker/CodeCracker.csproj @@ -71,6 +71,8 @@ + + diff --git a/src/CSharp/CodeCracker/Refactoring/SortUsingsAnalyzer.cs b/src/CSharp/CodeCracker/Refactoring/SortUsingsAnalyzer.cs new file mode 100644 index 000000000..2c0cdd16d --- /dev/null +++ b/src/CSharp/CodeCracker/Refactoring/SortUsingsAnalyzer.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CodeCracker.CSharp.Refactoring +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SortUsingsAnalyzer : DiagnosticAnalyzer + { + internal const string Title = "Sort by length."; + internal const string MessageFormat = "Sort Using directives by length"; + internal const string Category = SupportedCategories.Refactoring; + + internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId.SortUsings.ToDiagnosticId(), + Title, + MessageFormat, + Category, + DiagnosticSeverity.Hidden, + isEnabledByDefault: true, + helpLinkUri: HelpLink.ForDiagnostic(DiagnosticId.SortUsings)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.CompilationUnit); + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + if (context.IsGenerated()) return; + var root = (CompilationUnitSyntax)context.Node; + if (root.Usings.Count > 0) + context.ReportDiagnostic(Diagnostic.Create(Rule, root.GetLocation())); + } + } +} diff --git a/src/CSharp/CodeCracker/Refactoring/SortUsingsCodeFixProvider.cs b/src/CSharp/CodeCracker/Refactoring/SortUsingsCodeFixProvider.cs new file mode 100644 index 000000000..e96dbb46e --- /dev/null +++ b/src/CSharp/CodeCracker/Refactoring/SortUsingsCodeFixProvider.cs @@ -0,0 +1,60 @@ +using System.Linq; +using System.Threading; +using System.Composition; +using Microsoft.CodeAnalysis; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CodeCracker.CSharp.Refactoring +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SortUsingsCodeFixProvider)), Shared] + public class SortUsingsCodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticId.SortUsings.ToDiagnosticId()); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix(CodeAction.Create("Sort by length", ct => SortUsingsAsync(context.Document, diagnostic, ct), nameof(SortUsingsCodeFixProvider)), diagnostic); + return Task.FromResult(0); + } + + private static async Task SortUsingsAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var sourceSpan = (CompilationUnitSyntax)root.FindNode(diagnostic.Location.SourceSpan); + + var collector = new UsingCollector(); + collector.Visit(root); + + var sections = new List(); + sections.AddRange(collector.Usings + .OrderBy(u => u.ToString().Contains(" static ")) + .ThenBy(u => u.ToString().Length)); + + var newUsings = sourceSpan.WithUsings(SyntaxFactory.List(sections)).WithAdditionalAnnotations(Formatter.Annotation); + var newRoot = root.ReplaceNode(sourceSpan, newUsings); + return document.WithSyntaxRoot(newRoot); + } + } + + internal sealed class UsingCollector : CSharpSyntaxWalker + { + private readonly List usingCollection = new List(); + + public IEnumerable Usings => usingCollection; + + public override void VisitUsingDirective(UsingDirectiveSyntax node) + { + this.usingCollection.Add(node); + } + } +} diff --git a/src/Common/CodeCracker.Common/DiagnosticId.cs b/src/Common/CodeCracker.Common/DiagnosticId.cs index 4dfc7ecb6..3884c00dd 100644 --- a/src/Common/CodeCracker.Common/DiagnosticId.cs +++ b/src/Common/CodeCracker.Common/DiagnosticId.cs @@ -85,5 +85,6 @@ public enum DiagnosticId SwitchCaseWithoutDefault = 120, ReadOnlyComplexTypes = 121, ReplaceWithGetterOnlyAutoProperty = 125, + SortUsings = 126, } } \ No newline at end of file diff --git a/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj b/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj index 4d186cfbe..a883a6bf1 100644 --- a/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj +++ b/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj @@ -132,6 +132,7 @@ + diff --git a/test/CSharp/CodeCracker.Test/Refactoring/SortUsingsTest.cs b/test/CSharp/CodeCracker.Test/Refactoring/SortUsingsTest.cs new file mode 100644 index 000000000..0b61016dd --- /dev/null +++ b/test/CSharp/CodeCracker.Test/Refactoring/SortUsingsTest.cs @@ -0,0 +1,65 @@ +using Xunit; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using CodeCracker.CSharp.Refactoring; + +namespace CodeCracker.Test.CSharp.Refactoring +{ + public class SortUsingsTest : CodeFixVerifier + { + [Fact] + public async Task CreateDiagnosticSortingUsings() + { + const string usings = @"using System; +using System.Collections.Generic; +using System.Linq; +using static System.Console; +using System.Text; +using static System.DateTime; +using System.Threading.Tasks;"; + + const string source = @"static void Main(string[] args) +{ +}"; + + var diagnostic = new DiagnosticResult + { + Id = DiagnosticId.SortUsings.ToDiagnosticId(), + Message = "Sort Using directives by length", + Severity = DiagnosticSeverity.Hidden, + Locations = new[] { new DiagnosticResultLocation("Test0.cs", 2, 5) } + }; + await VerifyCSharpDiagnosticAsync(source.WrapInCSharpClass("Program", usings), diagnostic); + } + + [Fact] + public async Task FixSortingUsings() + { + const string actualUsings = @"using System; +using System.Collections.Generic; +using System.Linq; +using static System.Console; +using System.Text; +using static System.DateTime; +using System.Threading.Tasks;"; + + const string expectedUsings = @"using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using static System.Console; +using static System.DateTime;"; + + const string code = @"static void Main(string[] args) +{ +}"; + const string typeName = "Program"; + + var oldSource = code.WrapInCSharpClass(typeName, actualUsings); + var newSource = code.WrapInCSharpClass(typeName, expectedUsings); + await VerifyCSharpFixAsync(oldSource, newSource); + } + + } +}