diff --git a/CodeMaid.UnitTests/CachedSettingSetTests.cs b/CodeMaid.UnitTests/CachedSettingSetTests.cs index b99d5fd5..b3e48720 100644 --- a/CodeMaid.UnitTests/CachedSettingSetTests.cs +++ b/CodeMaid.UnitTests/CachedSettingSetTests.cs @@ -1,4 +1,7 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestTools.UnitTesting; using SteveCadwallader.CodeMaid.Helpers; using SteveCadwallader.CodeMaid.Properties; using System; diff --git a/CodeMaid.UnitTests/Cleanup/AddPaddingTests.cs b/CodeMaid.UnitTests/Cleanup/AddPaddingTests.cs new file mode 100644 index 00000000..d702c459 --- /dev/null +++ b/CodeMaid.UnitTests/Cleanup/AddPaddingTests.cs @@ -0,0 +1,414 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SteveCadwallader.CodeMaid.Properties; +using System.Threading.Tasks; + +namespace SteveCadwallader.CodeMaid.UnitTests.Cleanup +{ + // TODO: Add setup/teradown to set codemaid settings. + [TestClass] + public class AddPaddingTests + { + private readonly TestWorkspace testWorkspace; + + public AddPaddingTests() + { + testWorkspace = new TestWorkspace(); + } + + [TestMethod] + public async Task ShouldPadClassesAsync() + { + var source = +@" +internal class MyClass +{ +} +internal class MyClass2 +{ +} +"; + + var expected = +@" +internal class MyClass +{ +} + +internal class MyClass2 +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadPropertiesAsync() + { + var source = +@" +internal class Temp +{ + public int MyProperty { get; set; } + private void Do() + { + } +} +"; + + var expected = +@" +internal class Temp +{ + public int MyProperty { get; set; } + + private void Do() + { + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadMixedTypeDeclaAsync() + { + var source = +@" +internal struct Struct +{ +} +internal enum MyEnum +{ + Some = 0, + None = 1, +} +"; + + var expected = +@" +internal struct Struct +{ +} + +internal enum MyEnum +{ + Some = 0, + None = 1, +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadSwitchCaseAsync() + { + var source = +@" +public class Class +{ + private void Do() + { + int number = 1; + + switch (number) + { + case 0: + Console.WriteLine(""The number is zero""); + break; + + case 1: + Console.WriteLine(""The number is one""); + break; + + case 2: + Console.WriteLine(""The number is two""); + break; + + default: + Console.WriteLine(""The number is not zero, one, or two""); + break; + } + } +} +"; + + var expected = +@" +public class Class +{ + private void Do() + { + int number = 1; + + switch (number) + { + case 0: + Console.WriteLine(""The number is zero""); + break; + + case 1: + Console.WriteLine(""The number is one""); + break; + + case 2: + Console.WriteLine(""The number is two""); + break; + + default: + Console.WriteLine(""The number is not zero, one, or two""); + break; + } + } +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadTypeAsync() + { + var source = +@" +class Class where T : struct +{ +} +"; + + var expected = +@" +internal class Class where T : struct +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldBetweenMultLineAccessorAsync() + { + Settings.Default.Cleaning_InsertBlankLinePaddingBetweenPropertiesMultiLineAccessors = true; + Assert.IsTrue(Settings.Default.Cleaning_InsertBlankLinePaddingBetweenPropertiesMultiLineAccessors); + + var source = +@" +class Class +{ + public string FullName + { + get + { + return _fullName; + } + set => _fullName = value; + } + + public string FirstName + { + get => _firstName; + set => _firstName = value; + } +} +"; + + var expected = +@" +internal class Class +{ + public string FullName + { + get + { + return _fullName; + } + + set => _fullName = value; + } + + public string FirstName + { + get => _firstName; + set => _firstName = value; + } +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadCommentsAsync() + { + var source = +@" +internal + // + class Temp +{ + private void Do() + { + } + // Single + private void Foo() + { + // Should not pad + var a = 10; + } +} +"; + + var expected = +@" +internal + + // + class Temp +{ + private void Do() + { + } + + // Single + private void Foo() + { + // Should not pad + var a = 10; + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadRegionAsync() + { + var source = +@" +#region FirstReg +public class Attr : Attribute +{ + public void Do() { } + #endregion FirstReg + #region SecondReg + public Attr(int i) + { + } + #endregion SecondReg +} +"; + + var expected = +@" +#region FirstReg + +public class Attr : Attribute +{ + public void Do() { } + + #endregion FirstReg + + #region SecondReg + + public Attr(int i) + { + } + + #endregion SecondReg +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadRegionWithBracesAsync() + { + var source = +@" +public class Attrib : Attribute +{ + #region FirstReg + public void Do() + { + #endregion FirstReg + } + + public void Foo() + { + #region SecondReg + } + #endregion SecondReg +} +"; + + var expected = +@" +public class Attrib : Attribute +{ + #region FirstReg + + public void Do() + { + #endregion FirstReg + } + + public void Foo() + { + #region SecondReg + } + + #endregion SecondReg +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadMixedTypeDeclaAndCommentsAsync() + { + var source = +@" +internal struct MyStruct +{ +} +/// +/// +/// +internal struct Struct +{ +} +// Should Pad +internal enum MyEnum +{ + Some = 0, + None = 1, +} +"; + + var expected = +@" +internal struct MyStruct +{ +} + +/// +/// +/// +internal struct Struct +{ +} + +// Should Pad +internal enum MyEnum +{ + Some = 0, + None = 1, +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + } +} \ No newline at end of file diff --git a/CodeMaid.UnitTests/Cleanup/InsertModifiersTests.cs b/CodeMaid.UnitTests/Cleanup/InsertModifiersTests.cs new file mode 100644 index 00000000..63ff1c9d --- /dev/null +++ b/CodeMaid.UnitTests/Cleanup/InsertModifiersTests.cs @@ -0,0 +1,600 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SteveCadwallader.CodeMaid.Properties; +using System.Threading.Tasks; + +namespace SteveCadwallader.CodeMaid.UnitTests.Cleanup +{ + [TestClass] + public class InsertModifiersTests + { + private readonly TestWorkspace testWorkspace; + + public InsertModifiersTests() + { + testWorkspace = new TestWorkspace(); + } + + [TestMethod] + public async Task ShouldAddClassAccessorAsync() + { + var source = +@" +class MyClass +{ +} +"; + + var expected = +@" +internal class MyClass +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldPadClassAccessorAsync() + { + var source = +@" +internal class MyClass +{ +} +internal class MyClass2 +{ +} +"; + + var expected = +@" +internal class MyClass +{ +} + +internal class MyClass2 +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + + [TestMethod] + public async Task ShouldAddSamePartialClassAccessorsAsync() + { + var source = +@" +public partial class Temp +{ +} + +partial class Temp +{ +} +"; + + var expected = +@" +public partial class Temp +{ +} + +public partial class Temp +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddNestedClassAccessorAsync() + { + var source = +@" +class Temp +{ + int MyProperty { get; set; } +} + +public class Outer +{ + class Temp + { + int MyProperty { get; set; } + } +} +"; + + var expected = +@" +internal class Temp +{ + private int MyProperty { get; set; } +} + +public class Outer +{ + private class Temp + { + private int MyProperty { get; set; } + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddStructAccessorAsync() + { + var source = +@" +struct MyStruct +{ +} +"; + + var expected = +@" +internal struct MyStruct +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddRefStructAccessorAsync() + { + var source = +@" +ref struct MyStruct +{ +} + +readonly ref struct MyReadonlyStruct +{ +} +"; + + var expected = +@" +internal ref struct MyStruct +{ +} + +internal readonly ref struct MyReadonlyStruct +{ +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddPropertyAccessorAsync() + { + var source = +@" +class Sample +{ + int Prop { get; set; } +} +"; + + var expected = +@" +internal class Sample +{ + private int Prop { get; set; } +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldNotRemoveRequiredPropertyAsync() + { + var source = +@" +class Sample +{ + required int Prop { get; set; } +} +"; + + var expected = +@" +internal class Sample +{ + private required int Prop { get; set; } +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddMethodsAccessorAsync() + { + var source = +@" +class ExampleClass +{ + void Do() + { + } +} +"; + + var expected = +@" +internal class ExampleClass +{ + private void Do() + { + } +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldNotAddPartialMethodAccessorAsync() + { + var source = +@" +public partial class ExampleClass +{ + partial void Do() + { + } +} +"; + + var expected = +@" +public partial class ExampleClass +{ + partial void Do() + { + } +} +"; + + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddDefaultAbstractVirtualAccessorsAsync() + { + var source = +@" +abstract class MyAbstract +{ + virtual void VirtualMethod() + { + } + + abstract void AbstractMethod(); +} +"; + + var expected = +@" +internal abstract class MyAbstract +{ + public virtual void VirtualMethod() + { + } + + protected abstract void AbstractMethod(); +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task TestInheritsAbstractAsync() + { + var source = +@" +abstract class MyAbstract +{ + private protected abstract void AbstractMethod(); +} + +class Derive : MyAbstract +{ + override void AbstractMethod() + { + } +} +"; + + var expected = +@" +internal abstract class MyAbstract +{ + private protected abstract void AbstractMethod(); +} + +internal class Derive : MyAbstract +{ + private protected override void AbstractMethod() + { + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task TestShouldNotRemoveFileAsync() + { + var source = +@" +file class MyFile +{ + int Prop { get; set; } +} + +file struct MyFileStruct +{ + int Prop { get; set; } +} +"; + + var expected = +@" +file class MyFile +{ + private int Prop { get; set; } +} + +file struct MyFileStruct +{ + private int Prop { get; set; } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddDelegateAccessorAsync() + { + var source = +@" +class MyDelegate +{ + delegate int PerformCalculation(int x, int y); +} +"; + + var expected = +@" +internal class MyDelegate +{ + private delegate int PerformCalculation(int x, int y); +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddEventAccessorAsync() + { + var source = +@" +class MyEvent +{ + event MyEventHandler MyEvent; +} +"; + + var expected = +@" +internal class MyEvent +{ + private event MyEventHandler MyEvent; +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddEnumAccessorAsync() + { + var source = +@" +enum MyEnum +{ + Some = 0, + None = 1, +} +"; + + var expected = +@" +internal enum MyEnum +{ + Some = 0, + None = 1, +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddNestedEnumAccessorAsync() + { + var source = +@" +class MyClass +{ + enum MyEnum + { + Some = 0, + None = 1, + } +} +"; + + var expected = +@" +internal class MyClass +{ + private enum MyEnum + { + Some = 0, + None = 1, + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddInterfaceAccessorAsync() + { + var source = +@" +interface IMyInterface +{ + int MyProp { get; set; } + + void Do(); +} +"; + + var expected = +@" +internal interface IMyInterface +{ + int MyProp { get; set; } + + void Do(); +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddNestedInterfaceAccessorAsync() + { + var source = +@" +class MyClass +{ + interface IMyInterface + { + int MyProp { get; set; } + + void Do(); + } +} +"; + + var expected = +@" +internal class MyClass +{ + private interface IMyInterface + { + int MyProp { get; set; } + + void Do(); + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldAddFieldAccessorAsync() + { + var source = +@" +class MyClass +{ + int _number; +} +"; + + var expected = +@" +internal class MyClass +{ + private int _number; +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldNotChangeInvalidSyntaxAsync() + { + var source = +@" +class ITemp +{ + void Do() + { + int MyProperty { get; set; } + } +} +"; + + var expected = +@" +internal class ITemp +{ + private void Do() + { + int MyProperty { get; set; } + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + + [TestMethod] + public async Task ShouldNotChangeInterfaceDescendantsAsync() + { + var source = +@" +interface IInterface +{ + class C + { + class D + { + + } + } +} +"; + + var expected = +@" +internal interface IInterface +{ + class C + { + class D + { + + } + } +} +"; + await testWorkspace.VerifyCleanupAsync(source, expected); + } + } +} \ No newline at end of file diff --git a/CodeMaid.UnitTests/Cleanup/TestWorkspace.cs b/CodeMaid.UnitTests/Cleanup/TestWorkspace.cs new file mode 100644 index 00000000..ef1d0f70 --- /dev/null +++ b/CodeMaid.UnitTests/Cleanup/TestWorkspace.cs @@ -0,0 +1,70 @@ +using CodeMaidShared.Logic.Cleaning; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NUnit.Framework; +using Shouldly; +using System; +using System.Threading.Tasks; +using StringAssert = NUnit.Framework.StringAssert; + +namespace SteveCadwallader.CodeMaid.UnitTests.Cleanup +{ + public class TestWorkspace + { + public async Task VerifyCleanupAsync(string input, string expected) + { + var document = SetDocument(input); + + var syntaxTree = await Document.GetSyntaxRootAsync(); + var semanticModel = await Document.GetSemanticModelAsync(); + + var rewriter = new RoslynCleaner(); + InsertExplicitAccessorMiddleware.Initialize(rewriter, semanticModel); + InsertNodePaddingMiddleware.Initialize(rewriter); + + InsertTokenPaddingMiddleware.Initialize(rewriter); + + var result = rewriter.Process(syntaxTree, Workspace); + var resultString = result.ToFullString(); + + //To support cross platform line endings use shouldly's IgnoreLineEndings option. + // TODO: Add cross platform string compare and remove shoudly. + resultString.ShouldBe(expected, StringCompareShould.IgnoreLineEndings); + //NUnit.Framework.Assert.AreEqual(expected, resultString); + //StringAssert.AreEqualIgnoringCase(expected, result.ToFullString()); + } + + public TestWorkspace() + { + var source = +@" +public class ThisShouldAppear +{ +} +"; + + Workspace = new AdhocWorkspace(); + + var projName = "TestProject"; + var projectId = ProjectId.CreateNewId(); + var versionStamp = VersionStamp.Create(); + var projectInfo = ProjectInfo.Create(projectId, versionStamp, projName, projName, LanguageNames.CSharp); + var newProject = Workspace.AddProject(projectInfo); + + var sourceText = SourceText.From(source); + Document = Workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText); + } + + public AdhocWorkspace Workspace { get; private set; } + + public Document Document { get; private set; } + + public Document SetDocument(string text) + { + Document = Document.WithText(SourceText.From(text)); + NUnit.Framework.Assert.IsTrue(Workspace.TryApplyChanges(Document.Project.Solution)); + return Document; + } + } +} \ No newline at end of file diff --git a/CodeMaid.UnitTests/CodeMaid.UnitTests.csproj b/CodeMaid.UnitTests/CodeMaid.UnitTests.csproj index 23fc90cc..0a4d1e02 100644 --- a/CodeMaid.UnitTests/CodeMaid.UnitTests.csproj +++ b/CodeMaid.UnitTests/CodeMaid.UnitTests.csproj @@ -54,6 +54,7 @@ + @@ -66,6 +67,8 @@ + + @@ -101,6 +104,9 @@ 4.0.0 + + 4.1.0 + diff --git a/CodeMaid.VS2022/CodeMaid.VS2022.csproj b/CodeMaid.VS2022/CodeMaid.VS2022.csproj index 10e8a1cc..8e3cedd8 100644 --- a/CodeMaid.VS2022/CodeMaid.VS2022.csproj +++ b/CodeMaid.VS2022/CodeMaid.VS2022.csproj @@ -721,6 +721,15 @@ 1.0.2 + + 4.4.0 + + + 4.4.0 + + + 4.4.0 + 17.0.0-previews-2-31512-422 runtime diff --git a/CodeMaid.config b/CodeMaid.config index 12830564..ad5551e1 100644 --- a/CodeMaid.config +++ b/CodeMaid.config @@ -1,27 +1,27 @@ - - -
- - - - - - \.Designer\.cs$||\.Designer\.vb$||\.resx$||CodeMaid.IntegrationTests\\.*\\Data\\||CodeMaid\\CodeMaid.cs||CodeMaid\\source.extension.cs - - - True - - - False - - - True - - - True - - - + + +
+ + + + + + True + + + True + + + True + + + True + + + \ No newline at end of file diff --git a/CodeMaid/CodeMaid.csproj b/CodeMaid/CodeMaid.csproj index e0ea7465..98d8e6c6 100644 --- a/CodeMaid/CodeMaid.csproj +++ b/CodeMaid/CodeMaid.csproj @@ -36,7 +36,7 @@ full false bin\Debug\ - DEBUG;TRACE + DEBUG prompt 4 false @@ -339,6 +339,15 @@ 1.0.2 + + 4.4.0 + + + 4.4.0 + + + 4.4.0 + 16.10.31321.278 runtime diff --git a/CodeMaidShared/CodeMaidPackage.cs b/CodeMaidShared/CodeMaidPackage.cs index f8a1ef57..80e81b2c 100644 --- a/CodeMaidShared/CodeMaidPackage.cs +++ b/CodeMaidShared/CodeMaidPackage.cs @@ -206,6 +206,10 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke await RegisterCommandsAsync(); await RegisterEventListenersAsync(); + var componentModel = (IComponentModel)this.GetService((typeof(SComponentModel))); + + //var workspace = componentModel.GetService + Instance = this; } diff --git a/CodeMaidShared/CodeMaidShared.projitems b/CodeMaidShared/CodeMaidShared.projitems index 8fa3bac2..cbfc04b6 100644 --- a/CodeMaidShared/CodeMaidShared.projitems +++ b/CodeMaidShared/CodeMaidShared.projitems @@ -83,14 +83,27 @@ + + + + + + + + + + + + + @@ -384,4 +397,7 @@ Resources.resx + + + \ No newline at end of file diff --git a/CodeMaidShared/Helpers/CommandHelper.cs b/CodeMaidShared/Helpers/CommandHelper.cs index 962064e7..20ccce7f 100644 --- a/CodeMaidShared/Helpers/CommandHelper.cs +++ b/CodeMaidShared/Helpers/CommandHelper.cs @@ -54,6 +54,12 @@ public Command FindCommand(params string[] commandNames) { if (commandNames == null || commandNames.Length == 0) return null; + var commands = _package.IDE.Commands.OfType().Select(x => x.Name).ToArray(); + + var strings = string.Join(",\n", commands); + + var c = _package.IDE.Commands.OfType().Where(x => x.Name.Contains("Cleanup")).ToArray(); + return _package.IDE.Commands.OfType().FirstOrDefault(x => commandNames.Contains(x.Name)); } diff --git a/CodeMaidShared/Logic/Cleaning/CodeCleanupManager.cs b/CodeMaidShared/Logic/Cleaning/CodeCleanupManager.cs index 850536ca..f9d2bc46 100644 --- a/CodeMaidShared/Logic/Cleaning/CodeCleanupManager.cs +++ b/CodeMaidShared/Logic/Cleaning/CodeCleanupManager.cs @@ -1,4 +1,7 @@ +using CodeMaidShared.Logic.Cleaning; using EnvDTE; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Shell; using SteveCadwallader.CodeMaid.Helpers; using SteveCadwallader.CodeMaid.Logic.Formatting; using SteveCadwallader.CodeMaid.Logic.Reorganizing; @@ -8,6 +11,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Document = EnvDTE.Document; +using TextDocument = EnvDTE.TextDocument; + namespace SteveCadwallader.CodeMaid.Logic.Cleaning { @@ -273,59 +279,62 @@ private void RunCodeCleanupCSharp(Document document) _removeWhitespaceLogic.RemoveMultipleConsecutiveBlankLines(textDocument); // Perform insertion of blank line padding cleanup. - _insertBlankLinePaddingLogic.InsertPaddingBeforeRegionTags(regions); - _insertBlankLinePaddingLogic.InsertPaddingAfterRegionTags(regions); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeRegionTags(regions); + //_insertBlankLinePaddingLogic.InsertPaddingAfterRegionTags(regions); - _insertBlankLinePaddingLogic.InsertPaddingBeforeEndRegionTags(regions); - _insertBlankLinePaddingLogic.InsertPaddingAfterEndRegionTags(regions); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeEndRegionTags(regions); + //_insertBlankLinePaddingLogic.InsertPaddingAfterEndRegionTags(regions); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(usingStatementsThatStartBlocks); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(usingStatementsThatEndBlocks); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(usingStatementsThatStartBlocks); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(usingStatementsThatEndBlocks); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(namespaces); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(namespaces); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(namespaces); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(namespaces); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(classes); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(classes); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(classes); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(classes); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(delegates); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(delegates); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(delegates); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(delegates); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(enumerations); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(enumerations); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(enumerations); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(enumerations); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(events); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(events); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(events); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(events); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(fields); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(fields); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(fields); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(fields); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(interfaces); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(interfaces); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(interfaces); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(interfaces); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(methods); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(methods); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(methods); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(methods); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(properties); - _insertBlankLinePaddingLogic.InsertPaddingBetweenMultiLinePropertyAccessors(properties); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(properties); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(properties); + //_insertBlankLinePaddingLogic.InsertPaddingBetweenMultiLinePropertyAccessors(properties); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(properties); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(structs); - _insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(structs); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCodeElements(structs); + //_insertBlankLinePaddingLogic.InsertPaddingAfterCodeElements(structs); - _insertBlankLinePaddingLogic.InsertPaddingBeforeCaseStatements(textDocument); - _insertBlankLinePaddingLogic.InsertPaddingBeforeSingleLineComments(textDocument); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeCaseStatements(textDocument); + //_insertBlankLinePaddingLogic.InsertPaddingBeforeSingleLineComments(textDocument); // Perform insertion of explicit access modifier cleanup. - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnClasses(classes); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnDelegates(delegates); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnEnumerations(enumerations); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnEvents(events); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnFields(fields); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnInterfaces(interfaces); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnMethods(methods); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnProperties(properties); - _insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnStructs(structs); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnDelegates(delegates); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnEnumerations(enumerations); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnEvents(events); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnFields(fields); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnInterfaces(interfaces); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnClasses(classes); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnStructs(structs); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnMethods(methods); + //_insertExplicitAccessModifierLogic.InsertExplicitAccessModifiersOnProperties(properties); + + RoslynBuildAndRun(_package); + // Perform insertion of whitespace cleanup. _insertWhitespaceLogic.InsertEOFTrailingNewLine(textDocument); @@ -340,6 +349,38 @@ private void RunCodeCleanupCSharp(Document document) _commentFormatLogic.FormatComments(textDocument); } + public static void RoslynBuildAndRun(AsyncPackage package) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + Global.Package = package; + + var document = Global.GetActiveDocument(package); + + if (document == null || !document.TryGetSyntaxRoot(out SyntaxNode root)) + { + throw new InvalidOperationException(); + } + + var semanticModel = document.GetSemanticModelAsync().Result; + + var cleaner = new RoslynCleaner(); + + InsertExplicitAccessorMiddleware.Initialize(cleaner, semanticModel); + InsertNodePaddingMiddleware.Initialize(cleaner); + + InsertTokenPaddingMiddleware.Initialize(cleaner); + + var newRoot = cleaner.Process(root, Global.GetWorkspace(package)); + + document = document.WithSyntaxRoot(newRoot); + var success = Global.GetWorkspace(package).TryApplyChanges(document.Project.Solution); + if (!success) + { + OutputWindowHelper.DiagnosticWriteLine("Error applying roslyn cleanup changes."); + } + } + /// /// Attempts to run code cleanup on the specified VB.Net document. /// diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/AccessibilityModifiersRequired.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/AccessibilityModifiersRequired.cs new file mode 100644 index 00000000..ae3eaa30 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/AccessibilityModifiersRequired.cs @@ -0,0 +1,20 @@ +namespace CodeMaidShared.Logic.Cleaning +{ + internal enum AccessibilityModifiersRequired + { + // The rule is not run + Never = 0, + + // Accessibility modifiers are added if missing, even if default + Always = 1, + + // Future proofing for when C# adds default interface methods. At that point + // accessibility modifiers will be allowed in interfaces, and some people may + // want to require them, while some may want to keep the traditional C# style + // that public interface members do not need accessibility modifiers. + ForNonInterfaceMembers = 2, + + // Remove any accessibility modifier that matches the default + OmitIfDefault = 3 + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/CSharpAccessibilityFacts.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/CSharpAccessibilityFacts.cs new file mode 100644 index 00000000..52f6b357 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/CSharpAccessibilityFacts.cs @@ -0,0 +1,425 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using SteveCadwallader.CodeMaid.Logic.Cleaning; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal static class CSharpAccessibilityFacts + { + public static bool ShouldUpdateAccessibilityModifier( + MemberDeclarationSyntax member, + AccessibilityModifiersRequired option, + out Accessibility accessibility, + out bool modifierAdded) + { + modifierAdded = false; + accessibility = Accessibility.NotApplicable; + + // Have to have a name to report the issue on. + var name = member.GetNameToken(); + if (name.IsKind(SyntaxKind.None)) + return false; + + // Certain members never have accessibility. Don't bother reporting on them. + if (!CanHaveAccessibility(member)) + return false; + + //if (!IsParentValid(member)) + // return false; + + // This analyzer bases all of its decisions on the accessibility + accessibility = GetAccessibility(member); + + // Omit will flag any accessibility values that exist and are default + // The other options will remove or ignore accessibility + var isOmit = option == AccessibilityModifiersRequired.OmitIfDefault; + modifierAdded = !isOmit; + + if (isOmit) + { + if (accessibility == Accessibility.NotApplicable) + return false; + + var parentKind = member.GetRequiredParent().Kind(); + switch (parentKind) + { + // Check for default modifiers in namespace and outside of namespace + case SyntaxKind.CompilationUnit: + case SyntaxKind.FileScopedNamespaceDeclaration: + case SyntaxKind.NamespaceDeclaration: + { + // Default is internal + if (accessibility != Accessibility.Internal) + return false; + } + + break; + + case SyntaxKind.ClassDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordStructDeclaration: + { + // Inside a type, default is private + if (accessibility != Accessibility.Private) + return false; + } + + break; + + default: + return false; // Unknown parent kind, don't do anything + } + } + else + { + // Mode is always, so we have to flag missing modifiers + if (accessibility != Accessibility.NotApplicable) + return false; + } + + return true; + } + + // TODO Unneeded if descendants of an interface are not passed into ShouldUpdateAccessibilityModifier + // Cant check for grandparent interfaces + public static bool IsParentValid(SyntaxNode node) + { + if (node.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.InterfaceDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration or SyntaxKind.EnumDeclaration or SyntaxKind.DelegateDeclaration) + { + return node.Parent.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration; + } + + // Non object declarations must be inside an object declaration. + // Can probably simplify to: Parent is not interface or Parent is class/struct/record/namespace + if (node.Kind() is SyntaxKind.EventFieldDeclaration or SyntaxKind.FieldDeclaration or SyntaxKind.PropertyDeclaration or SyntaxKind.MethodDeclaration) + { + return node.Parent.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration; + } + return true; + } + + public static bool CanHaveAccessibility(SyntaxNode declaration, bool ignoreDeclarationModifiers = false) + { + switch (declaration.Kind()) + { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordStructDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.DelegateDeclaration: + return ignoreDeclarationModifiers || !((MemberDeclarationSyntax)declaration).Modifiers.Any(SyntaxKind.FileKeyword); + + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + return true; + + case SyntaxKind.VariableDeclaration: + case SyntaxKind.VariableDeclarator: + var declarationKind = GetDeclarationKind(declaration); + return declarationKind is DeclarationKind.Field or DeclarationKind.Event; + + case SyntaxKind.ConstructorDeclaration: + // Static constructor can't have accessibility + return ignoreDeclarationModifiers || !((ConstructorDeclarationSyntax)declaration).Modifiers.Any(SyntaxKind.StaticKeyword); + + case SyntaxKind.PropertyDeclaration: + return ((PropertyDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null; + + case SyntaxKind.IndexerDeclaration: + return ((IndexerDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null; + + case SyntaxKind.OperatorDeclaration: + return ((OperatorDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null; + + case SyntaxKind.ConversionOperatorDeclaration: + return ((ConversionOperatorDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null; + + case SyntaxKind.MethodDeclaration: + var method = (MethodDeclarationSyntax)declaration; + if (method.ExplicitInterfaceSpecifier != null) + { + // explicit interface methods can't have accessibility. + return false; + } + + if (method.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + // partial methods can't have accessibility modifiers. + return false; + } + + return true; + + case SyntaxKind.EventDeclaration: + return ((EventDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null; + + default: + return false; + } + } + + public static Accessibility GetAccessibility(SyntaxNode declaration) + { + if (!CanHaveAccessibility(declaration)) + return Accessibility.NotApplicable; + + var modifierTokens = GetModifierTokens(declaration); + GetAccessibilityAndModifiers(modifierTokens, out var accessibility, out _, out _); + return accessibility; + } + + public static void GetAccessibilityAndModifiers(SyntaxTokenList modifierList, out Accessibility accessibility, out DeclarationModifiers modifiers, out bool isDefault) + { + accessibility = Accessibility.NotApplicable; + modifiers = DeclarationModifiers.None; + isDefault = false; + + foreach (var token in modifierList) + { + accessibility = (token.Kind(), accessibility) switch + { + (SyntaxKind.PublicKeyword, _) => Accessibility.Public, + + (SyntaxKind.PrivateKeyword, Accessibility.Protected) => Accessibility.ProtectedAndInternal, + (SyntaxKind.PrivateKeyword, _) => Accessibility.Private, + + (SyntaxKind.InternalKeyword, Accessibility.Protected) => Accessibility.ProtectedOrInternal, + (SyntaxKind.InternalKeyword, _) => Accessibility.Internal, + + (SyntaxKind.ProtectedKeyword, Accessibility.Private) => Accessibility.ProtectedAndInternal, + (SyntaxKind.ProtectedKeyword, Accessibility.Internal) => Accessibility.ProtectedOrInternal, + (SyntaxKind.ProtectedKeyword, _) => Accessibility.Protected, + + _ => accessibility, + }; + + modifiers |= token.Kind() switch + { + SyntaxKind.AbstractKeyword => DeclarationModifiers.Abstract, + SyntaxKind.NewKeyword => DeclarationModifiers.New, + SyntaxKind.OverrideKeyword => DeclarationModifiers.Override, + SyntaxKind.VirtualKeyword => DeclarationModifiers.Virtual, + SyntaxKind.StaticKeyword => DeclarationModifiers.Static, + SyntaxKind.AsyncKeyword => DeclarationModifiers.Async, + SyntaxKind.ConstKeyword => DeclarationModifiers.Const, + SyntaxKind.ReadOnlyKeyword => DeclarationModifiers.ReadOnly, + SyntaxKind.SealedKeyword => DeclarationModifiers.Sealed, + SyntaxKind.UnsafeKeyword => DeclarationModifiers.Unsafe, + SyntaxKind.PartialKeyword => DeclarationModifiers.Partial, + SyntaxKind.RefKeyword => DeclarationModifiers.Ref, + SyntaxKind.VolatileKeyword => DeclarationModifiers.Volatile, + SyntaxKind.ExternKeyword => DeclarationModifiers.Extern, + SyntaxKind.FileKeyword => DeclarationModifiers.File, + SyntaxKind.RequiredKeyword => DeclarationModifiers.Required, + _ => DeclarationModifiers.None, + }; + + isDefault |= token.Kind() == SyntaxKind.DefaultKeyword; + } + } + + public static DeclarationKind GetDeclarationKind(SyntaxNode declaration) + { + switch (declaration.Kind()) + { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.RecordDeclaration: + return DeclarationKind.Class; + + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordStructDeclaration: + return DeclarationKind.Struct; + + case SyntaxKind.InterfaceDeclaration: + return DeclarationKind.Interface; + + case SyntaxKind.EnumDeclaration: + return DeclarationKind.Enum; + + case SyntaxKind.DelegateDeclaration: + return DeclarationKind.Delegate; + + case SyntaxKind.MethodDeclaration: + return DeclarationKind.Method; + + case SyntaxKind.OperatorDeclaration: + return DeclarationKind.Operator; + + case SyntaxKind.ConversionOperatorDeclaration: + return DeclarationKind.ConversionOperator; + + case SyntaxKind.ConstructorDeclaration: + return DeclarationKind.Constructor; + + case SyntaxKind.DestructorDeclaration: + return DeclarationKind.Destructor; + + case SyntaxKind.PropertyDeclaration: + return DeclarationKind.Property; + + case SyntaxKind.IndexerDeclaration: + return DeclarationKind.Indexer; + + case SyntaxKind.EventDeclaration: + return DeclarationKind.CustomEvent; + + case SyntaxKind.EnumMemberDeclaration: + return DeclarationKind.EnumMember; + + case SyntaxKind.CompilationUnit: + return DeclarationKind.CompilationUnit; + + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.FileScopedNamespaceDeclaration: + return DeclarationKind.Namespace; + + case SyntaxKind.UsingDirective: + return DeclarationKind.NamespaceImport; + + case SyntaxKind.Parameter: + return DeclarationKind.Parameter; + + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + return DeclarationKind.LambdaExpression; + + case SyntaxKind.FieldDeclaration: + var fd = (FieldDeclarationSyntax)declaration; + if (fd.Declaration != null && fd.Declaration.Variables.Count == 1) + { + // this node is considered the declaration if it contains only one variable. + return DeclarationKind.Field; + } + else + { + return DeclarationKind.None; + } + + case SyntaxKind.EventFieldDeclaration: + var ef = (EventFieldDeclarationSyntax)declaration; + if (ef.Declaration != null && ef.Declaration.Variables.Count == 1) + { + // this node is considered the declaration if it contains only one variable. + return DeclarationKind.Event; + } + else + { + return DeclarationKind.None; + } + + case SyntaxKind.LocalDeclarationStatement: + var ld = (LocalDeclarationStatementSyntax)declaration; + if (ld.Declaration != null && ld.Declaration.Variables.Count == 1) + { + // this node is considered the declaration if it contains only one variable. + return DeclarationKind.Variable; + } + else + { + return DeclarationKind.None; + } + + case SyntaxKind.VariableDeclaration: + { + var vd = (VariableDeclarationSyntax)declaration; + if (vd.Variables.Count == 1 && vd.Parent == null) + { + // this node is the declaration if it contains only one variable and has no parent. + return DeclarationKind.Variable; + } + else + { + return DeclarationKind.None; + } + } + + case SyntaxKind.VariableDeclarator: + { + var vd = declaration.Parent as VariableDeclarationSyntax; + + // this node is considered the declaration if it is one among many, or it has no parent + if (vd == null || vd.Variables.Count > 1) + { + if (ParentIsFieldDeclaration(vd)) + { + return DeclarationKind.Field; + } + else if (ParentIsEventFieldDeclaration(vd)) + { + return DeclarationKind.Event; + } + else + { + return DeclarationKind.Variable; + } + } + + break; + } + + case SyntaxKind.AttributeList: + var list = (AttributeListSyntax)declaration; + if (list.Attributes.Count == 1) + { + return DeclarationKind.Attribute; + } + + break; + + case SyntaxKind.Attribute: + if (declaration.Parent is not AttributeListSyntax parentList || parentList.Attributes.Count > 1) + { + return DeclarationKind.Attribute; + } + + break; + + case SyntaxKind.GetAccessorDeclaration: + return DeclarationKind.GetAccessor; + + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + return DeclarationKind.SetAccessor; + + case SyntaxKind.AddAccessorDeclaration: + return DeclarationKind.AddAccessor; + + case SyntaxKind.RemoveAccessorDeclaration: + return DeclarationKind.RemoveAccessor; + } + + return DeclarationKind.None; + } + + public static SyntaxTokenList GetModifierTokens(SyntaxNode declaration) + => declaration switch + { + MemberDeclarationSyntax memberDecl => memberDecl.Modifiers, + ParameterSyntax parameter => parameter.Modifiers, + LocalDeclarationStatementSyntax localDecl => localDecl.Modifiers, + LocalFunctionStatementSyntax localFunc => localFunc.Modifiers, + AccessorDeclarationSyntax accessor => accessor.Modifiers, + VariableDeclarationSyntax varDecl => GetModifierTokens(varDecl.Parent ?? throw new InvalidOperationException("Token's parent was null")), + VariableDeclaratorSyntax varDecl => GetModifierTokens(varDecl.Parent ?? throw new InvalidOperationException("Token's parent was null")), + AnonymousFunctionExpressionSyntax anonymous => anonymous.Modifiers, + _ => default, + }; + + public static bool ParentIsFieldDeclaration(SyntaxNode? node) + => node?.Parent.IsKind(SyntaxKind.FieldDeclaration) ?? false; + + public static bool ParentIsEventFieldDeclaration(SyntaxNode? node) + => node?.Parent.IsKind(SyntaxKind.EventFieldDeclaration) ?? false; + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Global.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Global.cs new file mode 100644 index 00000000..99acbb97 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Global.cs @@ -0,0 +1,68 @@ +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System.Linq; +using Document = Microsoft.CodeAnalysis.Document; +using DteDocument = EnvDTE.Document; +using Solution = Microsoft.CodeAnalysis.Solution; + +namespace SteveCadwallader.CodeMaid.Logic.Cleaning +{ + internal static class Global + { + public static AsyncPackage Package; + + public static T GetService(AsyncPackage package) + => (T)package?.GetServiceAsync(typeof(T))?.Result; + + public static DteDocument GetActiveDteDocument(AsyncPackage package) + { + ThreadHelper.ThrowIfNotOnUIThread(); + dynamic dte = GetService(package); + return (DteDocument)dte.ActiveDocument; + } + + static IVsStatusbar Statusbar; + + internal static void SetStatusMessage(AsyncPackage package, string message) + { + if (Statusbar == null) + { + Statusbar = GetService(package); + // StatusBar = Package.GetGlobalService(typeof(IVsStatusbar)) as IVsStatusbar; + } + ThreadHelper.ThrowIfNotOnUIThread(); + Statusbar.SetText(message); + } + + public static Document GetActiveDocument(AsyncPackage package) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + Solution solution = GetWorkspace(package).CurrentSolution; + string activeDocPath = GetActiveDteDocument(package)?.FullName; + + if (activeDocPath != null) + return solution.Projects + .SelectMany(x => x.Documents) + .FirstOrDefault(x => x.SupportsSyntaxTree && + x.SupportsSemanticModel && + x.FilePath == activeDocPath); + return null; + } + + private static VisualStudioWorkspace workspace = null; + + public static VisualStudioWorkspace GetWorkspace(AsyncPackage package) + { + if (workspace == null) + { + IComponentModel componentModel = GetService(package) as IComponentModel; + workspace = componentModel.GetService(); + } + return workspace; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/IRoslynNodeMiddleware.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/IRoslynNodeMiddleware.cs new file mode 100644 index 00000000..5051408b --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/IRoslynNodeMiddleware.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal interface IRoslynNodeMiddleware + { + public SyntaxNode Invoke(SyntaxNode original, SyntaxNode newNode); + + // TODO this is messy, don't know how else to do it. + public void SetNodeDelegate(Func next); + } +} diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/IRoslynTokenMiddleware.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/IRoslynTokenMiddleware.cs new file mode 100644 index 00000000..207f4830 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/IRoslynTokenMiddleware.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal interface IRoslynTokenMiddleware + { + public SyntaxToken Invoke(SyntaxToken original, SyntaxToken newToken); + + // TODO this is messy, don't know how else to do it. + public void SetTokenDelegate(Func next); + } +} diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertExplicitAccessorMiddleware.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertExplicitAccessorMiddleware.cs new file mode 100644 index 00000000..894db824 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertExplicitAccessorMiddleware.cs @@ -0,0 +1,60 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Editing; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal class InsertExplicitAccessorMiddleware : IRoslynNodeMiddleware + { + private RoslynInsertExplicitAccessModifierLogic _insertAccess; + public InsertExplicitAccessorMiddleware(SemanticModel semanticModel) + { + _insertAccess = new RoslynInsertExplicitAccessModifierLogic(semanticModel); + } + private Func Next { get; set; } + + // Use this messy functions to ensure that the current node is not a descendant of an interface. + // This is to mimic the recursive CSharpAddAccessibilityModifiersDiagnosticAnalyzer.ProcessMemberDeclaration + // search where any non structs/classes are ignored. + // FindAncestorOrSelf might help but would be slower. + // Dont terminate on finding an interface in case I want to roslynize more cleanup functions. + private bool InsideInterface { get; set; } + + public static RoslynCleaner Initialize(RoslynCleaner cleanup, SemanticModel model) + { + cleanup.AddNodeMiddleware(new InsertExplicitAccessorMiddleware(model)); + + return cleanup; + } + + public SyntaxNode Invoke(SyntaxNode original, SyntaxNode newNode) + { + if (original == null) + { + return original; + } + + var inInterface = InsideInterface; + + if (original.IsKind(SyntaxKind.InterfaceDeclaration)) + InsideInterface = true; + + // Might have to account for namespaces here. + newNode = Next(original, newNode); + + if (inInterface == false) + { + newNode = _insertAccess.TryAddExplicitModifier(original, newNode); + } + + InsideInterface = inInterface; + return newNode; + } + + public void SetNodeDelegate(Func next) + { + Next = next; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertNodePaddingMiddleware.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertNodePaddingMiddleware.cs new file mode 100644 index 00000000..1284d033 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertNodePaddingMiddleware.cs @@ -0,0 +1,43 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal class InsertNodePaddingMiddleware : IRoslynNodeMiddleware + { + public InsertNodePaddingMiddleware() { } + + private bool ShouldAddPadding { get; set; } + private bool IsFirstNode { get; set; } + private Func Next { get; set; } + + public static RoslynCleaner Initialize(RoslynCleaner cleanup) + { + cleanup.AddNodeMiddleware(new InsertNodePaddingMiddleware()); + return cleanup; + } + + public SyntaxNode Invoke(SyntaxNode original, SyntaxNode newNode) + { + var shouldAddPadding = ShouldAddPadding; + var isFirst = IsFirstNode; + + ShouldAddPadding = false; + IsFirstNode = true; + + newNode = Next(original, newNode); + + (newNode, ShouldAddPadding) = RoslynInsertPaddingLogic.TryAddPadding(original, newNode, shouldAddPadding, isFirst); + + // Have to ignore inheritance/type/attribute nodes until the first member node. + IsFirstNode = isFirst ? newNode is TypeParameterListSyntax or AttributeArgumentListSyntax or BaseListSyntax or TypeParameterConstraintClauseSyntax : false; + return newNode; + } + + public void SetNodeDelegate(Func next) + { + Next = next; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertTokenPaddingMiddleware.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertTokenPaddingMiddleware.cs new file mode 100644 index 00000000..d1f48bcf --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/InsertTokenPaddingMiddleware.cs @@ -0,0 +1,93 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using SteveCadwallader.CodeMaid.Logic.Cleaning; +using SteveCadwallader.CodeMaid.Properties; +using System; +using System.Linq; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal class InsertTokenPaddingMiddleware : IRoslynTokenMiddleware + { + public bool BeforeIsOpen { get; set; } = false; + private Func Next { get; set; } + + public SyntaxToken Invoke(SyntaxToken token, SyntaxToken newToken) + { + var beforeIsOpenToken = BeforeIsOpen; + + if (!token.IsKind(SyntaxKind.None)) + { + BeforeIsOpen = token.IsKind(SyntaxKind.OpenBraceToken); + } + newToken = Next(token, newToken); + + // Read trivia: + // Assume that leading trivia must start on a new line. + // Valid line is a single line with white space and endofline, preceeded by a non blank line. + // Also check that + return TryPadTrivia(newToken, beforeIsOpenToken); + } + public void SetTokenDelegate(Func next) + { + Next = next; + } + + public static void Initialize(RoslynCleaner roslynCleanup) + { + var tokenPadding = new InsertTokenPaddingMiddleware(); + roslynCleanup.AddTokenMiddleware(tokenPadding); + } + + // TODO Revisit logic, cleanup, refactor. Consider calculating the trivia lines on the go while inserting padding. + // Might not work if all trivia is on the same line. + private static SyntaxToken TryPadTrivia(SyntaxToken newToken, bool priorIsOpen) + { + var trivia = newToken.LeadingTrivia.ToList(); + + var (triviaLines, last) = RoslynExtensions.ReadTriviaLines(trivia); + triviaLines.Add((newToken.Kind(), last)); + + // Set previous line type to either open brace or a place holder. + var prevLine = priorIsOpen ? SyntaxKind.OpenBraceToken : SyntaxKind.BadDirectiveTrivia; + + var insertCount = 0; + + for (int i = 0; i < triviaLines.Count; i++) + { + var (currentLine, pos) = triviaLines[i]; + if (TryAddPadding(prevLine, currentLine)) + { + trivia.Insert(pos + insertCount, SyntaxFactory.EndOfLine(Environment.NewLine)); + insertCount++; + } + prevLine = currentLine; + } + + if (insertCount > 0) + { + return newToken.WithLeadingTrivia(trivia); + } + + return newToken; + } + + private static bool TryAddPadding(SyntaxKind prior, SyntaxKind current) + { + return current switch + { + SyntaxKind.RegionDirectiveTrivia when prior is not (SyntaxKind.WhitespaceTrivia or SyntaxKind.OpenBraceToken) &&Settings.Default.Cleaning_InsertBlankLinePaddingBeforeRegionTags => true, + + not (SyntaxKind.WhitespaceTrivia or SyntaxKind.CloseBraceToken) when prior == SyntaxKind.RegionDirectiveTrivia &&Settings.Default.Cleaning_InsertBlankLinePaddingAfterRegionTags => true, + + SyntaxKind.EndRegionDirectiveTrivia when prior is not (SyntaxKind.WhitespaceTrivia or SyntaxKind.OpenBraceToken) &&Settings.Default.Cleaning_InsertBlankLinePaddingBeforeEndRegionTags => true, + + not (SyntaxKind.WhitespaceTrivia or SyntaxKind.CloseBraceToken) when prior == SyntaxKind.EndRegionDirectiveTrivia &&Settings.Default.Cleaning_InsertBlankLinePaddingAfterEndRegionTags => true, + + SyntaxKind.SingleLineCommentTrivia when prior is not (SyntaxKind.SingleLineCommentTrivia or SyntaxKind.SingleLineDocumentationCommentTrivia or SyntaxKind.WhitespaceTrivia or SyntaxKind.OpenBraceToken) => true, + + _ => false, + }; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/RoslynInsertExplicitAccessModifierLogic.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/RoslynInsertExplicitAccessModifierLogic.cs new file mode 100644 index 00000000..8264c6f6 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/RoslynInsertExplicitAccessModifierLogic.cs @@ -0,0 +1,85 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SteveCadwallader.CodeMaid.Logic.Cleaning; +using SteveCadwallader.CodeMaid.Properties; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + /// + /// A class for encapsulating insertion of explicit access modifier logic. + /// + internal class RoslynInsertExplicitAccessModifierLogic + { + #region Fields + + private readonly SemanticModel _semanticModel; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public RoslynInsertExplicitAccessModifierLogic(SemanticModel semanticModel) + { + _semanticModel = semanticModel; + } + + #endregion Constructors + + public SyntaxNode TryAddExplicitModifier(SyntaxNode original, SyntaxNode newNode) + { + return newNode switch + { + DelegateDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnDelegates => AddAccessibility(original, newNode), + EventFieldDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnEvents => AddAccessibility(original, newNode), + EnumDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnEnumerations => AddAccessibility(original, newNode), + FieldDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnFields => AddAccessibility(original, newNode), + InterfaceDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnInterfaces => AddAccessibility(original, newNode), + + PropertyDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnProperties => AddAccessibility(original, newNode), + MethodDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnMethods => AddAccessibility(original, newNode), + + ClassDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnClasses => AddAccessibility(original, newNode), + StructDeclarationSyntax when Settings.Default.Cleaning_InsertExplicitAccessModifiersOnStructs => AddAccessibility(original, newNode), + + //RecordDeclarationSyntax when node.IsKind(SyntaxKind.RecordDeclaration) && Settings.Default.Cleaning_InsertExplicitAccessModifiersOnRecords => AddAccessibility(original, node), + //RecordDeclarationSyntax when node.IsKind(SyntaxKind.RecordStructDeclaration) && Settings.Default.Cleaning_InsertExplicitAccessModifiersOnRecordStructs => AddAccessibility(original, node), + + _ => newNode, + }; + } + + private SyntaxNode AddAccessibility(SyntaxNode original, SyntaxNode newNode) + { + if (!CSharpAccessibilityFacts.ShouldUpdateAccessibilityModifier(original as MemberDeclarationSyntax, AccessibilityModifiersRequired.Always, out var _, out var canChange)) + { + return newNode; + } + + var mapped = MapToDeclarator(original); + + var symbol = _semanticModel.GetDeclaredSymbol(mapped); + if (symbol is null) + { + throw new ArgumentNullException(nameof(symbol)); + } + + var preferredAccessibility = AddAccessibilityModifiersHelpers.GetPreferredAccessibility(symbol); + return InternalGenerator.WithAccessibility(newNode, preferredAccessibility); + } + + private static SyntaxNode MapToDeclarator(SyntaxNode node) + { + return node switch + { + FieldDeclarationSyntax field => field.Declaration.Variables[0], + EventFieldDeclarationSyntax eventField => eventField.Declaration.Variables[0], + _ => node, + }; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/RoslynInsertPaddingLogic.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/RoslynInsertPaddingLogic.cs new file mode 100644 index 00000000..bcef61ac --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/Handlers/RoslynInsertPaddingLogic.cs @@ -0,0 +1,161 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using SteveCadwallader.CodeMaid.Logic.Cleaning; +using SteveCadwallader.CodeMaid.Properties; + +namespace CodeMaidShared.Logic.Cleaning +{ + /// + /// A class for encapsulating insertion of blank line padding logic. + /// + internal static class RoslynInsertPaddingLogic + { + public static (SyntaxNode node, bool requiresPaddingAfter) TryAddPadding(SyntaxNode original, SyntaxNode newNode, bool previousRequiresPaddingStart, bool isFirst) + { + // Assume that whitespace blank lines are considered valid padding. + // Add padding to start + // If addPaddingStart + // when no padding at first leading trivia + // when not first node. + // Else + // when settings require padding + // when does not contain a new line in any of the trivia + // when not first node + + // Return addTrailingPadding when setting.AddPaddingEnd + + // If Settings require padding at the end set true + + // Cannot add padding to end, if padding is needed let the next node add padding. + bool requiresPaddingAfter = RequiresPaddingAfter(newNode); + bool shouldAddPaddingBefore = RequiresPaddingBefore(newNode); + + if (isFirst || StartHasPadding(newNode)) + { + return (newNode, requiresPaddingAfter); + } + + var containsAnyPadding = HasAnyPadding(newNode); + + if (previousRequiresPaddingStart || (shouldAddPaddingBefore && !containsAnyPadding)) + { + newNode = InternalGenerator.AddBlankLineToStart(newNode); + } + + return (newNode, requiresPaddingAfter); + } + + private static bool RequiresPaddingBefore(SyntaxNode newNode) + { + bool shouldAddPaddingBefore = (newNode.Kind(), Settings.Default) switch + { + // Pad multiline accessors, assume that other nodes cannot be inside an accessor list. + (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration or SyntaxKind.InitAccessorDeclaration, { Cleaning_InsertBlankLinePaddingBetweenPropertiesMultiLineAccessors: true }) when newNode.SpansMultipleLines() => true, + + (SyntaxKind.UsingStatement, { Cleaning_InsertBlankLinePaddingBeforeUsingStatementBlocks: true }) => true, + (SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration, { Cleaning_InsertBlankLinePaddingBeforeNamespaces: true }) => true, + (SyntaxKind.DefaultSwitchLabel or SyntaxKind.CaseSwitchLabel, { Cleaning_InsertBlankLinePaddingBeforeCaseStatements: true }) => true, + (SyntaxKind.ClassDeclaration, { Cleaning_InsertBlankLinePaddingBeforeClasses: true }) => true, + (SyntaxKind.DelegateDeclaration, { Cleaning_InsertBlankLinePaddingBeforeDelegates: true }) => true, + (SyntaxKind.EnumDeclaration, { Cleaning_InsertBlankLinePaddingBeforeEnumerations: true }) => true, + (SyntaxKind.EventDeclaration, { Cleaning_InsertBlankLinePaddingBeforeEvents: true }) => true, + + (SyntaxKind.FieldDeclaration, { Cleaning_InsertBlankLinePaddingBeforeFieldsMultiLine: true }) when newNode.SpansMultipleLines() => true, + (SyntaxKind.FieldDeclaration, { Cleaning_InsertBlankLinePaddingBeforeFieldsSingleLine: true }) when newNode.SpansMultipleLines() == false => true, + + (SyntaxKind.InterfaceDeclaration, { Cleaning_InsertBlankLinePaddingBeforeInterfaces: true }) => true, + (SyntaxKind.MethodDeclaration, { Cleaning_InsertBlankLinePaddingBeforeMethods: true }) => true, + + (SyntaxKind.PropertyDeclaration, { Cleaning_InsertBlankLinePaddingBeforePropertiesMultiLine: true }) when newNode.SpansMultipleLines() => true, + (SyntaxKind.PropertyDeclaration, { Cleaning_InsertBlankLinePaddingBeforePropertiesSingleLine: true }) when newNode.SpansMultipleLines() == false => true, + + (SyntaxKind.StructDeclaration, { Cleaning_InsertBlankLinePaddingBeforeStructs: true }) => true, + + (SyntaxKind.RecordDeclaration, { Cleaning_InsertBlankLinePaddingBeforeStructs: true }) => true, + (SyntaxKind.RecordStructDeclaration, { Cleaning_InsertBlankLinePaddingBeforeStructs: true }) => true, + + _ => false, + }; + return shouldAddPaddingBefore; + } + + private static bool RequiresPaddingAfter(SyntaxNode newNode) + { + bool shouldAddPaddingAfter = (newNode.Kind(), Settings.Default) switch + { + // Pad multiline accessors, assume that other nodes cannot be inside an accessor list. + (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration or SyntaxKind.InitAccessorDeclaration, { Cleaning_InsertBlankLinePaddingBetweenPropertiesMultiLineAccessors: true }) when newNode.SpansMultipleLines() => true, + + (SyntaxKind.UsingStatement, { Cleaning_InsertBlankLinePaddingAfterUsingStatementBlocks: true }) => true, + (SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration, { Cleaning_InsertBlankLinePaddingAfterNamespaces: true }) => true, + (SyntaxKind.ClassDeclaration, { Cleaning_InsertBlankLinePaddingAfterClasses: true }) => true, + (SyntaxKind.DelegateDeclaration, { Cleaning_InsertBlankLinePaddingAfterDelegates: true }) => true, + (SyntaxKind.EnumDeclaration, { Cleaning_InsertBlankLinePaddingAfterEnumerations: true }) => true, + (SyntaxKind.EventDeclaration, { Cleaning_InsertBlankLinePaddingAfterEvents: true }) => true, + + (SyntaxKind.FieldDeclaration, { Cleaning_InsertBlankLinePaddingAfterFieldsSingleLine: true }) when newNode.SpansMultipleLines() => true, + (SyntaxKind.FieldDeclaration, { Cleaning_InsertBlankLinePaddingAfterFieldsSingleLine: true }) when newNode.SpansMultipleLines() == false => true, + + (SyntaxKind.InterfaceDeclaration, { Cleaning_InsertBlankLinePaddingAfterInterfaces: true }) => true, + (SyntaxKind.MethodDeclaration, { Cleaning_InsertBlankLinePaddingAfterMethods: true }) => true, + + (SyntaxKind.PropertyDeclaration, { Cleaning_InsertBlankLinePaddingAfterPropertiesMultiLine: true }) when newNode.SpansMultipleLines() => true, + (SyntaxKind.PropertyDeclaration, { Cleaning_InsertBlankLinePaddingAfterPropertiesSingleLine: true }) when newNode.SpansMultipleLines() == false => true, + + (SyntaxKind.StructDeclaration, { Cleaning_InsertBlankLinePaddingAfterStructs: true }) => true, + + (SyntaxKind.RecordDeclaration, { Cleaning_InsertBlankLinePaddingAfterStructs: true }) => true, + (SyntaxKind.RecordStructDeclaration, { Cleaning_InsertBlankLinePaddingAfterStructs: true }) => true, + + _ => false, + }; + return shouldAddPaddingAfter; + } + + private static bool StartHasPadding(SyntaxNode node) + { + foreach (var item in node.GetLeadingTrivia()) + { + var kind = item.Kind(); + if (kind == SyntaxKind.WhitespaceTrivia) + { + continue; + } + if (kind == SyntaxKind.EndOfLineTrivia) + { + return true; + } + + return false; + } + + return false; + } + + private static bool HasAnyPadding(SyntaxNode node) + { + var isPadding = true; + foreach (var item in node.GetLeadingTrivia()) + { + var kind = item.Kind(); + if (kind == SyntaxKind.WhitespaceTrivia) + { + continue; + } + if (kind == SyntaxKind.EndOfLineTrivia) + { + if (isPadding) + { + return true; + } + isPadding = true; + continue; + } + + isPadding = false; + } + + return false; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/InternalGenerator.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/InternalGenerator.cs new file mode 100644 index 00000000..5acbb820 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/InternalGenerator.cs @@ -0,0 +1,282 @@ +using CodeMaidShared.Logic.Cleaning; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SteveCadwallader.CodeMaid.Logic.Cleaning +{ + internal static class InternalGenerator + { + public static SyntaxNode WithAccessibility(SyntaxNode declaration, Accessibility accessibility) + { + if (!CSharpAccessibilityFacts.CanHaveAccessibility(declaration) && + accessibility != Accessibility.NotApplicable) + { + return declaration; + } + + return Isolate(declaration, d => + { + var a = d as TypeDeclarationSyntax; + var tokens = CSharpAccessibilityFacts.GetModifierTokens(d); + CSharpAccessibilityFacts.GetAccessibilityAndModifiers(tokens, out _, out var modifiers, out _); + if (modifiers.IsFile && accessibility != Accessibility.NotApplicable) + { + // If user wants to set accessibility for a file-local declaration, we remove file. + // Otherwise, code will be in error: + // error CS9052: File-local type '{0}' cannot use accessibility modifiers. + modifiers = modifiers.WithIsFile(false); + } + + if (modifiers.IsStatic && declaration.IsKind(SyntaxKind.ConstructorDeclaration) && accessibility != Accessibility.NotApplicable) + { + // If user wants to add accessibility for a static constructor, we remove static modifier + modifiers = modifiers.WithIsStatic(false); + } + + var newTokens = Merge(tokens, AsModifierList(accessibility, modifiers)); + return SetModifierTokens(d, newTokens); + }); + } + + public static SyntaxNode AddBlankLineToStart(SyntaxNode declaration) + { + var originalTrivia = declaration.GetLeadingTrivia(); + var newTrivia = originalTrivia.Insert(0, SyntaxFactory.EndOfLine(Environment.NewLine)); + + var newNode = declaration.WithLeadingTrivia(newTrivia); + + return newNode; + } + + private static SyntaxNode SetModifierTokens(SyntaxNode declaration, SyntaxTokenList modifiers) + => declaration switch + { + MemberDeclarationSyntax memberDecl => memberDecl.WithModifiers(modifiers), + ParameterSyntax parameter => parameter.WithModifiers(modifiers), + LocalDeclarationStatementSyntax localDecl => localDecl.WithModifiers(modifiers), + LocalFunctionStatementSyntax localFunc => localFunc.WithModifiers(modifiers), + AccessorDeclarationSyntax accessor => accessor.WithModifiers(modifiers), + AnonymousFunctionExpressionSyntax anonymous => anonymous.WithModifiers(modifiers), + _ => declaration, + }; + + internal static SyntaxTokenList Merge(SyntaxTokenList original, SyntaxTokenList newList) + { + // return tokens from newList, but use original tokens of kind matches + return new SyntaxTokenList(newList.Select( + token => Any(original, token.RawKind) + ? original.First(tk => tk.RawKind == token.RawKind) + : token)); + } + + private static bool Any(SyntaxTokenList original, int rawKind) + { + foreach (var token in original) + { + if (token.RawKind == rawKind) + { + return true; + } + } + + return false; + } + + private static SyntaxTokenList AsModifierList(Accessibility accessibility, DeclarationModifiers modifiers) + { + var list = new List(); + + switch (accessibility) + { + case Accessibility.Internal: + list.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword)); + break; + + case Accessibility.Public: + list.Add(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); + break; + + case Accessibility.Private: + list.Add(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); + break; + + case Accessibility.Protected: + list.Add(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword)); + break; + + case Accessibility.ProtectedOrInternal: + list.Add(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword)); + list.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword)); + break; + + case Accessibility.ProtectedAndInternal: + list.Add(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); + list.Add(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword)); + break; + + case Accessibility.NotApplicable: + break; + } + + if (modifiers.IsFile) + list.Add(SyntaxFactory.Token(SyntaxKind.FileKeyword)); + + if (modifiers.IsAbstract) + list.Add(SyntaxFactory.Token(SyntaxKind.AbstractKeyword)); + + if (modifiers.IsNew) + list.Add(SyntaxFactory.Token(SyntaxKind.NewKeyword)); + + if (modifiers.IsSealed) + list.Add(SyntaxFactory.Token(SyntaxKind.SealedKeyword)); + + if (modifiers.IsOverride) + list.Add(SyntaxFactory.Token(SyntaxKind.OverrideKeyword)); + + if (modifiers.IsVirtual) + list.Add(SyntaxFactory.Token(SyntaxKind.VirtualKeyword)); + + if (modifiers.IsStatic) + list.Add(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + + if (modifiers.IsAsync) + list.Add(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)); + + if (modifiers.IsConst) + list.Add(SyntaxFactory.Token(SyntaxKind.ConstKeyword)); + + if (modifiers.IsReadOnly) + list.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)); + + if (modifiers.IsUnsafe) + list.Add(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); + + if (modifiers.IsVolatile) + list.Add(SyntaxFactory.Token(SyntaxKind.VolatileKeyword)); + + if (modifiers.IsExtern) + list.Add(SyntaxFactory.Token(SyntaxKind.ExternKeyword)); + + if (modifiers.IsRequired) + list.Add(SyntaxFactory.Token(SyntaxKind.RequiredKeyword)); + + // partial and ref must be last + if (modifiers.IsRef) + list.Add(SyntaxFactory.Token(SyntaxKind.RefKeyword)); + + if (modifiers.IsPartial) + list.Add(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + + // Modified + list = list.Select(x => x.WithTrailingTrivia(SyntaxFactory.Space)).ToList(); + + return SyntaxFactory.TokenList(list); + } + + private static SyntaxNode Isolate(SyntaxNode declaration, Func editor) + => PreserveTrivia(AsIsolatedDeclaration(declaration), editor); + + private static SyntaxNode AsIsolatedDeclaration(SyntaxNode declaration) + { + switch (declaration.Kind()) + { + case SyntaxKind.VariableDeclaration: + var vd = (VariableDeclarationSyntax)declaration; + if (vd.Parent != null && vd.Variables.Count == 1) + { + return AsIsolatedDeclaration(vd.Parent); + } + + break; + + case SyntaxKind.VariableDeclarator: + var v = (VariableDeclaratorSyntax)declaration; + if (v.Parent != null && v.Parent.Parent != null) + { + return ClearTrivia(WithVariable(v.Parent.Parent, v)); + } + + break; + + case SyntaxKind.Attribute: + var attr = (AttributeSyntax)declaration; + if (attr.Parent != null) + { + var attrList = (AttributeListSyntax)attr.Parent; + return attrList.WithAttributes(SyntaxFactory.SingletonSeparatedList(attr)).WithTarget(null); + } + + break; + } + + return declaration; + } + + private static SyntaxNode WithVariable(SyntaxNode declaration, VariableDeclaratorSyntax variable) + { + var vd = GetVariableDeclaration(declaration); + if (vd != null) + { + return WithVariableDeclaration(declaration, vd.WithVariables(SyntaxFactory.SingletonSeparatedList(variable))); + } + + return declaration; + } + + private static VariableDeclarationSyntax? GetVariableDeclaration(SyntaxNode declaration) + => declaration.Kind() switch + { + SyntaxKind.FieldDeclaration => ((FieldDeclarationSyntax)declaration).Declaration, + SyntaxKind.EventFieldDeclaration => ((EventFieldDeclarationSyntax)declaration).Declaration, + SyntaxKind.LocalDeclarationStatement => ((LocalDeclarationStatementSyntax)declaration).Declaration, + _ => null, + }; + + private static SyntaxNode WithVariableDeclaration(SyntaxNode declaration, VariableDeclarationSyntax variables) + => declaration.Kind() switch + { + SyntaxKind.FieldDeclaration => ((FieldDeclarationSyntax)declaration).WithDeclaration(variables), + SyntaxKind.EventFieldDeclaration => ((EventFieldDeclarationSyntax)declaration).WithDeclaration(variables), + SyntaxKind.LocalDeclarationStatement => ((LocalDeclarationStatementSyntax)declaration).WithDeclaration(variables), + _ => declaration, + }; + + public static TNode ClearTrivia(TNode node) where TNode : SyntaxNode + { + if (node != null) + { + return node.WithLeadingTrivia(SyntaxFactory.ElasticMarker) + .WithTrailingTrivia(SyntaxFactory.ElasticMarker); + } + else + { + return null; + } + } + + private static SyntaxNode? PreserveTrivia(TNode? node, Func nodeChanger) where TNode : SyntaxNode + { + if (node == null) + { + return node; + } + + var nodeWithoutTrivia = node.WithoutLeadingTrivia().WithoutTrailingTrivia(); + + var changedNode = nodeChanger(nodeWithoutTrivia); + if (changedNode == nodeWithoutTrivia) + { + return node; + } + + return changedNode + .WithLeadingTrivia(node.GetLeadingTrivia().Concat(changedNode.GetLeadingTrivia())) + .WithTrailingTrivia(changedNode.GetTrailingTrivia().Concat(node.GetTrailingTrivia())); + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/RoslynCleaner.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/RoslynCleaner.cs new file mode 100644 index 00000000..cd5e779c --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/RoslynCleaner.cs @@ -0,0 +1,65 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.VisualStudio.Shell; +using SteveCadwallader.CodeMaid.Logic.Cleaning; +using System; + +namespace CodeMaidShared.Logic.Cleaning +{ + internal class RoslynCleaner : CSharpSyntaxRewriter + { + public RoslynCleaner() + { + UpdateNodePipeline = (x, _) => base.Visit(x); + UpdateTokenPipeline = (x, _) => base.VisitToken(x); + } + + private Func UpdateNodePipeline { get; set; } + private Func UpdateTokenPipeline { get; set; } + + public override SyntaxNode Visit(SyntaxNode original) + { + if (original == null) + { + return original; + } + var newNode = original; + + return UpdateNodePipeline(original, newNode); + } + + public override SyntaxToken VisitToken(SyntaxToken token) + { + if (token == null) + { + return token; + } + var newToken = token; + + return UpdateTokenPipeline(token, newToken); + } + + public SyntaxNode Process(SyntaxNode root, Workspace workspace) + { + var rewrite = Visit(root); + return rewrite; + + //return Formatter.Format(rewrite, SyntaxAnnotation.ElasticAnnotation, workspace); + } + + public void AddNodeMiddleware(IRoslynNodeMiddleware middleware) + { + middleware.SetNodeDelegate(UpdateNodePipeline); + + UpdateNodePipeline = middleware.Invoke; + } + + public void AddTokenMiddleware(IRoslynTokenMiddleware middleware) + { + middleware.SetTokenDelegate(UpdateTokenPipeline); + + UpdateTokenPipeline = middleware.Invoke; + } + } +} \ No newline at end of file diff --git a/CodeMaidShared/Logic/Cleaning/Roslyn/RoslynExtensions.cs b/CodeMaidShared/Logic/Cleaning/Roslyn/RoslynExtensions.cs new file mode 100644 index 00000000..a0336020 --- /dev/null +++ b/CodeMaidShared/Logic/Cleaning/Roslyn/RoslynExtensions.cs @@ -0,0 +1,165 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SteveCadwallader.CodeMaid.Logic.Cleaning +{ + internal static class RoslynExtensions + { + // TODO Refactor + public static (List<(SyntaxKind, int)>, int) ReadTriviaLines(List trivia) + { + var output = new List<(SyntaxKind, int)>(); + var lineType = SyntaxKind.WhitespaceTrivia; + var position = 0; + + for (int i = 0; i < trivia.Count; i++) + { + var value = trivia[i]; + if (value.IsKind(SyntaxKind.EndOfLineTrivia)) + { + output.Add((lineType, position)); + position = i + 1; + lineType = SyntaxKind.WhitespaceTrivia; + } + else if (value.IsKind(SyntaxKind.WhitespaceTrivia)) + { + } + else if (value.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.SingleLineDocumentationCommentTrivia) + { + if (lineType == SyntaxKind.WhitespaceTrivia) + { + lineType = value.Kind(); + } + } + else if (value.Kind() is SyntaxKind.RegionDirectiveTrivia or SyntaxKind.EndRegionDirectiveTrivia) + { + if (lineType == SyntaxKind.WhitespaceTrivia) + { + lineType = value.Kind(); + output.Add((lineType, position)); + position = i + 1; + lineType = SyntaxKind.WhitespaceTrivia; + } + } + else + { + lineType = SyntaxKind.BadDirectiveTrivia; + } + } + + return (output, position); + } + + public static bool SpansMultipleLines(this SyntaxNode node) + { + var startLine = node.SyntaxTree.GetLineSpan(node.Span).StartLinePosition.Line; + var endLine = node.SyntaxTree.GetLineSpan(node.Span).EndLinePosition.Line; + + return startLine != endLine; + } + + // TODO use GetRequiredSemanticModelAsync + public static async ValueTask GetRequiredSemanticModelAsync(this Document document, CancellationToken cancellationToken) + { + if (document.TryGetSemanticModel(out var semanticModel)) + return semanticModel; + + semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return semanticModel ?? throw new InvalidOperationException($"Syntax tree is required to accomplish the task but is not supported by document {document.Name}"); + } + + public static SyntaxNode GetRequiredParent(this SyntaxNode node) + => node.Parent ?? throw new InvalidOperationException("Node's parent was null"); + + public static SyntaxToken GetNameToken(this MemberDeclarationSyntax member) + { + if (member != null) + { + switch (member.Kind()) + { + case SyntaxKind.EnumDeclaration: + return ((EnumDeclarationSyntax)member).Identifier; + + case SyntaxKind.ClassDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordStructDeclaration: + return ((TypeDeclarationSyntax)member).Identifier; + + case SyntaxKind.DelegateDeclaration: + return ((DelegateDeclarationSyntax)member).Identifier; + + case SyntaxKind.FieldDeclaration: + return ((FieldDeclarationSyntax)member).Declaration.Variables.First().Identifier; + + case SyntaxKind.EventFieldDeclaration: + return ((EventFieldDeclarationSyntax)member).Declaration.Variables.First().Identifier; + + case SyntaxKind.PropertyDeclaration: + return ((PropertyDeclarationSyntax)member).Identifier; + + case SyntaxKind.EventDeclaration: + return ((EventDeclarationSyntax)member).Identifier; + + case SyntaxKind.MethodDeclaration: + return ((MethodDeclarationSyntax)member).Identifier; + + case SyntaxKind.ConstructorDeclaration: + return ((ConstructorDeclarationSyntax)member).Identifier; + + case SyntaxKind.DestructorDeclaration: + return ((DestructorDeclarationSyntax)member).Identifier; + + case SyntaxKind.IndexerDeclaration: + return ((IndexerDeclarationSyntax)member).ThisKeyword; + + case SyntaxKind.OperatorDeclaration: + return ((OperatorDeclarationSyntax)member).OperatorToken; + } + } + + // Conversion operators don't have names. + return default; + } + } + + internal static class AddAccessibilityModifiersHelpers + { + internal static Accessibility GetPreferredAccessibility(ISymbol symbol) + { + // If we have an overridden member, then if we're adding an accessibility modifier, use the + // accessibility of the member we're overriding as both should be consistent here. + if (symbol.GetOverriddenMember() is { DeclaredAccessibility: var accessibility }) + return accessibility; + + // Default abstract members to be protected, and virtual members to be public. They can't be private as + // that's not legal. And these are reasonable default values for them. + if (symbol is IMethodSymbol or IPropertySymbol or IEventSymbol) + { + if (symbol.IsAbstract) + return Accessibility.Protected; + + if (symbol.IsVirtual) + return Accessibility.Public; + } + + // Otherwise, default to whatever accessibility no-accessibility means for this member; + return symbol.DeclaredAccessibility; + } + + public static ISymbol? GetOverriddenMember(this ISymbol? symbol) + => symbol switch + { + IMethodSymbol method => method.OverriddenMethod, + IPropertySymbol property => property.OverriddenProperty, + IEventSymbol @event => @event.OverriddenEvent, + _ => null, + }; + } +} \ No newline at end of file