diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5dc136a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Auto-detect text files so that they can be stored with LF endings +# in github, and CRLF endings locally in Windows. +* text=auto + +# Avoid potential misdetection of some common file types +*.cs text +*.csproj text +*.Config text +*.config text +*.StyleCop text +*.resx text + +# Apparently ReSharper uses LF line endings in its settings files, +# so tell git that we know this to avoid warnings. +*.DotSettings text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0881b8d..94e9b58 100644 --- a/.gitignore +++ b/.gitignore @@ -97,8 +97,10 @@ publish/ *.pubxml # NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ +packages/ + +# NuGet packages +*.nupkg # Windows Azure Build Output csx diff --git a/nuget/OrderUsings.nuspec b/nuget/OrderUsings.nuspec new file mode 100644 index 0000000..7bc4908 --- /dev/null +++ b/nuget/OrderUsings.nuspec @@ -0,0 +1,25 @@ + + + + OrderUsings + Order and space using directives + 1.0.0 + Ian Griffiths + Ian Griffiths + Enables configurable fine control over the ordering, grouping, and spacing of C# using directives. Instead of being limited to two groups - System*, and everything else - you can define any number of groups in any order to arrange using directives however makes most sense for your project. + Control over the ordering, grouping, and spacing of C# using directives. + Copyright © 2014 Ian Griffiths + • 1.0.0 - First release (supporting only ReSharper 8.1) + https://github.com/idg10/order-usings + https://github.com/idg10/order-usings/blob/master/LICENSE + + + + + + + + + diff --git a/src/.nuget/packages.config b/src/.nuget/packages.config new file mode 100644 index 0000000..7025a72 --- /dev/null +++ b/src/.nuget/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/OrderAndSpacingDeterminationTestBase.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/OrderAndSpacingDeterminationTestBase.cs new file mode 100644 index 0000000..5c751f4 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/OrderAndSpacingDeterminationTestBase.cs @@ -0,0 +1,102 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination +{ + using System.Collections.Generic; + using System.Linq; + + using NUnit.Framework; + + using OrderUsings.Configuration; + using OrderUsings.Processing; + + public abstract class OrderAndSpacingDeterminationTestBase + { + internal static readonly UsingDirective AliasSystemPathAsPath = new UsingDirective { Namespace = "System.IO.Path", Alias = "Path" }; + internal static readonly UsingDirective AliasSystemLaterAsEarlier = new UsingDirective { Namespace = "System.Later", Alias = "Earlier" }; + internal static readonly UsingDirective AliasSystemTextAsSystem = new UsingDirective { Namespace = "System.Text", Alias = "System" }; + + internal static readonly UsingDirective ImportSystem = new UsingDirective { Namespace = "System" }; + internal static readonly UsingDirective ImportSystemCollectionsGeneric = new UsingDirective { Namespace = "System.Collections.Generic" }; + internal static readonly UsingDirective ImportSystemLinq = new UsingDirective { Namespace = "System.Linq" }; + + internal static readonly UsingDirective ImportMicrosoftCSharp = new UsingDirective { Namespace = "Microsoft.CSharp" }; + + internal static readonly UsingDirective ImportOther = new UsingDirective { Namespace = "Other" }; + internal static readonly UsingDirective ImportOtherA = new UsingDirective { Namespace = "Other.A" }; + internal static readonly UsingDirective ImportOtherB = new UsingDirective { Namespace = "Other.B" }; + + internal static readonly UsingDirective ImportMyLocal = new UsingDirective { Namespace = "MyLocal" }; + internal static readonly UsingDirective ImportMyLocalA = new UsingDirective { Namespace = "MyLocal.A" }; + internal static readonly UsingDirective ImportMyLocalB = new UsingDirective { Namespace = "MyLocal.B" }; + + internal static readonly UsingDirective ImportRuhroh = new UsingDirective { Namespace = "Ruhroh" }; + + + internal OrderUsingsConfiguration Configuration { get; private set; } + + [SetUp] + public void BaseInitialize() + { + Configuration = new OrderUsingsConfiguration + { + GroupsAndSpaces = GetRules() + }; + } + + internal abstract List GetRules(); + + internal void Verify(UsingDirective[] directivesIn, params UsingDirective[][] expectedGroups) + { + foreach (var permutation in AllOrderings(directivesIn)) + { + var results = OrderAndSpacingGenerator.DetermineOrderAndSpacing( + permutation, Configuration); + + Assert.AreEqual(expectedGroups.Length, results.Count); + for (int i = 0; i < expectedGroups.Length; ++i) + { + Assert.AreEqual(results[i].Count, expectedGroups[i].Length, "Item count in group " + i); + for (int j = 0; j < expectedGroups[i].Length; ++j) + { + UsingDirective expectedUsing = expectedGroups[i][j]; + UsingDirective actualUsing = results[i][j]; + string message = string.Format( + "Expected {0} at {1},{2}, found {3}", expectedUsing, i, j, actualUsing); + Assert.AreSame(expectedUsing, actualUsing, message); + } + } + } + } + + // This is the same for all configurations. We only want to run the test + // once per config, so we don't make this a [Test] in this base class - that + // would run it once per derived class. Instead, just one derived classes + // per config will defer to this. + protected void VerifyEmptyHandling() + { + Verify(new UsingDirective[0], new UsingDirective[0][]); + } + + private static IEnumerable> AllOrderings(IEnumerable items) + { + bool returnedAtLeastOne = false; + int index = 0; +// ReSharper disable PossibleMultipleEnumeration + foreach (T item in items) + { + returnedAtLeastOne = true; + int thisIndex = index; + foreach (var remainders in AllOrderings(items.Where((x, i) => i != thisIndex))) + { + yield return new[] { item }.Concat(remainders); + } + index += 1; + } + // ReSharper restore PossibleMultipleEnumeration + + if (!returnedAtLeastOne) + { + yield return Enumerable.Empty(); + } + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/SinglePatternMatchesAllTestBase.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/SinglePatternMatchesAllTestBase.cs new file mode 100644 index 0000000..da6cbf6 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/SinglePatternMatchesAllTestBase.cs @@ -0,0 +1,24 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using System.Collections.Generic; + + using OrderUsings.Configuration; + + public abstract class SinglePatternMatchesAllTestBase : OrderAndSpacingDeterminationTestBase + { + internal override List GetRules() + { + return new List + { + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.ImportOrAlias, + NamespacePattern = "*", + AliasPattern = "*", + Priority = 1, + OrderAliasesBy = OrderAliasBy.Alias + }) + }; + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenAliasesOrderedByAlias.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenAliasesOrderedByAlias.cs new file mode 100644 index 0000000..83e3179 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenAliasesOrderedByAlias.cs @@ -0,0 +1,30 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using NUnit.Framework; + + public class WhenAliasesOrderedByAlias : SinglePatternMatchesAllTestBase + { + [Test] + public void ProducesSingleGroupOrderedByAliasWhenAliasesAndNamespaceOtherwise() + { + Verify( + new[] + { + AliasSystemPathAsPath, + ImportSystem, + ImportRuhroh, + AliasSystemLaterAsEarlier + }, + new[] + { + new[] + { + AliasSystemLaterAsEarlier, + AliasSystemPathAsPath, + ImportRuhroh, + ImportSystem + } + }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenAliasesOrderedByNamespace.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenAliasesOrderedByNamespace.cs new file mode 100644 index 0000000..9df0758 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenAliasesOrderedByNamespace.cs @@ -0,0 +1,49 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using System.Collections.Generic; + + using NUnit.Framework; + + using OrderUsings.Configuration; + + public class WhenAliasesOrderedByNamespace : SinglePatternMatchesAllTestBase + { + internal override List GetRules() + { + var r = base.GetRules(); + r[0].Rule.OrderAliasesBy = OrderAliasBy.Namespace; + return r; + } + + [Test] + public void ProducesSingleGroupOrderedByNamespace() + { + Verify( + new[] + { + AliasSystemPathAsPath, + ImportSystem, + ImportRuhroh, + AliasSystemLaterAsEarlier + }, + new[] + { + new[] + { + ImportRuhroh, + ImportSystem, + AliasSystemPathAsPath, + AliasSystemLaterAsEarlier + } + }); + } + + // This class introduces a change in config, so we should verify that + // empty input handling still works. + [Test] + public void EmptyUsingsProducesNoGroups() + { + VerifyEmptyHandling(); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenImportAndAliasShareName.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenImportAndAliasShareName.cs new file mode 100644 index 0000000..e0a1314 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenImportAndAliasShareName.cs @@ -0,0 +1,33 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using NUnit.Framework; + + public class WhenImportAndAliasShareName : SinglePatternMatchesAllTestBase + { + [Test] + public void GroupItemsShouldPutNonAliasFirst() + { + // Bizarre though it seems, this: + // using System; + // using System = System.Text; + // is legal. If a group orders using alias directives by Alias (which is the default) + // we need to pick one to go first. We put the non-alias one first (i.e., the order + // shown above), since this seems most consistent with the behaviour of ordering + // usings lexographically within a group. + Verify( + new[] + { + AliasSystemTextAsSystem, + ImportSystem + }, + new[] + { + new[] + { + ImportSystem, + AliasSystemTextAsSystem + } + }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenNoUsings.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenNoUsings.cs new file mode 100644 index 0000000..5293236 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenNoUsings.cs @@ -0,0 +1,13 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using NUnit.Framework; + + public class WhenNoUsings : SinglePatternMatchesAllTestBase + { + [Test] + public void ProducesNoGroups() + { + VerifyEmptyHandling(); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenOneImport.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenOneImport.cs new file mode 100644 index 0000000..a99b386 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenOneImport.cs @@ -0,0 +1,13 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using NUnit.Framework; + + public class WhenOneImport : SinglePatternMatchesAllTestBase + { + [Test] + public void ProducesOneGroup() + { + Verify(new[] { ImportSystem }, new[] { new[] { ImportSystem } }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenThreeImports.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenThreeImports.cs new file mode 100644 index 0000000..646d4de --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/SinglePatternMatchesAll/WhenThreeImports.cs @@ -0,0 +1,28 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.SinglePatternMatchesAll +{ + using NUnit.Framework; + + public class WhenThreeImports : SinglePatternMatchesAllTestBase + { + [Test] + public void ProducesOneGroupInAlphabeticalOrder() + { + Verify( + new[] + { + ImportSystem, + ImportSystemCollectionsGeneric, + ImportSystemLinq + }, + new[] + { + new[] + { + ImportSystem, + ImportSystemCollectionsGeneric, + ImportSystemLinq + } + }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase.cs new file mode 100644 index 0000000..e3de473 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase.cs @@ -0,0 +1,39 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificAdjacentPatternsWithoutSpaceThenFallback +{ + using System.Collections.Generic; + + using OrderUsings.Configuration; + using OrderUsings.Tests.OrderAndSpacingDetermination; + + public abstract class TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase : OrderAndSpacingDeterminationTestBase + { + internal override List GetRules() + { + return new List + { + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.Import, + NamespacePattern = "System*", + Priority = 1 + }), + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.Import, + NamespacePattern = "Microsoft*", + Priority = 1 + }), + + ConfigurationRule.ForSpace(), + + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.ImportOrAlias, + NamespacePattern = "*", + AliasPattern = "*", + Priority = 2 + }) + }; + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenNoUsings.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenNoUsings.cs new file mode 100644 index 0000000..cb15d91 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenNoUsings.cs @@ -0,0 +1,13 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificAdjacentPatternsWithoutSpaceThenFallback +{ + using NUnit.Framework; + + public class WhenNoUsings : TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase + { + [Test] + public void ProducesNoGroups() + { + VerifyEmptyHandling(); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenOneImport.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenOneImport.cs new file mode 100644 index 0000000..632d5fe --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenOneImport.cs @@ -0,0 +1,25 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificAdjacentPatternsWithoutSpaceThenFallback +{ + using NUnit.Framework; + + public class WhenOneImport : TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase + { + [Test] + public void FirstRuleMatchProducesOneGroup() + { + Verify(new[] { ImportSystem }, new[] { new[] { ImportSystem } }); + } + + [Test] + public void SecondRuleMatchProducesOneGroup() + { + Verify(new[] { ImportMicrosoftCSharp }, new[] { new[] { ImportMicrosoftCSharp } }); + } + + [Test] + public void FallbackRuleMatchProducesOneGroup() + { + Verify(new[] { ImportOther }, new[] { new[] { ImportOther } }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenThreeImports.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenThreeImports.cs new file mode 100644 index 0000000..126445a --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificAdjacentPatternsWithoutSpaceThenFallback/WhenThreeImports.cs @@ -0,0 +1,89 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificAdjacentPatternsWithoutSpaceThenFallback +{ + using NUnit.Framework; + + public class WhenThreeImports : TwoSpecificAdjacentPatternsWithoutSpaceThenFallbackBase + { + [Test] + public void AllMatchingFirstProducesOneGroup() + { + Verify( + new[] + { + ImportSystem, + ImportSystemCollectionsGeneric, + ImportSystemLinq + }, + new[] + { + new[] { ImportSystem, ImportSystemCollectionsGeneric, ImportSystemLinq } + }); + } + + [Test] + public void AllMatchingSecondProducesOneGroup() + { + Verify( + new[] + { + ImportSystem, + ImportSystemCollectionsGeneric, + ImportSystemLinq + }, + new[] + { + new[] { ImportSystem, ImportSystemCollectionsGeneric, ImportSystemLinq } + }); + } + + [Test] + public void AllMatchingFirstAndSecondInOrderProducesOneGroup() + { + Verify( + new[] + { + ImportSystem, + ImportSystemCollectionsGeneric, + ImportSystemLinq, + ImportMicrosoftCSharp + }, + new[] + { + new[] { ImportSystem, ImportSystemCollectionsGeneric, ImportSystemLinq, ImportMicrosoftCSharp } + }); + } + + [Test] + public void AllMatchingFirstAndSecondOutOfOrderProducesOneGroup() + { + Verify( + new[] + { + ImportSystem, + ImportMicrosoftCSharp, + ImportSystemCollectionsGeneric, + ImportSystemLinq + }, + new[] + { + new[] { ImportSystem, ImportSystemCollectionsGeneric, ImportSystemLinq, ImportMicrosoftCSharp } + }); + } + + [Test] + public void AllMatchFallbackProducesOneGroup() + { + Verify( + new[] + { + ImportOther, + ImportOtherA, + ImportOtherB + }, + new[] + { + new[] { ImportOther, ImportOtherA, ImportOtherB } + }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase.cs new file mode 100644 index 0000000..51a3e45 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase.cs @@ -0,0 +1,41 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificPatternsWithFallbackInMiddleAllSpaced +{ + using System.Collections.Generic; + + using OrderUsings.Configuration; + + public abstract class TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase : OrderAndSpacingDeterminationTestBase + { + internal override List GetRules() + { + return new List + { + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.Import, + NamespacePattern = "System*", + Priority = 1 + }), + + ConfigurationRule.ForSpace(), + + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.ImportOrAlias, + NamespacePattern = "*", + AliasPattern = "*", + Priority = 2 + }), + + ConfigurationRule.ForSpace(), + + ConfigurationRule.ForGroupRule(new GroupRule + { + Type = MatchType.Import, + NamespacePattern = "MyLocal*", + Priority = 1 + }) + }; + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenNoUsings.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenNoUsings.cs new file mode 100644 index 0000000..28ac5f9 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenNoUsings.cs @@ -0,0 +1,13 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificPatternsWithFallbackInMiddleAllSpaced +{ + using NUnit.Framework; + + public class WhenNoUsings : TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase + { + [Test] + public void ProducesNoGroups() + { + VerifyEmptyHandling(); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenOneImport.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenOneImport.cs new file mode 100644 index 0000000..d4b853c --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenOneImport.cs @@ -0,0 +1,25 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificPatternsWithFallbackInMiddleAllSpaced +{ + using NUnit.Framework; + + public class WhenOneImport : TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase + { + [Test] + public void FirstRuleMatchProducesOneGroup() + { + Verify(new[] { ImportSystem }, new[] { new[] { ImportSystem } }); + } + + [Test] + public void FallbackRuleMatchProducesOneGroup() + { + Verify(new[] { ImportOther }, new[] { new[] { ImportOther } }); + } + + [Test] + public void LastRuleMatchProducesOneGroup() + { + Verify(new[] { ImportMyLocal }, new[] { new[] { ImportMyLocal } }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenThreeImports.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenThreeImports.cs new file mode 100644 index 0000000..301f58f --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenThreeImports.cs @@ -0,0 +1,124 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificPatternsWithFallbackInMiddleAllSpaced +{ + using NUnit.Framework; + + public class WhenThreeImports : TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase + { + [Test] + public void AllMatchingFirstProducesOneGroup() + { + Verify( + new[] + { + ImportSystem, + ImportSystemCollectionsGeneric, + ImportSystemLinq + }, + new[] + { + new[] { ImportSystem, ImportSystemCollectionsGeneric, ImportSystemLinq } + }); + } + + [Test] + public void AllMatchFallbackProducesOneGroup() + { + Verify( + new[] + { + ImportOther, + ImportOtherA, + ImportOtherB + }, + new[] + { + new[] { ImportOther, ImportOtherA, ImportOtherB } + }); + } + + [Test] + public void AllMatchLastProducesOneGroup() + { + Verify( + new[] + { + ImportMyLocal, + ImportMyLocalA, + ImportMyLocalB + }, + new[] + { + new[] { ImportMyLocal, ImportMyLocalA, ImportMyLocalB } + }); + } + + [Test] + public void MatchingFirstAndFallbackProducesTwoGroups() + { + Verify( + new[] + { + ImportSystem, + ImportSystemLinq, + ImportOther + }, + new[] + { + new[] { ImportSystem, ImportSystemLinq }, + new[] { ImportOther } + }); + } + + [Test] + public void MatchingFirstAndLastProducesTwoGroups() + { + Verify( + new[] + { + ImportSystem, + ImportMyLocal, + ImportMyLocalA + }, + new[] + { + new[] { ImportSystem }, + new[] { ImportMyLocal, ImportMyLocalA } + }); + } + + [Test] + public void MatchingFallbackAndLastProducesTwoGroups() + { + Verify( + new[] + { + ImportOther, + ImportMyLocal, + ImportMyLocalA + }, + new[] + { + new[] { ImportOther }, + new[] { ImportMyLocal, ImportMyLocalA } + }); + } + + [Test] + public void MatchingFirstFallbackAndLastProducesThreeGroups() + { + Verify( + new[] + { + ImportSystem, + ImportOther, + ImportMyLocal + }, + new[] + { + new[] { ImportSystem }, + new[] { ImportOther }, + new[] { ImportMyLocal } + }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenTwoUsings.cs b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenTwoUsings.cs new file mode 100644 index 0000000..e8e20c2 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderAndSpacingDetermination/TwoSpecificPatternsWithFallbackInMiddleAllSpaced/WhenTwoUsings.cs @@ -0,0 +1,68 @@ +namespace OrderUsings.Tests.OrderAndSpacingDetermination.TwoSpecificPatternsWithFallbackInMiddleAllSpaced +{ + using NUnit.Framework; + + public class WhenTwoUsings : TwoSpecificPatternsWithFallbackInMiddleAllSpacedBase + { + [Test] + public void UsingsMatchFirstAndLastProduceTwoGroups() + { + Verify( + new[] + { + ImportSystem, + ImportMyLocal + }, + new[] + { + new[] { ImportSystem }, + new[] { ImportMyLocal } + }); + } + + [Test] + public void UsingsMatchingFirstRuleProduceOneGroup() + { + Verify( + new[] + { + ImportSystem, + ImportSystemLinq + }, + new[] + { + new[] { ImportSystem, ImportSystemLinq } + }); + } + + [Test] + public void UsingsMatchFallbackRuleProduceOneGroup() + { + Verify( + new[] + { + ImportOtherA, + ImportOtherB + }, + new[] + { + new[] { ImportOtherA, ImportOtherB } + }); + } + + [Test] + public void UsingsMatchingLastRuleProduceOneGroup() + { + Verify( + new[] + { + ImportMyLocalA, + ImportMyLocalB + }, + new[] + { + new[] { ImportMyLocalA, ImportMyLocalB } + }); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderChecking/WhenListsDifferent.cs b/src/OrderUsings.Core.Tests/OrderChecking/WhenListsDifferent.cs new file mode 100644 index 0000000..3c36af4 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderChecking/WhenListsDifferent.cs @@ -0,0 +1,43 @@ +namespace OrderUsings.Tests.OrderChecking +{ + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenListsDifferent + { + private static readonly UsingDirective D1 = new UsingDirective { Namespace = "System" }; + private static readonly UsingDirective D2 = new UsingDirective { Namespace = "System.Collections.Generic" }; + private static readonly UsingDirective D3 = new UsingDirective { Namespace = "System.Linq" }; + + [Test] + public void ReportsPositionWhenExpectedFirstItemInSecondPlace() + { + var required = new[] { D1, D2, D3 }; + var current = new[] { D2, D1, D3 }; + Relocation result = OrderChecker.GetNextUsingToMove(required, current); + Assert.AreEqual(1, result.From, "Source"); + Assert.AreEqual(0, result.To, "Destination"); + } + + [Test] + public void ReportsPositionWhenExpectedFirstItemInThirdPlace() + { + var required = new[] { D1, D2, D3 }; + var current = new[] { D2, D3, D1 }; + Relocation result = OrderChecker.GetNextUsingToMove(required, current); + Assert.AreEqual(2, result.From, "Source"); + Assert.AreEqual(0, result.To, "Destination"); + } + + [Test] + public void ReportsPositionWhenExpectedSecondItemInWrongPlace() + { + var required = new[] { D1, D2, D3 }; + var current = new[] { D1, D3, D2 }; + Relocation result = OrderChecker.GetNextUsingToMove(required, current); + Assert.AreEqual(2, result.From, "Source"); + Assert.AreEqual(1, result.To, "Destination"); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderChecking/WhenListsEmpty.cs b/src/OrderUsings.Core.Tests/OrderChecking/WhenListsEmpty.cs new file mode 100644 index 0000000..1b87f52 --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderChecking/WhenListsEmpty.cs @@ -0,0 +1,18 @@ +namespace OrderUsings.Tests.OrderChecking +{ + using System.Collections.Generic; + + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenListsEmpty + { + [Test] + public void ReturnsNull() + { + var empty = new List(); + Assert.IsNull(OrderChecker.GetNextUsingToMove(empty, empty)); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderChecking/WhenListsIdentical.cs b/src/OrderUsings.Core.Tests/OrderChecking/WhenListsIdentical.cs new file mode 100644 index 0000000..e2a175c --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderChecking/WhenListsIdentical.cs @@ -0,0 +1,19 @@ +namespace OrderUsings.Tests.OrderChecking +{ + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenListsIdentical + { + [Test] + public void ReturnsNull() + { + var d1 = new UsingDirective { Namespace = "System" }; + var d2 = new UsingDirective { Namespace = "System.Linq" }; + var required = new[] { d1, d2 }; + var current = new[] { d1, d2 }; + Assert.IsNull(OrderChecker.GetNextUsingToMove(required, current)); + } + } +} diff --git a/src/OrderUsings.Core.Tests/OrderUsings.Core.Tests.csproj b/src/OrderUsings.Core.Tests/OrderUsings.Core.Tests.csproj new file mode 100644 index 0000000..30534bc --- /dev/null +++ b/src/OrderUsings.Core.Tests/OrderUsings.Core.Tests.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {638CD37F-59CB-42AE-873D-5E09E9C32EAA} + Library + Properties + OrderUsings.Tests + OrderUsings.Core.Tests + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\NUnit.2.6.3\lib\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9875da0a-db09-47b2-80b5-80b08e430cef} + OrderUsings.Core + + + + + + + + \ No newline at end of file diff --git a/src/OrderUsings.Core.Tests/Properties/AssemblyInfo.cs b/src/OrderUsings.Core.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4694ce8 --- /dev/null +++ b/src/OrderUsings.Core.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("OrderUsings.Core.Tests")] +[assembly: AssemblyDescription("Tests for Order Usings core logic")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Ian Griffiths")] +[assembly: AssemblyProduct("Order Usings")] +[assembly: AssemblyCopyright("Copyright © Ian Griffiths, 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5b04bc0f-0e52-463e-9e41-e4024a4bda3d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/OrderUsings.Core.Tests/Settings.StyleCop b/src/OrderUsings.Core.Tests/Settings.StyleCop new file mode 100644 index 0000000..ca702f3 --- /dev/null +++ b/src/OrderUsings.Core.Tests/Settings.StyleCop @@ -0,0 +1,39 @@ + + + + + + + False + + + + + + + + + + False + + + + + False + + + + + + + + + + False + + + + + + + \ No newline at end of file diff --git a/src/OrderUsings.Core.Tests/SpacingChecking/WhenListsEmpty.cs b/src/OrderUsings.Core.Tests/SpacingChecking/WhenListsEmpty.cs new file mode 100644 index 0000000..0779bf1 --- /dev/null +++ b/src/OrderUsings.Core.Tests/SpacingChecking/WhenListsEmpty.cs @@ -0,0 +1,18 @@ +namespace OrderUsings.Tests.SpacingChecking +{ + using System.Collections.Generic; + + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenListsEmpty + { + [Test] + public void ReturnsNull() + { + var empty = new List>(); + Assert.IsNull(SpacingChecker.GetNextModification(empty, empty)); + } + } +} diff --git a/src/OrderUsings.Core.Tests/SpacingChecking/WhenRequiredSpaceNotPresent.cs b/src/OrderUsings.Core.Tests/SpacingChecking/WhenRequiredSpaceNotPresent.cs new file mode 100644 index 0000000..bc5c434 --- /dev/null +++ b/src/OrderUsings.Core.Tests/SpacingChecking/WhenRequiredSpaceNotPresent.cs @@ -0,0 +1,33 @@ +namespace OrderUsings.Tests.SpacingChecking +{ + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenRequiredSpaceNotPresent + { + private static readonly UsingDirective D1 = new UsingDirective { Namespace = "System" }; + private static readonly UsingDirective D2 = new UsingDirective { Namespace = "Moq" }; + private static readonly UsingDirective D3 = new UsingDirective { Namespace = "MyProject" }; + + [Test] + public void ReportsPositionWhenNoSpacePresent() + { + var required = new[] { new[] { D1, D2 }, new[] { D3 } }; + var current = new[] { new[] { D2, D1, D3 } }; + SpaceChange result = SpacingChecker.GetNextModification(required, current); + Assert.IsTrue(result.ShouldInsert, "ShouldInsert"); + Assert.AreEqual(2, result.Index); + } + + [Test] + public void ReportsPositionWhenSpaceTooLate() + { + var required = new[] { new[] { D1 }, new[] { D3, D2 } }; + var current = new[] { new[] { D1, D2 }, new[] { D3 } }; + SpaceChange result = SpacingChecker.GetNextModification(required, current); + Assert.IsTrue(result.ShouldInsert, "ShouldInsert"); + Assert.AreEqual(1, result.Index); + } + } +} diff --git a/src/OrderUsings.Core.Tests/SpacingChecking/WhenSpacingCorrect.cs b/src/OrderUsings.Core.Tests/SpacingChecking/WhenSpacingCorrect.cs new file mode 100644 index 0000000..d4f319f --- /dev/null +++ b/src/OrderUsings.Core.Tests/SpacingChecking/WhenSpacingCorrect.cs @@ -0,0 +1,29 @@ +namespace OrderUsings.Tests.SpacingChecking +{ + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenSpacingCorrect + { + private static readonly UsingDirective D1 = new UsingDirective { Namespace = "System" }; + private static readonly UsingDirective D2 = new UsingDirective { Namespace = "Moq" }; + private static readonly UsingDirective D3 = new UsingDirective { Namespace = "MyProject" }; + + [Test] + public void WithTwoGroups() + { + var required = new[] { new[] { D1, D2 }, new[] { D3 } }; + var current = new[] { new[] { D1, D2 }, new[] { D3 } }; + Assert.IsNull(SpacingChecker.GetNextModification(required, current)); + } + + [Test] + public void WithOneGroup() + { + var required = new[] { new[] { D1, D3, D2 } }; + var current = new[] { new[] { D1, D2, D3 } }; + Assert.IsNull(SpacingChecker.GetNextModification(required, current)); + } + } +} diff --git a/src/OrderUsings.Core.Tests/SpacingChecking/WhenUnwantedSpacePresent.cs b/src/OrderUsings.Core.Tests/SpacingChecking/WhenUnwantedSpacePresent.cs new file mode 100644 index 0000000..a24e5f6 --- /dev/null +++ b/src/OrderUsings.Core.Tests/SpacingChecking/WhenUnwantedSpacePresent.cs @@ -0,0 +1,33 @@ +namespace OrderUsings.Tests.SpacingChecking +{ + using NUnit.Framework; + + using OrderUsings.Processing; + + public class WhenUnwantedSpacePresent + { + private static readonly UsingDirective D1 = new UsingDirective { Namespace = "System" }; + private static readonly UsingDirective D2 = new UsingDirective { Namespace = "Moq" }; + private static readonly UsingDirective D3 = new UsingDirective { Namespace = "MyProject" }; + + [Test] + public void ReportsPositionWhenOnlyOneWasExpected() + { + var required = new[] { new[] { D1, D2 }, new[] { D3 } }; + var current = new[] { new[] { D1 }, new[] { D2 }, new[] { D3 } }; + SpaceChange result = SpacingChecker.GetNextModification(required, current); + Assert.IsFalse(result.ShouldInsert, "ShouldInsert"); + Assert.AreEqual(1, result.Index); + } + + [Test] + public void ReportsPositionWhenNoSpaceExpected() + { + var required = new[] { new[] { D1, D3, D2 } }; + var current = new[] { new[] { D1, D2 }, new[] { D3 } }; + SpaceChange result = SpacingChecker.GetNextModification(required, current); + Assert.IsFalse(result.ShouldInsert, "ShouldInsert"); + Assert.AreEqual(2, result.Index); + } + } +} diff --git a/src/OrderUsings.Core.Tests/packages.config b/src/OrderUsings.Core.Tests/packages.config new file mode 100644 index 0000000..ad37a52 --- /dev/null +++ b/src/OrderUsings.Core.Tests/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/OrderUsings.Core/Configuration/ConfigurationRule.cs b/src/OrderUsings.Core/Configuration/ConfigurationRule.cs new file mode 100644 index 0000000..3387846 --- /dev/null +++ b/src/OrderUsings.Core/Configuration/ConfigurationRule.cs @@ -0,0 +1,66 @@ +namespace OrderUsings.Configuration +{ + using System; + + /// + /// Represents a single entry from the configuration - either a + /// or an entry indicating that a blank line is required. + /// + public struct ConfigurationRule + { + private readonly GroupRule _rule; + + /// + /// Initializes a representing a . + /// + /// The rule. + private ConfigurationRule(GroupRule rule) + { + _rule = rule; + } + + /// + /// Gets a value indicating whether this rule represents a space or a group rule. + /// + public bool IsSpace + { + get { return _rule == null; } + } + + /// + /// Gets this entry's group rule. (Throws if you use this on an entry representing + /// a space.) + /// + public GroupRule Rule + { + get + { + if (_rule == null) + { + throw new InvalidOperationException("Cannot fetch Rule for entry representing space"); + } + + return _rule; + } + } + + /// + /// Creates a representing a . + /// + /// The rule. + /// The configuration rule. + public static ConfigurationRule ForGroupRule(GroupRule rule) + { + return new ConfigurationRule(rule); + } + + /// + /// Creates a representing a space between group rules. + /// + /// The configuration rule. + public static ConfigurationRule ForSpace() + { + return new ConfigurationRule(); + } + } +} diff --git a/src/OrderUsings.Core/Configuration/ConfigurationSerializer.cs b/src/OrderUsings.Core/Configuration/ConfigurationSerializer.cs new file mode 100644 index 0000000..00ed5b0 --- /dev/null +++ b/src/OrderUsings.Core/Configuration/ConfigurationSerializer.cs @@ -0,0 +1,85 @@ +namespace OrderUsings.Configuration +{ + using System; + using System.IO; + using System.Linq; + using System.Xml.Linq; + + /// + /// Loads and saves configuration settings. + /// + public class ConfigurationSerializer + { + private const string Ns = "http://schemas.interact-sw.co.uk/OrderUsings/2014"; + private static readonly XName GroupsName = XName.Get("Groups", Ns); + private static readonly XName GroupName = XName.Get("Group", Ns); + private static readonly XName SpaceName = XName.Get("Space", Ns); + + /// + /// Loads configuration settings from XML. + /// + /// A stream containing settings in XML. + /// A representing the settings + /// in the file. + public static OrderUsingsConfiguration FromXml(TextReader xmlConfiguration) + { + var xml = XDocument.Load(xmlConfiguration); + var groupsElement = xml.Element(GroupsName); + if (groupsElement == null) + { + throw new ArgumentException("Root element must be Groups"); + } + + return new OrderUsingsConfiguration + { + GroupsAndSpaces = xml + .Elements(XName.Get("Groups", Ns)) + .Elements() + .Select(RuleFromElement) + .ToList() + }; + } + + /// + /// Produces a from an XML element, which must be + /// either a Group or a Space element. + /// + /// The XML element describing the rule. + /// A . + private static ConfigurationRule RuleFromElement(XElement elem) + { + if (elem.Name == GroupName) + { + var aliasText = (string) elem.Attribute("Type"); + var type = MatchType.Import; + switch (aliasText) + { + case "Alias": + type = MatchType.Alias; + break; + + case "ImportOrAlias": + type = MatchType.ImportOrAlias; + break; + } + + return ConfigurationRule.ForGroupRule(new GroupRule + { + Priority = ((int?) elem.Attribute("Priority")) ?? 1, + NamespacePattern = (string) elem.Attribute("NamespacePattern"), + AliasPattern = (string) elem.Attribute("AliasPattern"), + Type = type, + OrderAliasesBy = ((string) elem.Attribute("AliasOrderKey")) == "Namespace" ? + OrderAliasBy.Namespace : OrderAliasBy.Alias + }); + } + + if (elem.Name == SpaceName) + { + return ConfigurationRule.ForSpace(); + } + + throw new ArgumentException("Elements in must be either or "); + } + } +} diff --git a/src/OrderUsings.Core/Configuration/GroupRule.cs b/src/OrderUsings.Core/Configuration/GroupRule.cs new file mode 100644 index 0000000..3bcded4 --- /dev/null +++ b/src/OrderUsings.Core/Configuration/GroupRule.cs @@ -0,0 +1,52 @@ +namespace OrderUsings.Configuration +{ + /// + /// Represents a configuration entry describing a group of namespaces. + /// + public class GroupRule + { + /// + /// Gets or sets a value that determines which group wins when a directive + /// matches multiple groups' rules. + /// + public int Priority { get; set; } + + /// + /// Gets or sets the regular expression against which a using directive's + /// namespace will be tested, to determine whether it belongs to this group. + /// + /// + /// Only used for rules that match using directives that import namespaces, + /// i.e. when is either + /// or . + /// + public string NamespacePattern { get; set; } + + /// + /// Gets or sets the pattern against which a using directive's + /// alias will be tested (in the case where the directive defines an + /// alias) to determine whether it belongs to this group. + /// + /// + /// Use a * for wildcard matching, e.g. System*. + /// + /// Only used for rules that match using alias directives i.e. when + /// is either + /// or . + /// + /// + public string AliasPattern { get; set; } + + /// + /// Gets or sets a value indicating what types of using directives this + /// rule matches. + /// + public MatchType Type { get; set; } + + /// + /// Gets or sets a value indicating how using alias directives should be ordered + /// within the group. + /// + public OrderAliasBy OrderAliasesBy { get; set; } + } +} diff --git a/src/OrderUsings.Core/Configuration/MatchType.cs b/src/OrderUsings.Core/Configuration/MatchType.cs new file mode 100644 index 0000000..c7c22c3 --- /dev/null +++ b/src/OrderUsings.Core/Configuration/MatchType.cs @@ -0,0 +1,23 @@ +namespace OrderUsings.Configuration +{ + /// + /// Describes how a matches a using directive. + /// + public enum MatchType + { + /// + /// Match only directives that import types from a namespace (and not using alias directives). + /// + Import, + + /// + /// Match only using alias directives. + /// + Alias, + + /// + /// Match using directives of either kind. + /// + ImportOrAlias + } +} diff --git a/src/OrderUsings.Core/Configuration/OrderAliasBy.cs b/src/OrderUsings.Core/Configuration/OrderAliasBy.cs new file mode 100644 index 0000000..a22f4c6 --- /dev/null +++ b/src/OrderUsings.Core/Configuration/OrderAliasBy.cs @@ -0,0 +1,68 @@ +namespace OrderUsings.Configuration +{ + /// + /// Determines how using alias directives are ordered within a group. + /// + /// + /// + /// If a single group ends up containing multiple using alias directives, we can + /// sort them either by the alias itself or the namespace. E.g., if you choose + /// , you get: + /// + /// + /// using B = Quux; + /// using D = Foo; + /// + /// + /// But with , the same directives would (if they end up matching + /// the same group) be sorted so that the namespaces are in order, i.e.: + /// + /// + /// using D = Foo; + /// using B = Quux; + /// + /// + /// If a single includes both using alias directives and + /// ordinary using directives, these will be intermingled. (You use separate rules + /// if you want them separated.) So with you would get this + /// sort of thing: + /// + /// + /// using A; + /// using A.Something; + /// using B = Quux; + /// using C.G; + /// using D = Foo; + /// using Faz; + /// using Foz; + /// using P; + /// using Z; + /// + /// + /// With , the same directives would go in this order: + /// + /// + /// using A; + /// using A.Something; + /// using C.G; + /// using Faz; + /// using D = Foo; + /// using Foz; + /// using P; + /// using B = Quux; + /// using Z; + /// + /// + public enum OrderAliasBy + { + /// + /// The order is based on the alias defined by the directive. + /// + Alias, + + /// + /// The order is based on the directive's namespace. + /// + Namespace + } +} diff --git a/src/OrderUsings.Core/Configuration/OrderUsingsConfiguration.cs b/src/OrderUsings.Core/Configuration/OrderUsingsConfiguration.cs new file mode 100644 index 0000000..33f85e7 --- /dev/null +++ b/src/OrderUsings.Core/Configuration/OrderUsingsConfiguration.cs @@ -0,0 +1,15 @@ +namespace OrderUsings.Configuration +{ + using System.Collections.Generic; + + /// + /// Describes the required order and spacing for using directives. + /// + public class OrderUsingsConfiguration + { + /// + /// Gets or sets the grouping and spacing rules. + /// + public List GroupsAndSpaces { get; set; } + } +} diff --git a/src/OrderUsings.Core/OrderUsings.Core.csproj b/src/OrderUsings.Core/OrderUsings.Core.csproj new file mode 100644 index 0000000..9aa0e66 --- /dev/null +++ b/src/OrderUsings.Core/OrderUsings.Core.csproj @@ -0,0 +1,67 @@ + + + + + Debug + AnyCPU + {9875DA0A-DB09-47B2-80B5-80B08E430CEF} + Library + Properties + OrderUsings + OrderUsings.Core + v4.0 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/OrderUsings.Core/Processing/ImportInspector.cs b/src/OrderUsings.Core/Processing/ImportInspector.cs new file mode 100644 index 0000000..202e05e --- /dev/null +++ b/src/OrderUsings.Core/Processing/ImportInspector.cs @@ -0,0 +1,136 @@ +namespace OrderUsings.Processing +{ + using System.Collections.Generic; + using System.Linq; + + using OrderUsings.Configuration; + + /// + /// Contains logic for import inspection that is common to various processing stages. + /// + public static class ImportInspector + { + /// + /// Takes a configuration and a description of a using directive list, and + /// produces two things: a list containing just the directives (with the + /// items representing blank lines removed), and a description of the + /// correct order and grouping for these items for the given configuration. + /// + /// The configuration that will determine the + /// correct order and grouping. + /// A list of using directives and the blank lines + /// interspersed therein. (To simplify processing, this may be null to + /// represent the absence of any using directives.) + /// The 'flattened' list (just the using directives, + /// with any blank lines stripped out) will be written to this argument, + /// unless items is null, in which case this will be set to null. + /// The correct ordering and spacing + /// for the using directives (as determined by the configuration) will be + /// written to this argument (unless items is null, in which case + /// this will be set to null). + /// + /// The correct order and spacing is represented as a list of lists. Each + /// nested list represents a group of usings, where each group should be + /// separated by a blank line. + /// + public static void FlattenImportsAndDetermineOrderAndSpacing( + OrderUsingsConfiguration configuration, + List items, + out List imports, + out List> requiredOrderByGroups) + { + imports = null; + requiredOrderByGroups = null; + if (items != null) + { + imports = items + .Where(i => !i.IsBlankLine) + .Select(i => i.Directive) + .ToList(); + + requiredOrderByGroups = + OrderAndSpacingGenerator.DetermineOrderAndSpacing(imports, configuration); + } + } + + /// + /// Determines which using directive should be moved where to bring the directive one + /// step closer to the correct order. This method should be called repeatedly (applying + /// each update that it generates between each call) until it returns null to indicate + /// that the order is correct. + /// + /// The correct order (e.g., as determined by + /// a call to ). + /// The using directives in their current order. + /// Null if the directives are already in the correct order. Otherwise, + /// a describing which item to move where. + /// + /// This method will only ever indicate that directives should be moved backwards. + /// This means that it will not necessarily produce the optimal sequence of changes. + /// (E.g., given a target order of 1,2,3,4,5 and a current order of of 5,1,2,3,4, + /// you would end up calling this method 4 times even though an optimal re-ordering + /// that allowed items to move forwards would need only one move). It does keep + /// things simple, though. + /// + public static Relocation GetNextUsingToMove( + List> requiredOrderByGroups, + List imports) + { + var requiredOrder = + from itemGroup in requiredOrderByGroups + from item in itemGroup + select item; + return OrderChecker.GetNextUsingToMove(requiredOrder, imports); + } + + /// + /// Given a set of using directives which are already in the correct order, determines + /// where to remove or add a blank line to bring them one step closer to the correct + /// spacing. This method should be called repeatedly (applying each update that it + /// generates between each call) until it returns null to indicate that the order + /// is correct. + /// + /// The correct order and spacing (e.g., as + /// determined by a call to ). + /// The directives as they are currently ordered and spaced. + /// Null if the directives are already in the correct order. Otherwise, + /// a describing where to add or remove a blank line. + public static SpaceChange GetNextSpacingModification( + List> requiredOrderByGroups, + List items) + { + SpaceChange nextChange = null; + if (requiredOrderByGroups != null) + { + var importsByGroup = new List>(); + foreach (UsingDirectiveOrSpace item in items) + { + if (importsByGroup.Count == 0) + { + importsByGroup.Add(new List()); + } + + List currentGroup = importsByGroup[importsByGroup.Count - 1]; + + if (item.IsBlankLine) + { + if (currentGroup.Count > 0) + { + importsByGroup.Add(new List()); + } + } + else + { + currentGroup.Add(item.Directive); + } + } + + nextChange = SpacingChecker.GetNextModification( + requiredOrderByGroups, + importsByGroup); + } + + return nextChange; + } + } +} diff --git a/src/OrderUsings.Core/Processing/OrderAndSpacingGenerator.cs b/src/OrderUsings.Core/Processing/OrderAndSpacingGenerator.cs new file mode 100644 index 0000000..c032e44 --- /dev/null +++ b/src/OrderUsings.Core/Processing/OrderAndSpacingGenerator.cs @@ -0,0 +1,139 @@ +namespace OrderUsings.Processing +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + using OrderUsings.Configuration; + + /// + /// Generates the correct order and spacing for a set of using directives + /// given the rules in a particular configuration. + /// + public static class OrderAndSpacingGenerator + { + private static readonly UsingComparer CompareByAlias = new UsingComparer(OrderAliasBy.Alias); + private static readonly UsingComparer CompareByNamespace = new UsingComparer(OrderAliasBy.Namespace); + + /// + /// Calculates how using directives should be ordered and, where appropriate, + /// split into groups separated by blank lines. + /// + /// The directives for which to determine the order + /// and spacing. + /// The configuration settings describing the + /// required ordering and spacing. + /// A list of lists. This will be empty if the input list was empty. + /// Otherwise there will be at least one list; if the rules require that any + /// of the directives be separated by spaces this will be indicated by + /// returning multiple lists - a blank line should appear between each of + /// the lists. Within each of the nested lists returned, the directives are + /// in the order they should appear. + public static List> DetermineOrderAndSpacing( + IEnumerable directives, OrderUsingsConfiguration configuration) + { + if (directives == null) + { + throw new ArgumentNullException("directives"); + } + + Dictionary groupNamespaceMatchers = configuration.GroupsAndSpaces + .Where(gs => !gs.IsSpace) + .ToDictionary( + gs => gs.Rule, + gs => new Regex(gs.Rule.NamespacePattern.Replace(".", "\\.").Replace("*", ".*"))); + + ILookup directivesByGroup = directives.ToLookup( + d => groupNamespaceMatchers + .Where(e => e.Value.IsMatch(d.Namespace)) + .OrderBy(e => e.Key.Priority) + .First().Key); + + List currentItemSet = null; + var results = new List>(); + GroupRule lastRule = null; + foreach (ConfigurationRule ruleEntry in configuration.GroupsAndSpaces) + { + if (ruleEntry.IsSpace) + { + MakeGroupFromCurrentItemsIfAny(ref currentItemSet, lastRule, results); + } + else + { + UsingComparer comparer = ruleEntry.Rule.OrderAliasesBy == OrderAliasBy.Alias ? + CompareByAlias : CompareByNamespace; + if (currentItemSet == null) + { + currentItemSet = new List(); + } + + currentItemSet.AddRange(directivesByGroup[ruleEntry.Rule].OrderBy(d => d, comparer)); + lastRule = ruleEntry.Rule; + } + } + + MakeGroupFromCurrentItemsIfAny(ref currentItemSet, lastRule, results); + + return results; + } + + private static void MakeGroupFromCurrentItemsIfAny( + ref List currentItemSet, GroupRule lastRule, List> results) + { + if (currentItemSet != null && currentItemSet.Count > 0 && lastRule != null) + { + results.Add(currentItemSet); + currentItemSet = null; + } + } + + /// + /// Determines the order in which directives should appear within a group. + /// + private class UsingComparer : IComparer + { + private readonly OrderAliasBy _orderType; + + public UsingComparer(OrderAliasBy orderType) + { + _orderType = orderType; + } + + public int Compare(UsingDirective x, UsingDirective y) + { + string left, right; + if (_orderType == OrderAliasBy.Alias) + { + left = x.Alias ?? x.Namespace; + right = y.Alias ?? y.Namespace; + } + else + { + left = x.Namespace; + right = y.Namespace; + } + + int result = string.Compare(left, right, StringComparison.Ordinal); + if (result == 0) + { + // In general a match means that one is an alias, and the other is an import, e.g. + // using System; + // using System = Foo.Bar; + // + // or if we're sorting by Namespace, + // using System; + // using Bar = System; + // + // In either case, we want the one that's not an alias to go first. + if (!ReferenceEquals(x.Alias, y.Alias)) + { + result = x.Alias == null ? -1 : 1; + } + } + + return result; + } + } + } +} diff --git a/src/OrderUsings.Core/Processing/OrderChecker.cs b/src/OrderUsings.Core/Processing/OrderChecker.cs new file mode 100644 index 0000000..82c1115 --- /dev/null +++ b/src/OrderUsings.Core/Processing/OrderChecker.cs @@ -0,0 +1,59 @@ +namespace OrderUsings.Processing +{ + using System; + using System.Collections.Generic; + + /// + /// Checks that the order of using directives matches the required order. + /// + public static class OrderChecker + { + /// + /// Compares the order of using directives in two lists. Returns null if they are + /// the same, and otherwise returns a description of the first element to move + /// to bring the current order closer into line with the required order. + /// + /// The order in which the directives should appear. + /// The order in which the directives currently appear. + /// Null if the orders match. Otherwise, a describing + /// the first element to move to bring the order closer to the required one. + /// + /// Code that simply needs to know whether the order is correct (e.g., when we want to + /// highlight bad ordering) will just use this to get a yes/no answer. Code that wants + /// to fix the order will call this repeatedly to generate a sequence of moves. + /// + public static Relocation GetNextUsingToMove( + IEnumerable requiredOrder, IEnumerable currentOrder) + { + int expectedIndex = 0; + using (var reqIt = requiredOrder.GetEnumerator()) + using (var currentIt = currentOrder.GetEnumerator()) + { + while (reqIt.MoveNext() && currentIt.MoveNext()) + { + UsingDirective expected = reqIt.Current; + if (!ReferenceEquals(expected, currentIt.Current)) + { + int currentIndex = expectedIndex; + while (currentIt.MoveNext()) + { + currentIndex += 1; + if (ReferenceEquals(expected, currentIt.Current)) + { + return new Relocation(currentIndex, expectedIndex); + } + } + + throw new ArgumentException( + "Lists should contain same items, but currentOrder was missing " + expected, + "currentOrder"); + } + + expectedIndex += 1; + } + } + + return null; + } + } +} diff --git a/src/OrderUsings.Core/Processing/Relocation.cs b/src/OrderUsings.Core/Processing/Relocation.cs new file mode 100644 index 0000000..cead01f --- /dev/null +++ b/src/OrderUsings.Core/Processing/Relocation.cs @@ -0,0 +1,39 @@ +namespace OrderUsings.Processing +{ + using System; + + /// + /// Describes how a using directive should be repositioned to bring a list + /// of usings one step closer to the configured order. + /// + public class Relocation + { + /// + /// Initializes a . + /// + /// The index of the item to be moved. + /// The index to which to move the item. Must be lower than from. + public Relocation(int from, int to) + { + if (from <= to) + { + throw new ArgumentException( + "Items must move towards front of list, so to must be lower than from", "to"); + } + + From = from; + To = to; + } + + /// + /// Gets the index of the item to move. This will always be higher than . + /// + public int From { get; private set; } + + /// + /// Gets the index to which the item should be moved. This will always be lower than + /// . + /// + public int To { get; private set; } + } +} diff --git a/src/OrderUsings.Core/Processing/SpaceChange.cs b/src/OrderUsings.Core/Processing/SpaceChange.cs new file mode 100644 index 0000000..b2e8147 --- /dev/null +++ b/src/OrderUsings.Core/Processing/SpaceChange.cs @@ -0,0 +1,42 @@ +namespace OrderUsings.Processing +{ + /// + /// Describes how to adjust the spacing in a list of using directives to bring them + /// one step closer to the configured spacing. + /// + public class SpaceChange + { + /// + /// Gets a value indicating whether the list should be changed by adding or + /// removing a space. + /// + public bool ShouldInsert { get; private set; } + + /// + /// Gets the index at which to add or remove a space. + /// + public int Index { get; private set; } + + /// + /// Creates a indicating that space should be inserted at + /// a particular index. + /// + /// The index at which space should be inserted. + /// A . + public static SpaceChange Insert(int index) + { + return new SpaceChange { ShouldInsert = true, Index = index }; + } + + /// + /// Creates a indicating that space should be removed at + /// a particular index. + /// + /// The index at which space should be removed. + /// A . + public static SpaceChange Remove(int index) + { + return new SpaceChange { ShouldInsert = false, Index = index }; + } + } +} diff --git a/src/OrderUsings.Core/Processing/SpacingChecker.cs b/src/OrderUsings.Core/Processing/SpacingChecker.cs new file mode 100644 index 0000000..79bf14b --- /dev/null +++ b/src/OrderUsings.Core/Processing/SpacingChecker.cs @@ -0,0 +1,52 @@ +namespace OrderUsings.Processing +{ + using System.Collections.Generic; + using System.Linq; + + /// + /// Checks that using directives have the required spacing. + /// + public static class SpacingChecker + { + /// + /// Compares the spacing (as represented by grouping) of using directives in + /// two lists. Returns null if they are the same, and otherwise returns a + /// description of the first line at which to either remove or insert space + /// to bring the list closer into line with the required spacing. (This presumes + /// that the order is already correct.) + /// + /// The required order and spacing, where + /// spacing is denoted by grouping. + /// The current order and spacing. + /// Null if the spacing matches. Otherwise, a + /// describing where to add or remove a blank line. + public static SpaceChange GetNextModification( + IEnumerable> requiredGroups, + IEnumerable> currentGroups) + { + var requiredByGroup = requiredGroups + .SelectMany((items, groupIndex) => items.Select(item => groupIndex)); + var currentByGroup = currentGroups + .SelectMany((items, groupIndex) => items.Select(item => groupIndex)); + var itemsByGroup = requiredByGroup.Zip(currentByGroup, (required, actual) => new { required, actual }); + + int index = 0; + foreach (var groupIndices in itemsByGroup) + { + if (groupIndices.actual < groupIndices.required) + { + return SpaceChange.Insert(index); + } + + if (groupIndices.actual > groupIndices.required) + { + return SpaceChange.Remove(index); + } + + index += 1; + } + + return null; + } + } +} diff --git a/src/OrderUsings.Core/Processing/UsingDirective.cs b/src/OrderUsings.Core/Processing/UsingDirective.cs new file mode 100644 index 0000000..3abc603 --- /dev/null +++ b/src/OrderUsings.Core/Processing/UsingDirective.cs @@ -0,0 +1,32 @@ +namespace OrderUsings.Processing +{ + /// + /// A non-technology-specific representation of a using directive. + /// + /// + /// We use this so that the core logic doesn't need to depend on any particular + /// framework. (This decouples us from any single version of ReSharper, or even + /// ReSharper at all, since I'd like to offer a StyleCop plug-in at some point.) + /// + public class UsingDirective + { + /// + /// Gets or sets the alias name created by this declaration, or null + /// if this is an import. + /// + public string Alias { get; set; } + + /// + /// Gets or sets either the namespace that this declaration imports, or + /// (if is non-null) the type or namespace for which + /// this defines an alias. + /// + public string Namespace { get; set; } + + /// + public override string ToString() + { + return Alias == null ? Namespace : Alias + " = " + Namespace; + } + } +} diff --git a/src/OrderUsings.Core/Processing/UsingDirectiveOrSpace.cs b/src/OrderUsings.Core/Processing/UsingDirectiveOrSpace.cs new file mode 100644 index 0000000..2d1ec4a --- /dev/null +++ b/src/OrderUsings.Core/Processing/UsingDirectiveOrSpace.cs @@ -0,0 +1,49 @@ +namespace OrderUsings.Processing +{ + using System; + + /// + /// Represents an entry in a list of using directives - either a directive or + /// a blank line. + /// + public struct UsingDirectiveOrSpace + { + private readonly UsingDirective _directive; + + /// + /// Initialises a representing + /// a using directive. (Use the no-arguments constructor to represent + /// a blank line.) + /// + /// The directive. + public UsingDirectiveOrSpace(UsingDirective directive) + { + _directive = directive; + } + + /// + /// Gets a value indicating whether this entry represents a blank line. + /// + public bool IsBlankLine + { + get { return _directive == null; } + } + + /// + /// Gets the directive represented by this entry. (Throws if this entry + /// represents a blank line.) + /// + public UsingDirective Directive + { + get + { + if (_directive == null) + { + throw new InvalidOperationException("Cannot use Directive property on a blank line"); + } + + return _directive; + } + } + } +} diff --git a/src/OrderUsings.Core/Properties/AssemblyInfo.cs b/src/OrderUsings.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a0d7cca --- /dev/null +++ b/src/OrderUsings.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("OrderUsings.Core")] +[assembly: AssemblyDescription("Non-environment-specific logic for Rules-based ordering for C# using directives")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Ian Griffiths")] +[assembly: AssemblyProduct("Order Usings")] +[assembly: AssemblyCopyright("Copyright © Ian Griffiths, 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] +[assembly: Guid("db0f7251-87d1-4d3f-a5b0-36da4a771c9d")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/OrderUsings.sln b/src/OrderUsings.sln new file mode 100644 index 0000000..76ce04c --- /dev/null +++ b/src/OrderUsings.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReSharper810", "ReSharper810\ReSharper810.csproj", "{D1291D67-AD57-4982-827B-0BEDD4B1C140}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resharper810.Tests", "Resharper810.Tests\Resharper810.Tests.csproj", "{372D6B66-6022-4656-AB50-4CBCADD09150}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1E2CF5ED-1599-4D40-A4BD-8F773A6D90DE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "data", "data", "{6D1823DC-EDB3-4423-88BF-0AB678000E68}" + ProjectSection(SolutionItems) = preProject + test\data\highlighting-order-01.cs = test\data\highlighting-order-01.cs + test\data\highlighting-order-01.cs.gold = test\data\highlighting-order-01.cs.gold + test\data\highlighting-spacing-01.cs = test\data\highlighting-spacing-01.cs + test\data\highlighting-spacing-01.cs.gold = test\data\highlighting-spacing-01.cs.gold + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderUsings.Core", "OrderUsings.Core\OrderUsings.Core.csproj", "{9875DA0A-DB09-47B2-80B5-80B08E430CEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderUsings.Core.Tests", "OrderUsings.Core.Tests\OrderUsings.Core.Tests.csproj", "{638CD37F-59CB-42AE-873D-5E09E9C32EAA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{00A0C928-006D-41E5-AF17-F018C79A9430}" + ProjectSection(SolutionItems) = preProject + .nuget\packages.config = .nuget\packages.config + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D1291D67-AD57-4982-827B-0BEDD4B1C140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1291D67-AD57-4982-827B-0BEDD4B1C140}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1291D67-AD57-4982-827B-0BEDD4B1C140}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1291D67-AD57-4982-827B-0BEDD4B1C140}.Release|Any CPU.Build.0 = Release|Any CPU + {372D6B66-6022-4656-AB50-4CBCADD09150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {372D6B66-6022-4656-AB50-4CBCADD09150}.Debug|Any CPU.Build.0 = Debug|Any CPU + {372D6B66-6022-4656-AB50-4CBCADD09150}.Release|Any CPU.ActiveCfg = Release|Any CPU + {372D6B66-6022-4656-AB50-4CBCADD09150}.Release|Any CPU.Build.0 = Release|Any CPU + {9875DA0A-DB09-47B2-80B5-80B08E430CEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9875DA0A-DB09-47B2-80B5-80B08E430CEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9875DA0A-DB09-47B2-80B5-80B08E430CEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9875DA0A-DB09-47B2-80B5-80B08E430CEF}.Release|Any CPU.Build.0 = Release|Any CPU + {638CD37F-59CB-42AE-873D-5E09E9C32EAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {638CD37F-59CB-42AE-873D-5E09E9C32EAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {638CD37F-59CB-42AE-873D-5E09E9C32EAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {638CD37F-59CB-42AE-873D-5E09E9C32EAA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6D1823DC-EDB3-4423-88BF-0AB678000E68} = {1E2CF5ED-1599-4D40-A4BD-8F773A6D90DE} + EndGlobalSection +EndGlobal diff --git a/src/OrderUsings.sln.DotSettings b/src/OrderUsings.sln.DotSettings new file mode 100644 index 0000000..6c1b61b --- /dev/null +++ b/src/OrderUsings.sln.DotSettings @@ -0,0 +1,22 @@ + + True + True + True + True + <?xml version="1.0" encoding="utf-8" ?> +<Groups + xmlns="http://schemas.interact-sw.co.uk/OrderUsings/2014" + > + <Group Priority="1" NamespacePattern="System*" /> + <Group Priority="1" NamespacePattern="Microsoft*" /> + <Space /> + <Group Priority="1" NamespacePattern="NUnit*" /> + <Space /> + <Group Priority="1" NamespacePattern="JetBrains*" /> + <Space /> + <Group Priority="9999" NamespacePattern="*" /> + <Space /> + <Group Priority="9999" NamespacePattern="*" AliasPattern="*" Type="Alias" /> + <Space /> + <Group Priority="1" NamespacePattern="OrderUsings*" /> +</Groups> \ No newline at end of file diff --git a/src/ReSharper810/CodeModel/ImportReader.cs b/src/ReSharper810/CodeModel/ImportReader.cs new file mode 100644 index 0000000..8e6ec0e --- /dev/null +++ b/src/ReSharper810/CodeModel/ImportReader.cs @@ -0,0 +1,61 @@ +namespace OrderUsings.ReSharper.CodeModel +{ + using System.Collections.Generic; + + using JetBrains.ReSharper.Psi.CSharp.Parsing; + using JetBrains.ReSharper.Psi.CSharp.Tree; + + using OrderUsings.Processing; + + /// + /// Converts from ReSharper's representation of an import list to our internal representation. + /// + internal static class ImportReader + { + /// + /// Returns a list of using directives and spacing from an element that can contain + /// a directive list (i.e., a file, or a namespace block). Returns null if the element + /// has no using directives. + /// + /// The file or namespace block. + /// Null if no using directives were present; a + /// list otherwise. + internal static List ReadImports(ICSharpTypeAndNamespaceHolderDeclaration holder) + { + List items = null; + foreach (IUsingDirective item in holder.Imports) + { + if (items == null) + { + items = new List(); + } + + var alias = item as IUsingAliasDirective; + items.Add(new UsingDirectiveOrSpace(new UsingDirective + { + Namespace = alias == null ? item.ImportedSymbolName.QualifiedName : alias.Alias.Name, + Alias = alias == null ? null : alias.AliasName + })); + + var syb = item.NextSibling; + bool first = true; + for (; syb != null && !(syb is IUsingDirective); syb = syb.NextSibling) + { + if (syb.NodeType == CSharpTokenType.NEW_LINE) + { + if (first) + { + first = false; + } + else + { + items.Add(new UsingDirectiveOrSpace()); + } + } + } + } + + return items; + } + } +} diff --git a/src/ReSharper810/Highlightings/BaseHighlighting.cs b/src/ReSharper810/Highlightings/BaseHighlighting.cs new file mode 100644 index 0000000..c8fd2e2 --- /dev/null +++ b/src/ReSharper810/Highlightings/BaseHighlighting.cs @@ -0,0 +1,76 @@ +namespace OrderUsings.ReSharper.Highlightings +{ + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Psi.CSharp.Tree; + + using OrderUsings.Configuration; + + /// + /// Common functionality for any highlighting for a using directive list. + /// + public abstract class BaseHighlighting : IHighlighting + { + private readonly ICSharpTypeAndNamespaceHolderDeclaration _typeAndNamespaceHolder; + private readonly OrderUsingsConfiguration _config; + + /// + /// Initializes a . + /// + /// The file or namespace block that contains + /// the import list being highlighted. + /// The configuration that was active when we determined that + /// the import list does not match the requirements. + internal BaseHighlighting( + ICSharpTypeAndNamespaceHolderDeclaration typeAndNamespaceHolder, OrderUsingsConfiguration config) + { + _config = config; + _typeAndNamespaceHolder = typeAndNamespaceHolder; + } + + /// + /// Gets file or namespace block that contains the import list being highlighted. + /// + public ICSharpTypeAndNamespaceHolderDeclaration TypeAndNamespaceHolder + { + get { return _typeAndNamespaceHolder; } + } + + /// + public string ToolTip + { + get { return ToolTipText; } + } + + /// + public string ErrorStripeToolTip + { + get { return ToolTipText; } + } + + /// + public int NavigationOffsetPatch + { + get { return 0; } + } + + /// + /// Gets configuration that was active when we determined that the import list + /// does not match the requirements. + /// + internal OrderUsingsConfiguration Config + { + get { return _config; } + } + + /// + /// Gets the text used as the main tooltip and the error stripe tooltip. + /// + protected abstract string ToolTipText { get; } + + /// + public bool IsValid() + { + return true; + } + } +} diff --git a/src/ReSharper810/Highlightings/UsingOrderHighlighting.cs b/src/ReSharper810/Highlightings/UsingOrderHighlighting.cs new file mode 100644 index 0000000..ed0a04d --- /dev/null +++ b/src/ReSharper810/Highlightings/UsingOrderHighlighting.cs @@ -0,0 +1,34 @@ +namespace OrderUsings.ReSharper.Highlightings +{ + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Psi.CSharp.Tree; + + using OrderUsings.Configuration; + + /// + /// A ReSharper highlighting indicating that a set of using directives don't meet the + /// configured ordering requirements. + /// + [StaticSeverityHighlighting(Severity.WARNING, "Using Directive Order & Spacing", Title = "Using directive order")] + public class UsingOrderHighlighting : BaseHighlighting + { + /// + /// Initializes a . + /// + /// The file or namespace block that contains + /// the import list being highlighted. + /// The configuration that was active when we determined that + /// the import list does not match the requirements. + internal UsingOrderHighlighting( + ICSharpTypeAndNamespaceHolderDeclaration typeAndNamespaceHolder, OrderUsingsConfiguration config) + : base(typeAndNamespaceHolder, config) + { + } + + /// + protected override string ToolTipText + { + get { return "Using directives do not match configured order"; } + } + } +} diff --git a/src/ReSharper810/Highlightings/UsingSpacingHighlighting.cs b/src/ReSharper810/Highlightings/UsingSpacingHighlighting.cs new file mode 100644 index 0000000..36eccba --- /dev/null +++ b/src/ReSharper810/Highlightings/UsingSpacingHighlighting.cs @@ -0,0 +1,36 @@ +namespace OrderUsings.ReSharper.Highlightings +{ + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Psi.CSharp.Tree; + + using OrderUsings.Configuration; + + /// + /// A ReSharper highlighting indicating that a set of using statements don't meet the + /// configured spacing requirements. + /// + [StaticSeverityHighlighting(ViolationSeverity, "Using Directive Order & Spacing", Title = "Using directive spacing")] + public class UsingSpacingHighlighting : BaseHighlighting + { + private const Severity ViolationSeverity = Severity.WARNING; + + /// + /// Initializes a . + /// + /// The file or namespace block that contains + /// the import list being highlighted. + /// The configuration that was active when we determined that + /// the import list does not match the requirements. + internal UsingSpacingHighlighting( + ICSharpTypeAndNamespaceHolderDeclaration typeAndNamespaceHolder, OrderUsingsConfiguration config) + : base(typeAndNamespaceHolder, config) + { + } + + /// + protected override string ToolTipText + { + get { return "Using directives do not match configured spacing"; } + } + } +} diff --git a/src/ReSharper810/Inspection/OrderUsingsDaemonStage.cs b/src/ReSharper810/Inspection/OrderUsingsDaemonStage.cs new file mode 100644 index 0000000..b558031 --- /dev/null +++ b/src/ReSharper810/Inspection/OrderUsingsDaemonStage.cs @@ -0,0 +1,82 @@ +namespace OrderUsings.ReSharper.Inspection +{ + using System; + using System.IO; + + using JetBrains.Application.Progress; + using JetBrains.Application.Settings; + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Daemon.CSharp.Stages; + using JetBrains.ReSharper.Psi.CSharp.Tree; + + using OrderUsings.Configuration; + using OrderUsings.ReSharper.Settings; + + /// + /// ReSharper entry point, enabling us to inspect source files in the background + /// as they are opened, and add highlights. + /// + [DaemonStage] + public class OrderUsingsDaemonStage : CSharpDaemonStageBase + { + /// + /// Invoked by ReSharper each time it wants us to perform some background processing + /// of a file. + /// + /// Provides information about and services relating to the + /// work we are being asked to do. + /// Settings information. + /// The kind of processing we're being asked to do. + /// The file to be processed. + /// A process object representing the work, or null if no work will be done. + protected override IDaemonStageProcess CreateProcess( + IDaemonProcess process, + IContextBoundSettingsStore settings, + DaemonProcessKind processKind, + ICSharpFile file) + { + if (process == null) + { + throw new ArgumentNullException("process"); + } + + // StyleCop's daemon stage looks for a processKind of DaemonProcessKind.OTHER + // and does nothing (returns null) if it sees it. This turns out to prevent + // highlights from showing up when you ask ReSharper to inspect code issues + // across the whole solution. I'm not sure why StyleCop deliberately opts out + // of it. Perhaps something goes horribly wrong, but I've not seen any sign + // of that yet, and we really do want solution-wide inspection to work. + + try + { + // I guess the base class checks that this is actually a C# file? + if (!IsSupported(process.SourceFile)) + { + return null; + } + + // StyleCop checks to see if there are already any errors in the file, and if + // there are, it decides to do nothing. + // TODO: Do we need to do that? + + // TODO: We should probably check for exemptions, e.g. generated source files. + } + catch (ProcessCancelledException) + { + return null; + } + + // TODO: should we get an injected ISettingsOptimization? + var orderUsingSettings = + settings.GetKey(SettingsOptimization.DoMeSlowly); + + OrderUsingsConfiguration config = null; + if (!string.IsNullOrWhiteSpace(orderUsingSettings.OrderSpecificationXml)) + { + config = ConfigurationSerializer.FromXml(new StringReader(orderUsingSettings.OrderSpecificationXml)); + } + + return new OrderUsingsDaemonStageProcess(process, file, config); + } + } +} diff --git a/src/ReSharper810/Inspection/OrderUsingsDaemonStageProcess.cs b/src/ReSharper810/Inspection/OrderUsingsDaemonStageProcess.cs new file mode 100644 index 0000000..a29533f --- /dev/null +++ b/src/ReSharper810/Inspection/OrderUsingsDaemonStageProcess.cs @@ -0,0 +1,150 @@ +namespace OrderUsings.ReSharper.Inspection +{ + using System; + using System.Collections.Generic; + + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Psi.CSharp.Tree; + using JetBrains.ReSharper.Psi.Tree; + + using OrderUsings.Configuration; + using OrderUsings.Processing; + using OrderUsings.ReSharper.CodeModel; + using OrderUsings.ReSharper.Highlightings; + + /// + /// Represents the processing for a particular file in our daemon stage. + /// + internal class OrderUsingsDaemonStageProcess : IDaemonStageProcess + { + private readonly ICSharpFile _file; + private readonly OrderUsingsConfiguration _config; + + /// + /// Initializes a . + /// + /// The process object supplied by R# for this work. + /// The file to process. + /// The order and spacing configuration to use. + public OrderUsingsDaemonStageProcess(IDaemonProcess process, ICSharpFile file, OrderUsingsConfiguration config) + { + _file = file; + _config = config; + DaemonProcess = process; + } + + /// + /// Gets the process object associated with this work. + /// + /// + /// The interface requires this. Quite why R# doesn't + /// already know the association is beyond me. + /// + public IDaemonProcess DaemonProcess { get; private set; } + + /// + /// Called by ReSharper when it wants us to execute our work. + /// + /// A call-back through which we supply the results + /// of our processing. + public void Execute(Action committer) + { + DaemonStageResult result = null; + if (_config != null) + { + List highlights = null; + + // Top-level imports (outside of any namespace blocks) are a singular special + // case; the other place that imports can be found is in namespace blocks, and + // since those can be nested, we have to walk them recursively. + CheckImports(_file, ref highlights); + WalkNamespaceDeclarations(_file.NamespaceDeclarations, ref highlights); + + if (highlights != null) + { + result = new DaemonStageResult(highlights); + } + } + + committer(result); + } + + /// + /// Recursively walk namespace declaration blocks, checking any import lists they + /// contain. + /// + /// The namespace declaration blocks + /// to check. + /// If any import lists are found that do not meet the + /// configured order and spacing requirements, they will be returned via this + /// argument. (To avoid unnecessary allocations in the happy path, we don't allocate + /// the list of highlights unless we need to generate at least one highlight, + /// which is why this is a ref parameter - it is initially null, + /// but gets allocated on demand if needed.) + private void WalkNamespaceDeclarations( + TreeNodeCollection namespaceDeclarationNodes, + ref List highlights) + { + foreach (var ns in namespaceDeclarationNodes) + { + CheckImports(ns, ref highlights); + WalkNamespaceDeclarations(ns.NamespaceDeclarations, ref highlights); + } + } + + /// + /// Checks an import list against the configured ordering and spacing. + /// + /// The import list container - either a file, or a namespace + /// declaration block. + /// If the import does not meet the configured requirements, + /// we allocate a list containing a highlight describing the problem and return + /// it via this argument. (We allocate the list on demand to avoid allocations + /// in the happy path in which all the import lists are correctly ordered and + /// spaced.) + private void CheckImports( + ICSharpTypeAndNamespaceHolderDeclaration holder, ref List highlights) + { + List items = ImportReader.ReadImports(holder); + List imports; + List> requiredOrderByGroups; + ImportInspector.FlattenImportsAndDetermineOrderAndSpacing( + _config, items, out imports, out requiredOrderByGroups); + + bool orderIsCorrect = true; + if (requiredOrderByGroups != null) + { + Relocation nextChange = ImportInspector.GetNextUsingToMove(requiredOrderByGroups, imports); + if (nextChange != null) + { + orderIsCorrect = false; + AddHighlight(holder, ref highlights, new UsingOrderHighlighting(holder, _config)); + } + } + + // If (and only if) the order is correct, we go on to check the spacing. + if (orderIsCorrect) + { + SpaceChange nextChange = ImportInspector.GetNextSpacingModification(requiredOrderByGroups, items); + if (nextChange != null) + { + AddHighlight(holder, ref highlights, new UsingSpacingHighlighting(holder, _config)); + } + } + } + + private void AddHighlight( + ICSharpTypeAndNamespaceHolderDeclaration holder, + ref List highlights, + IHighlighting highlight) + { + if (highlights == null) + { + highlights = new List(); + } + + highlights.Add(new HighlightingInfo( + holder.ImportsList.GetHighlightingRange(), highlight)); + } + } +} diff --git a/src/ReSharper810/Properties/AssemblyInfo.cs b/src/ReSharper810/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..631e047 --- /dev/null +++ b/src/ReSharper810/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +using JetBrains.Application.PluginSupport; + +[assembly: AssemblyTitle("OrderUsings.ReSharper810")] +[assembly: AssemblyDescription("Rules-based ordering for C# using directives")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Ian Griffiths")] +[assembly: AssemblyProduct("Order Usings")] +[assembly: AssemblyCopyright("Copyright © Ian Griffiths, 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +// The following information is displayed by ReSharper in the Plugins dialog +[assembly: PluginTitle("Order Usings")] +[assembly: PluginDescription("Rules-based ordering for C# using directives")] +[assembly: PluginVendor("Ian Griffiths")] + +[assembly: InternalsVisibleTo("ReSharper810.Tests")] \ No newline at end of file diff --git a/src/ReSharper810/QuickFixes/UsingOrderAndSpacingQuickFix.cs b/src/ReSharper810/QuickFixes/UsingOrderAndSpacingQuickFix.cs new file mode 100644 index 0000000..96df298 --- /dev/null +++ b/src/ReSharper810/QuickFixes/UsingOrderAndSpacingQuickFix.cs @@ -0,0 +1,189 @@ +namespace OrderUsings.ReSharper.QuickFixes +{ + using System; + using System.Collections.Generic; + + using JetBrains.Application; + using JetBrains.Application.Progress; + using JetBrains.DocumentModel.Transactions; + using JetBrains.ProjectModel; + using JetBrains.ReSharper.Feature.Services.Bulbs; + using JetBrains.ReSharper.Intentions.Extensibility; + using JetBrains.ReSharper.Psi; + using JetBrains.ReSharper.Psi.CSharp.Parsing; + using JetBrains.ReSharper.Psi.CSharp.Tree; + using JetBrains.ReSharper.Psi.ExtensionsAPI; + using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree; + using JetBrains.Text; + using JetBrains.TextControl; + using JetBrains.Util; + + using OrderUsings.Processing; + using OrderUsings.ReSharper.CodeModel; + using OrderUsings.ReSharper.Highlightings; + using OrderUsings.ReSharper.Inspection; + + /// + /// ReSharper quick fix that fixes the ordering and spacing issues detected during inspection by + /// . + /// + /// + /// ReSharper constructs this automatically - when it wants to know if any quick fixes are + /// available for a highlighting, it goes looking for classes annotated with + /// that implement (directly or, as in + /// this case, indirectly), and which have a constructor accepting the relevant highlighting type. + /// + [QuickFix] + public class UsingOrderAndSpacingQuickFix : QuickFixBase + { + private readonly BaseHighlighting _highlighting; + + /// + /// Initialises a for an order + /// mismatch highlighting. + /// + /// The highlighting for which this will be + /// a quick fix. + public UsingOrderAndSpacingQuickFix(UsingOrderHighlighting highlighting) + { + _highlighting = highlighting; + } + + /// + /// Initialises a for a spacing + /// mismatch highlighting. + /// + /// The highlighting for which this will be + /// a quick fix. + public UsingOrderAndSpacingQuickFix(UsingSpacingHighlighting highlighting) + { + _highlighting = highlighting; + } + + /// + public override string Text + { + get { return "Fix ordering and spacing"; } + } + + /// + public override bool IsAvailable(IUserDataHolder cache) + { + return true; + } + + /// + protected override Action ExecutePsiTransaction(ISolution solution, IProgressIndicator progress) + { + return textControl => + { + using (solution.GetComponent() + .CreateTransactionCookie(DefaultAction.Commit, "action name")) + { + var services = solution.GetPsiServices(); + services.Transactions.Execute( + "Code cleanup", + () => services.Locks.ExecuteWithWriteLock(() => + { + ICSharpTypeAndNamespaceHolderDeclaration holder = _highlighting.TypeAndNamespaceHolder; + + FixOrder(holder); + FixSpacing(holder); + })); + } + }; + } + + private void FixOrder(ICSharpTypeAndNamespaceHolderDeclaration holder) + { + // The reordering proceeds one item at a time, so we just keep reapplying it + // until there's nothing left to do. + // To avoid hanging VS in the event that an error in the logic causes the + // sequence of modifications not to terminate, we ensure we don't try to + // apply more changes than there are using directives. + int tries = 0; + int itemCount = 0; + while (tries == 0 || tries <= itemCount) + { + List items = ImportReader.ReadImports(holder); + List imports; + List> requiredOrderByGroups; + ImportInspector.FlattenImportsAndDetermineOrderAndSpacing( + _highlighting.Config, items, out imports, out requiredOrderByGroups); + + if (requiredOrderByGroups != null) + { + itemCount = imports.Count; + Relocation nextChange = ImportInspector.GetNextUsingToMove(requiredOrderByGroups, imports); + if (nextChange != null) + { + IUsingDirective toMove = holder.Imports[nextChange.From]; + IUsingDirective before = holder.Imports[nextChange.To]; + holder.RemoveImport(toMove); + holder.AddImportBefore(toMove, before); + tries += 1; + } + else + { + break; + } + } + } + } + + private void FixSpacing(ICSharpTypeAndNamespaceHolderDeclaration holder) + { + // The reordering proceeds one item at a time, so we just keep reapplying it + // until there's nothing left to do. + // To avoid hanging VS in the event that an error in the logic causes the + // sequence of modifications not to terminate, we ensure we don't try to + // apply more changes than there are either using directives or blank + // lines in the usings list. + int tries = 0; + int itemCount = 0; + while (tries == 0 || tries <= itemCount) + { + List items = ImportReader.ReadImports(holder); + itemCount = items.Count; + List imports; + List> requiredOrderByGroups; + ImportInspector.FlattenImportsAndDetermineOrderAndSpacing( + _highlighting.Config, items, out imports, out requiredOrderByGroups); + + SpaceChange nextChange = ImportInspector.GetNextSpacingModification(requiredOrderByGroups, items); + if (nextChange != null) + { + IUsingDirective usingBeforeSpace = holder.Imports[nextChange.Index - 1]; + if (nextChange.ShouldInsert) + { + using (WriteLockCookie.Create()) + { + var newLineText = new StringBuffer("\r\n"); + + LeafElementBase newLine = TreeElementFactory.CreateLeafElement( + CSharpTokenType.NEW_LINE, newLineText, 0, newLineText.Length); + LowLevelModificationUtil.AddChildAfter(usingBeforeSpace, newLine); + } + } + else + { + var syb = usingBeforeSpace.NextSibling; + for (; syb != null && !(syb is IUsingDirective); syb = syb.NextSibling) + { + if (syb.NodeType == CSharpTokenType.NEW_LINE) + { + LowLevelModificationUtil.DeleteChild(syb); + } + } + } + } + else + { + break; + } + + tries += 1; + } + } + } +} diff --git a/src/ReSharper810/ReSharper810.csproj b/src/ReSharper810/ReSharper810.csproj new file mode 100644 index 0000000..9912181 --- /dev/null +++ b/src/ReSharper810/ReSharper810.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {D1291D67-AD57-4982-827B-0BEDD4B1C140} + Library + Properties + OrderUsings.ReSharper + OrderUsings.ReSharper810 + v4.0 + 512 + + + + true + full + false + bin\Debug\ + JET_MODE_ASSERT;DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Program + $(VsInstallDir)devenv.exe + /ReSharper.Plugin $(AssemblyName).dll /ReSharper.Internal + $(MSBuildProjectDirectory)\$(OutputPath) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + {9875da0a-db09-47b2-80b5-80b08e430cef} + OrderUsings.Core + + + + + \ No newline at end of file diff --git a/src/ReSharper810/Settings/DefaultConfiguration.xml b/src/ReSharper810/Settings/DefaultConfiguration.xml new file mode 100644 index 0000000..4b5c23b --- /dev/null +++ b/src/ReSharper810/Settings/DefaultConfiguration.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/ReSharper810/Settings/OrderUsingsOptionsPage.cs b/src/ReSharper810/Settings/OrderUsingsOptionsPage.cs new file mode 100644 index 0000000..04f85ce --- /dev/null +++ b/src/ReSharper810/Settings/OrderUsingsOptionsPage.cs @@ -0,0 +1,114 @@ +namespace OrderUsings.ReSharper.Settings +{ + using System; + using System.IO; + using System.Windows; + using System.Windows.Controls; + + using JetBrains.Annotations; + using JetBrains.Application.Settings; + using JetBrains.DataFlow; + using JetBrains.ReSharper.Features.Common.Options; + using JetBrains.UI.CrossFramework; + using JetBrains.UI.Options; + + /// + /// UI for configuring the order and spacing of usings within ReSharper's + /// settings dialog. + /// + [OptionsPage(PageId, "Order Usings", null, ParentId = ToolsPage.PID)] + public class OrderUsingsOptionsPage : IOptionsPage + { + private const string PageId = "OrderUsingsOptionsId"; + + /// + /// Initializes a . + /// + /// Passed by ReSharper. Purpose unclear to me. + /// Passed by ReSharper, enabling us to get the + /// current configuration, and bind controls to the configuration system. + public OrderUsingsOptionsPage([NotNull] Lifetime lifetime, OptionsSettingsSmartContext settings) + { + if (lifetime == null) throw new ArgumentNullException("lifetime"); + + Control = InitView(lifetime, settings); + } + + /// + public EitherControl Control { get; private set; } + + /// + public string Id { get { return PageId; } } + + /// + public bool OnOk() + { + return true; + } + + /// + public bool ValidatePage() + { + return true; + } + + private EitherControl InitView(Lifetime lifetime, OptionsSettingsSmartContext settings) + { + var grid = new Grid { Background = SystemColors.ControlBrush }; + + grid.ColumnDefinitions.Add(new ColumnDefinition()); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + grid.RowDefinitions.Add(new RowDefinition()); + + var margin = new Thickness(3); + var label = new Label { Content = "Configuration:", Margin = margin }; + var text = new TextBox { AcceptsReturn = true, Margin = margin }; + settings.SetBinding( + lifetime, + x => x.OrderSpecificationXml, + text, + TextBox.TextProperty); + + Grid.SetRow(text, 1); + Grid.SetColumnSpan(text, 3); + + var buttonPadding = new Thickness(3); + var resetButton = new Button + { + Content = "Reset", + Margin = margin, + Padding = buttonPadding + }; + resetButton.Click += (s, e) => text.SetCurrentValue(TextBox.TextProperty, string.Empty); + Grid.SetColumn(resetButton, 1); + + var addBasicButton = new Button + { + Content = "Create basic configuration", + Margin = margin, + Padding = buttonPadding + }; + addBasicButton.Click += (s, e) => + { + var asm = typeof(OrderUsingsOptionsPage).Assembly; + Stream stream = asm.GetManifestResourceStream("OrderUsings.ReSharper.Settings.DefaultConfiguration.xml"); + if (stream != null) + { + using (var reader = new StreamReader(stream)) + { + text.SetCurrentValue(TextBox.TextProperty, reader.ReadToEnd()); + } + } + }; + Grid.SetColumn(addBasicButton, 2); + + grid.Children.Add(label); + grid.Children.Add(text); + grid.Children.Add(resetButton); + grid.Children.Add(addBasicButton); + return grid; + } + } +} diff --git a/src/ReSharper810/Settings/OrderUsingsSettings.cs b/src/ReSharper810/Settings/OrderUsingsSettings.cs new file mode 100644 index 0000000..fad011f --- /dev/null +++ b/src/ReSharper810/Settings/OrderUsingsSettings.cs @@ -0,0 +1,27 @@ +namespace OrderUsings.ReSharper.Settings +{ + using System.Reflection; + + using JetBrains.Application.Settings; + + /// + /// Plug-in settings persisted for us by ReSharper. + /// + /// + /// TODO: what are we supposed to use as the 'parent' in SettingsKey? + /// The docs basically give you no help at all here - there doesn't seem to be a + /// list of suitable types. The example uses InternetSettings with no indication as + /// to why you might choose that. I'm going with Missing for now, because some + /// examples already out there do that too, but it just means you end up as an + /// uncategorised top-level setting, which is not ideal. + /// + [SettingsKey(typeof(Missing), "GitHub settings")] + public class OrderUsingsSettings + { + /// + /// Gets or sets the XML content specifying the required order. + /// + [SettingsEntry("", "XML file specifying the required order for using directives")] + public string OrderSpecificationXml { get; set; } + } +} diff --git a/src/ReSharper810/packages.config b/src/ReSharper810/packages.config new file mode 100644 index 0000000..d631330 --- /dev/null +++ b/src/ReSharper810/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Resharper810.Tests/Highlighting/WhenOrderIsWrong.cs b/src/Resharper810.Tests/Highlighting/WhenOrderIsWrong.cs new file mode 100644 index 0000000..4c40cc8 --- /dev/null +++ b/src/Resharper810.Tests/Highlighting/WhenOrderIsWrong.cs @@ -0,0 +1,64 @@ +namespace Resharper810.Tests.Highlighting +{ + using System; + + using NUnit.Framework; + + using JetBrains.Application.Settings; + using JetBrains.ProjectModel; + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Daemon.CSharp; + using JetBrains.ReSharper.TestFramework; + + using OrderUsings.ReSharper; + using OrderUsings.ReSharper.Highlightings; + using OrderUsings.ReSharper.Settings; + + [TestFixture] + [TestSettingsKey(typeof(OrderUsingsSettings))] + public class WhenOrderIsWrong : CSharpHighlightingTestBase + { + protected override bool HighlightingPredicate(IHighlighting highlighting, IContextBoundSettingsStore settingsstore) + { + return highlighting is UsingOrderHighlighting; + } + + // This seems to be the earliest place from which we can get a settings store. We use + // this to push in settings without having to use a real settings file. (It looks like + // you can actually provide a test-local settings file, but for now, just providing it + // programmatically is easiest.) + protected override void WithProject(IProject project, ISettingsStore settingsStore, Action action) + { + // The docs all say to use plain BindToContext, but that has been marked as [Obsolete]. + // This appears to be what that obsolete method actually does. (And teh DataContexts.Empty + // just copies what the test code uses when it creates a bound settings store to pass to the + // code under test.) + IContextBoundSettingsStore boundStore = settingsStore.BindToContextTransient( + ContextRange.ManuallyRestrictWritesToOneContext( + (lifetime, contexts) => settingsStore.DataContexts.Empty)); + boundStore.SetValue( + settings => settings.OrderSpecificationXml, + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""); + base.WithProject(project, settingsStore, action); + } + + protected override string RelativeTestDataPath + { + get { return @""; } + } + + [Test] + [TestCase("highlighting-order-01.cs")] + public void Test(string testName) + { + DoTestFiles(testName); + } + } +} diff --git a/src/Resharper810.Tests/Highlighting/WhenSpacingIsWrong.cs b/src/Resharper810.Tests/Highlighting/WhenSpacingIsWrong.cs new file mode 100644 index 0000000..b9e6204 --- /dev/null +++ b/src/Resharper810.Tests/Highlighting/WhenSpacingIsWrong.cs @@ -0,0 +1,64 @@ +namespace Resharper810.Tests.Highlighting +{ + using System; + + using NUnit.Framework; + + using JetBrains.Application.Settings; + using JetBrains.ProjectModel; + using JetBrains.ReSharper.Daemon; + using JetBrains.ReSharper.Daemon.CSharp; + using JetBrains.ReSharper.TestFramework; + + using OrderUsings.ReSharper; + using OrderUsings.ReSharper.Highlightings; + using OrderUsings.ReSharper.Settings; + + [TestFixture] + [TestSettingsKey(typeof(OrderUsingsSettings))] + public class WhenSpacingIsWrong : CSharpHighlightingTestBase + { + protected override bool HighlightingPredicate(IHighlighting highlighting, IContextBoundSettingsStore settingsstore) + { + return highlighting is UsingSpacingHighlighting; + } + + // This seems to be the earliest place from which we can get a settings store. We use + // this to push in settings without having to use a real settings file. (It looks like + // you can actually provide a test-local settings file, but for now, just providing it + // programmatically is easiest.) + protected override void WithProject(IProject project, ISettingsStore settingsStore, Action action) + { + // The docs all say to use plain BindToContext, but that has been marked as [Obsolete]. + // This appears to be what that obsolete method actually does. (And teh DataContexts.Empty + // just copies what the test code uses when it creates a bound settings store to pass to the + // code under test.) + IContextBoundSettingsStore boundStore = settingsStore.BindToContextTransient( + ContextRange.ManuallyRestrictWritesToOneContext( + (lifetime, contexts) => settingsStore.DataContexts.Empty)); + boundStore.SetValue( + settings => settings.OrderSpecificationXml, + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""); + base.WithProject(project, settingsStore, action); + } + + protected override string RelativeTestDataPath + { + get { return @""; } + } + + [Test] + [TestCase("highlighting-spacing-01.cs")] + public void Test(string testName) + { + DoTestFiles(testName); + } + } +} diff --git a/src/Resharper810.Tests/Properties/AssemblyInfo.cs b/src/Resharper810.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e5a7d93 --- /dev/null +++ b/src/Resharper810.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Reflection; + +using NUnit.Framework; + +using JetBrains.Application; +using JetBrains.Threading; + +using OrderUsings.Configuration; +using OrderUsings.ReSharper; +using OrderUsings.ReSharper.Settings; + +/// +/// Test environment. Must be in the global namespace. +/// +[SetUpFixture] +// ReSharper disable once CheckNamespace +public class TestEnvironmentAssembly : ReSharperTestEnvironmentAssembly +{ + /// + /// Gets the assemblies to load into test environment. + /// Should include all assemblies which contain components. + /// + /// + /// The assemblies to load. + /// + private static IEnumerable GetAssembliesToLoad() + { + // Test assembly + yield return Assembly.GetExecutingAssembly(); + + yield return typeof(GroupRule).Assembly; + yield return typeof(OrderUsingsSettings).Assembly; + } + + public override void SetUp() + { + base.SetUp(); + ReentrancyGuard.Current.Execute( + "LoadAssemblies", + () => Shell.Instance.GetComponent().LoadAssemblies( + GetType().Name, GetAssembliesToLoad())); + } + + public override void TearDown() + { + ReentrancyGuard.Current.Execute( + "UnloadAssemblies", + () => Shell.Instance.GetComponent().UnloadAssemblies( + GetType().Name, GetAssembliesToLoad())); + base.TearDown(); + } +} diff --git a/src/Resharper810.Tests/Resharper810.Tests.csproj b/src/Resharper810.Tests/Resharper810.Tests.csproj new file mode 100644 index 0000000..5b6a537 --- /dev/null +++ b/src/Resharper810.Tests/Resharper810.Tests.csproj @@ -0,0 +1,75 @@ + + + + + + Tests + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {372D6B66-6022-4656-AB50-4CBCADD09150} + Library + Properties + Resharper810.Tests + Resharper810.Tests + v4.0 + 512 + + + + true + full + false + bin\Debug\ + JET_MODE_ASSERT;DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + {9875da0a-db09-47b2-80b5-80b08e430cef} + OrderUsings.Core + + + {d1291d67-ad57-4982-827b-0bedd4b1c140} + ReSharper810 + + + + + + + + + \ No newline at end of file diff --git a/src/Resharper810.Tests/Settings.StyleCop b/src/Resharper810.Tests/Settings.StyleCop new file mode 100644 index 0000000..ca702f3 --- /dev/null +++ b/src/Resharper810.Tests/Settings.StyleCop @@ -0,0 +1,39 @@ + + + + + + + False + + + + + + + + + + False + + + + + False + + + + + + + + + + False + + + + + + + \ No newline at end of file diff --git a/src/Resharper810.Tests/packages.config b/src/Resharper810.Tests/packages.config new file mode 100644 index 0000000..4017bc3 --- /dev/null +++ b/src/Resharper810.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Settings.StyleCop b/src/Settings.StyleCop new file mode 100644 index 0000000..c2235fd --- /dev/null +++ b/src/Settings.StyleCop @@ -0,0 +1,169 @@ + + + en-GB + + + + + + + False + + + + + + + + + + False + + + + + False + + + + + True + + + + + + + False + + + + + False + + + + + + + + + + False + + + + + + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + + + + + + False + + + + + + + \ No newline at end of file diff --git a/src/test/data/highlighting-order-01.cs b/src/test/data/highlighting-order-01.cs new file mode 100644 index 0000000..a9cc675 --- /dev/null +++ b/src/test/data/highlighting-order-01.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System; + +namespace Test +{ + public class Foo + { + public virtual bool Bar(List data) + { + return data.Count > 0; + } + } +} \ No newline at end of file diff --git a/src/test/data/highlighting-order-01.cs.gold b/src/test/data/highlighting-order-01.cs.gold new file mode 100644 index 0000000..faf9890 --- /dev/null +++ b/src/test/data/highlighting-order-01.cs.gold @@ -0,0 +1,15 @@ +|using System.Collections.Generic; +using System;|(0) + +namespace Test +{ + public class Foo + { + public virtual bool Bar(List data) + { + return data.Count > 0; + } + } +} +--------------------------------------------------------- +(0): ReSharper Warning: Using directives do not match configured order diff --git a/src/test/data/highlighting-spacing-01.cs b/src/test/data/highlighting-spacing-01.cs new file mode 100644 index 0000000..ee02455 --- /dev/null +++ b/src/test/data/highlighting-spacing-01.cs @@ -0,0 +1,14 @@ +using System; + +using System.Collections.Generic; + +namespace Test +{ + public class Foo + { + public virtual bool Bar(List data) + { + return data.Count > 0; + } + } +} \ No newline at end of file diff --git a/src/test/data/highlighting-spacing-01.cs.gold b/src/test/data/highlighting-spacing-01.cs.gold new file mode 100644 index 0000000..b2e7902 --- /dev/null +++ b/src/test/data/highlighting-spacing-01.cs.gold @@ -0,0 +1,16 @@ +|using System; + +using System.Collections.Generic;|(0) + +namespace Test +{ + public class Foo + { + public virtual bool Bar(List data) + { + return data.Count > 0; + } + } +} +--------------------------------------------------------- +(0): ReSharper Warning: Using directives do not match configured spacing