diff --git a/TidyCSharp.Cli/CliOptions.cs b/TidyCSharp.Cli/CliOptions.cs new file mode 100644 index 0000000..7702db9 --- /dev/null +++ b/TidyCSharp.Cli/CliOptions.cs @@ -0,0 +1,15 @@ +using CommandLine; + +namespace TidyCSharp.Cli; + +public class CliOptions +{ + [Option('p', "path", Required = true, HelpText = "Solution path.")] + public string SolutionPath { get; set; } + + [Option('o', "output", Required = true, HelpText = "Output path.")] + public string OutputPath { get; set; } + + [Option('m', "mode", Required = true, HelpText = "Readonly(r) or RunSafeRules(w) mode.")] + public string Mode { get; set; } +} diff --git a/TidyCSharp.Cli/CommandConstants/GuidList.cs b/TidyCSharp.Cli/CommandConstants/GuidList.cs new file mode 100644 index 0000000..b808968 --- /dev/null +++ b/TidyCSharp.Cli/CommandConstants/GuidList.cs @@ -0,0 +1,14 @@ +// Guids.cs +// MUST match guids.h + +namespace TidyCSharp.Cli.CommandConstants; + +internal static class GuidList +{ + public const string GuidGeeksProductivityToolsPkgString = "c6176957-c61c-4beb-8dd8-e7c0170b0bf3"; + + private const string GuidCleanupCmdSetString = "53366ba1-1788-42c8-922a-034d6dc89b12"; + // + + public static readonly Guid GuidCleanupCmdSet = new(GuidCleanupCmdSetString); +}; \ No newline at end of file diff --git a/TidyCSharp.Cli/CommandConstants/PkgCmdIDList.cs b/TidyCSharp.Cli/CommandConstants/PkgCmdIDList.cs new file mode 100644 index 0000000..592b73c --- /dev/null +++ b/TidyCSharp.Cli/CommandConstants/PkgCmdIDList.cs @@ -0,0 +1,9 @@ +// PkgCmdID.cs +// MUST match PkgCmdID.h + +namespace TidyCSharp.Cli.CommandConstants; + +internal static class PkgCmdIdList +{ + public const uint CmdCustomUpAllActions = 0x0138; +}; \ No newline at end of file diff --git a/TidyCSharp.Cli/Definition/UsingsCommands.cs b/TidyCSharp.Cli/Definition/UsingsCommands.cs new file mode 100644 index 0000000..eef7ea2 --- /dev/null +++ b/TidyCSharp.Cli/Definition/UsingsCommands.cs @@ -0,0 +1,16 @@ +namespace TidyCSharp.Cli.Definition; + +public static class UsingsCommands +{ + public const string RemoveAndSortVs15 = "EditorContextMenus.CodeWindow.OrganizeUsings.RemoveAndSort"; + + public const string RemoveAndSortVs17 = "EditorContextMenus.CodeWindow.RemoveAndSort"; + + public const string RemoveAndSortCommandName = "Edit.RemoveAndSort"; + + public const string SystemNamespace = "using System;"; + + public const string LinqNamespace = "using System.Linq;"; + + public const string GenericNamespace = "using System.Collections.Generic;"; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Environment/App.cs b/TidyCSharp.Cli/Environment/App.cs new file mode 100644 index 0000000..dc7927f --- /dev/null +++ b/TidyCSharp.Cli/Environment/App.cs @@ -0,0 +1,15 @@ +namespace TidyCSharp.Cli.Environment; + +public static class App +{ + public static void Initialize() + { + AppDomain.CurrentDomain.UnhandledException += GlobalExceptionHandlerEvent; + } + + private static void GlobalExceptionHandlerEvent(object sender, UnhandledExceptionEventArgs args) + { + ErrorNotification.ErrorNotification.WriteErrorToFile((Exception)args.ExceptionObject); + ErrorNotification.ErrorNotification.WriteErrorToOutputWindow((Exception)args.ExceptionObject); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Environment/ErrorList.cs b/TidyCSharp.Cli/Environment/ErrorList.cs new file mode 100644 index 0000000..253a3bd --- /dev/null +++ b/TidyCSharp.Cli/Environment/ErrorList.cs @@ -0,0 +1,76 @@ +namespace TidyCSharp.Cli.Environment; + +public static class ErrorList +{ + //static ErrorListProvider _errorListProvider; + // static Dictionary ListOfErrors = new Dictionary(); + + // public static void AddOrOverrideError(string key, Microsoft.VisualStudio.Shell.Task task) + // { + // if (ListOfErrors.ContainsKey(key)) + // { + // ListOfErrors[key] = task; + // } + // else + // { + // ListOfErrors.Add(key, task); + // } + + // if (task is ErrorTask) + // { + // WriteVisualStudioErrorList(task as ErrorTask); + // } + // } + + // public static void RemoveError(string key) + // { + // if (string.IsNullOrEmpty(key)) + // return; + + // if (ListOfErrors.ContainsKey(key)) + // { + // var task = ListOfErrors[key]; + // if (task == null) + // return; + + // if (task is ErrorTask) + // { + // _errorListProvider.Tasks.Remove(task); + // } + + // ListOfErrors.Remove(key); + // } + // } + + /// + /// Write an entry to the Visual Studio Error List. + /// + // static void WriteVisualStudioErrorList(ErrorTask errorTask) + // { + // if (_errorListProvider == null) + // { + // _errorListProvider = new ErrorListProvider(TidyCSharpPackage.Instance); + // } + + // // Check if this error is already in the error list, don't report more than once + // var alreadyReported = false; + // foreach (ErrorTask task in _errorListProvider.Tasks) + // { + // if (task.ErrorCategory == errorTask.ErrorCategory && + // task.Document == errorTask.Document && + // task.Line == errorTask.Line && + // task.Column == errorTask.Column && + // task.Text == errorTask.Text) + // { + // alreadyReported = true; + // break; + // } + // } + + // if (!alreadyReported) + // { + // // Add error to task list + // _errorListProvider.Tasks.Add(errorTask); + // } + // } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/ErrorNotification/ErrorNotification.cs b/TidyCSharp.Cli/ErrorNotification/ErrorNotification.cs new file mode 100644 index 0000000..86208a8 --- /dev/null +++ b/TidyCSharp.Cli/ErrorNotification/ErrorNotification.cs @@ -0,0 +1,23 @@ +namespace TidyCSharp.Cli.ErrorNotification; +internal static class ErrorNotification +{ + internal static void EmailError(string message) + { + + } + + internal static void WriteErrorToOutputWindow(Exception ex, string itemAddress = null) + { + + } + + internal static void WriteErrorToFile(Exception ex, string itemAddress = null) + { + File.AppendAllText(Path.GetDirectoryName(TidyCSharpPackage.Instance.Solution.FilePath) + "\\Tidy.Error.log", + Newtonsoft.Json.JsonConvert.SerializeObject(new + { + FileAddress = itemAddress, + Exception = ex, + })); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Extensions/Extensions.cs b/TidyCSharp.Cli/Extensions/Extensions.cs new file mode 100644 index 0000000..3392223 --- /dev/null +++ b/TidyCSharp.Cli/Extensions/Extensions.cs @@ -0,0 +1,297 @@ +namespace TidyCSharp.Cli.Extensions; + +public static class Extensions +{ + public static bool HasValue(this string text) => !string.IsNullOrEmpty(text); + + public static string Or(this string text, string ifEmpty) => text.HasValue() ? text : ifEmpty; + + public static string TrimStart(this string text, string textToTrim, bool ignoreCase = false) + { + if (text != null && text.StartsWith(textToTrim, ignoreCase, System.Globalization.CultureInfo.InvariantCulture)) + { + return text.Substring(textToTrim.Length); + } + else + { + return text; + } + } + + public static string TrimEnd(this string text, string textToTrim) + { + if (text != null && text.EndsWith(textToTrim)) + { + return text.TrimEnd(textToTrim.Length); + } + else + { + return text; + } + } + + public static string TrimEnd(this string text, int numberOfCharacters) + { + if (numberOfCharacters < 0) + throw new ArgumentException("numberOfCharacters must be greater than 0."); + + if (numberOfCharacters == 0) return text; + + if (text.IsEmpty() || text.Length <= numberOfCharacters) + return string.Empty; + + return text.Substring(0, text.Length - numberOfCharacters); + } + + public static bool IsEmpty(this string text) => string.IsNullOrEmpty(text); + + public static string Remove(this string text, params string[] substringsToExclude) + { + if (text.IsEmpty()) return text; + + var result = text; + + if (substringsToExclude != null) + foreach (var sub in substringsToExclude) + result = result.Replace(sub, ""); + + return result; + } + + public static bool EndsWithAny(this string input, params string[] listOfEndings) + { + if (listOfEndings != null) + foreach (var option in listOfEndings) + if (input.EndsWith(option)) return true; + + return false; + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static TK Get(this T item, Func selector) + { + if (ReferenceEquals(item, null)) + return default(TK); + return (selector != null) ? selector(item) : default(TK); + //else + //{ + // try + // { + // return selector(item); + // } + // catch (NullReferenceException) + // { + // return default(K); + // } + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static Guid? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + return (selector != null) ? selector(item) : null as Guid?; + //try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static int? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + return (selector != null) ? selector(item) : null as int?; + //try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static double? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + return (selector != null) ? selector(item) : null as double?; + //try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static decimal? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + return (selector != null) ? selector(item) : null as decimal?; + //try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static bool? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + return (selector != null) ? selector(item) : null as bool?; + //try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static string Get(this DateTime? item, Func selector) + { + if (item == null) return null; + return (selector != null) ? selector(item) : null; + //try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static byte? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + return (selector != null) ? selector(item) : null as byte?; + // try + //{ + // return selector(item); + //} + //catch (NullReferenceException) + //{ + // return null; + //} + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static DateTime? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + + try + { + return selector(item); + } + catch (NullReferenceException) + { + return null; + } + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static DateTime? Get(this T item, Func selector) where T : class + { + if (item == null) return null; + + try + { + return selector(item); + } + catch (NullReferenceException) + { + return null; + } + } + + /// + /// Gets a specified member of this object. If this is null, null will be returned. Otherwise the specified expression will be returned. + /// + public static T Get(this DateTime? item, Func selector) where T : struct + { + if (item == null) return default(T); + + try + { + return selector(item); + } + catch (NullReferenceException) + { + return default(T); + } + } + + public static string Replace(this string str, string oldValue, string newValue, bool caseSensitive) + { + if (caseSensitive) + return str.Replace(oldValue, newValue); + + var prevPos = 0; + var retval = str; + var pos = retval.IndexOf(oldValue, StringComparison.InvariantCultureIgnoreCase); + + while (pos > -1) + { + retval = str.Remove(pos, oldValue.Length); + retval = retval.Insert(pos, newValue); + prevPos = pos + newValue.Length; + pos = retval.IndexOf(oldValue, prevPos, StringComparison.InvariantCultureIgnoreCase); + } + + return retval; + } + + private const string SingleQuote = "'", DoubleQuote = "\""; + + public static string StripQuotation(this string str) + { + if (str.StartsWith(SingleQuote)) + return str + .TrimStart(SingleQuote) + .TrimEnd(SingleQuote); + + if (str.StartsWith(DoubleQuote)) + return str + .TrimStart(DoubleQuote) + .TrimEnd(DoubleQuote); + + return str; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Extensions/FileExtensions.cs b/TidyCSharp.Cli/Extensions/FileExtensions.cs new file mode 100644 index 0000000..67ffa0d --- /dev/null +++ b/TidyCSharp.Cli/Extensions/FileExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Extensions; + +public static class FileExtensions +{ + public static bool IsCsharpFile(this Document projectItem) + { + return projectItem.SourceCodeKind == SourceCodeKind.Regular && projectItem.Name.EndsWith(".cs"); + } + + public static bool IsCSharpDesignerFile(this Document projectItem) + { + return projectItem.SourceCodeKind == SourceCodeKind.Regular + && projectItem.Name.EndsWith(".designer.cs", StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionCSharpOnProjectItem.cs b/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionCSharpOnProjectItem.cs new file mode 100644 index 0000000..7cb59bc --- /dev/null +++ b/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionCSharpOnProjectItem.cs @@ -0,0 +1,22 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; +using static TidyCSharp.Cli.Menus.ActionsOnCSharp.CSharpAction; + +namespace TidyCSharp.Cli.Menus.ActionsOnCSharp; + +public class ActionCSharpOnProjectItem +{ + public static async Task ActionAsync(Document item, TargetAction targetAction, CleanupOptions cleanupOptions) + { + await targetAction(item, cleanupOptions); + + var linkedDocumentIds = item.GetLinkedDocumentIds().ToList(); + + if (linkedDocumentIds == null) return; + + foreach (var linkedDocumentId in linkedDocumentIds) + { + await ActionAsync(item.Project.Documents.Single(a => a.Id == linkedDocumentId), targetAction, cleanupOptions); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionCSharpOnSolution.cs b/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionCSharpOnSolution.cs new file mode 100644 index 0000000..fb3a088 --- /dev/null +++ b/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionCSharpOnSolution.cs @@ -0,0 +1,32 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; +using TidyCSharp.Cli.Utility; +using static TidyCSharp.Cli.Menus.ActionsOnCSharp.CSharpAction; + +namespace TidyCSharp.Cli.Menus.ActionsOnCSharp; + +public class ActionCSharpOnSolution +{ + public static async Task InvokeAsync(TargetAction action, CleanupOptions cleanupOptions) + { + try + { + var projects = SolutionActions.FindProjects(); + + for (var i = 0; i < projects.Count; i++) + { + var currentProject = projects[i]; + if (currentProject.Documents == null) continue; + if (currentProject.FilePath.EndsWith(".shproj", StringComparison.OrdinalIgnoreCase)) continue; + + foreach (var document in currentProject.Documents) + await ActionCSharpOnProjectItem.ActionAsync(document, action, cleanupOptions); + } + } + catch (Exception e) + { + ErrorNotification.ErrorNotification.WriteErrorToFile(e); + ErrorNotification.ErrorNotification.WriteErrorToOutputWindow(e); + ProcessActions.GeeksProductivityToolsProcess(); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionsCSharpOnFile.cs b/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionsCSharpOnFile.cs new file mode 100644 index 0000000..ddf0032 --- /dev/null +++ b/TidyCSharp.Cli/Menus/ActionsOnCSharp/ActionsCSharpOnFile.cs @@ -0,0 +1,157 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Extensions; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using TidyCSharp.Cli.Utility; + +namespace TidyCSharp.Cli.Menus.ActionsOnCSharp; + +public class ActionsCSharpOnFile +{ + public static async Task DoCleanupAsync(Document item, CleanupOptions cleanupOptions) + { + if (!item.IsCsharpFile() || item.IsCSharpDesignerFile()) return; + + try + { + var path = item.FilePath; + if (path.EndsWithAny(new[] { "AssemblyInfo.cs", "TheApplication.cs" })) return; + + var documentText = item.ToSyntaxNode().GetText().ToString(); + if (documentText.Contains("[EscapeGCop(\"Auto generated code.\")]")) return; + + if (item.ToSyntaxNode() + .DescendantNodesOfType() + .Any(x => x.Name.ToString() == "EscapeGCop" && + x.ArgumentList != null && + x.ArgumentList.Arguments.FirstOrDefault().ToString() + == "\"Auto generated code.\"")) + { + return; + } + + foreach (var actionTypeItem in cleanupOptions.ActionTypes) + { + if (actionTypeItem == CodeCleanerType.NormalizeWhiteSpaces) continue; + if (actionTypeItem == CodeCleanerType.OrganizeUsingDirectives) continue; + if (actionTypeItem == CodeCleanerType.ConvertMsharpGeneralMethods) continue; + + await CodeCleanerHost.RunAsync(item, actionTypeItem, cleanupOptions); + } + + if (cleanupOptions.ActionTypes.Contains(CodeCleanerType.NormalizeWhiteSpaces)) + { + await CodeCleanerHost.RunAsync(item, CodeCleanerType.NormalizeWhiteSpaces, cleanupOptions); + } + + if (cleanupOptions.ActionTypes.Contains(CodeCleanerType.OrganizeUsingDirectives)) + { + await CodeCleanerHost.RunAsync(item, CodeCleanerType.OrganizeUsingDirectives, cleanupOptions); + } + else + { + //TODO : item.Save(); + } + + if (cleanupOptions.ActionTypes.Contains(CodeCleanerType.ConvertMsharpGeneralMethods)) + { + await CodeCleanerHost.RunAsync(item, CodeCleanerType.ConvertMsharpGeneralMethods, cleanupOptions); + } + } + catch (Exception e) + { + ErrorNotification.ErrorNotification.WriteErrorToFile(e, item.FilePath); + ErrorNotification.ErrorNotification.WriteErrorToOutputWindow(e, item.FilePath); + ProcessActions.GeeksProductivityToolsProcess(); + } + } + + public static async Task ReportOnlyDoNotCleanupAsync(Document item, CleanupOptions cleanupOptions) + { + if (!item.IsCsharpFile() || item.IsCSharpDesignerFile()) return; + + try + { + // Sometimes cannot find document's file + try + { + var documentText = item.ToSyntaxNode().SyntaxTree.GetText().ToString(); + if (documentText.Contains("[EscapeGCop(\"Auto generated code.\")]")) return; + } + catch + { + return; + } + + var path = item.FilePath; + if (path.EndsWithAny(new[] { "AssemblyInfo.cs", "TheApplication.cs" })) return; + + using (var tidyruntimelog = new StreamWriter(Path.Combine(Path.GetTempPath(), "TidyCurrentfilelog.txt"), true)) + tidyruntimelog.WriteLine(path); + + + foreach (var actionTypeItem in cleanupOptions.ActionTypes) + { + if (actionTypeItem != CodeCleanerType.NormalizeWhiteSpaces + && actionTypeItem != CodeCleanerType.OrganizeUsingDirectives + && actionTypeItem != CodeCleanerType.ConvertMsharpGeneralMethods) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + await CodeCleanerHost.RunAsync(item, actionTypeItem, cleanupOptions, true); + watch.Stop(); + + using (var tidyruntimelog = new StreamWriter(Path.Combine(Path.GetTempPath(), "TidyCurrentActionslog.txt"), true)) + { + tidyruntimelog.WriteLine("Phase1-" + actionTypeItem.ToString() + "-" + watch.ElapsedMilliseconds + " ms"); + } + } + } + + if (cleanupOptions.ActionTypes.Contains(CodeCleanerType.NormalizeWhiteSpaces)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + await CodeCleanerHost.RunAsync(item, CodeCleanerType.NormalizeWhiteSpaces, cleanupOptions, true); + watch.Stop(); + + using (var tidyruntimelog = new StreamWriter(Path.Combine(Path.GetTempPath(), "TidyCurrentActionslog.txt"), true)) + { + tidyruntimelog.WriteLine("Phase2-" + "NormalizeWhiteSpaces" + "-" + watch.ElapsedMilliseconds + " ms"); + } + } + + if (cleanupOptions.ActionTypes.Contains(CodeCleanerType.OrganizeUsingDirectives)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + await CodeCleanerHost.RunAsync(item, CodeCleanerType.OrganizeUsingDirectives, cleanupOptions, true); + watch.Stop(); + + using (var tidyruntimelog = new StreamWriter(Path.Combine(Path.GetTempPath(), "TidyCurrentActionslog.txt"), true)) + { + tidyruntimelog.WriteLine("Phase3-" + "OrganizeUsingDirectives" + "-" + watch.ElapsedMilliseconds + " ms"); + } + } + + if (cleanupOptions.ActionTypes.Contains(CodeCleanerType.ConvertMsharpGeneralMethods)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + await CodeCleanerHost.RunAsync(item, CodeCleanerType.ConvertMsharpGeneralMethods, cleanupOptions, true); + watch.Stop(); + + using (var tidyruntimelog = new StreamWriter(Path.Combine(Path.GetTempPath(), "TidyCurrentActionslog.txt"), true)) + { + tidyruntimelog.WriteLine("Phase4-" + "ConvertMsharpGeneralMethods" + "-" + watch.ElapsedMilliseconds + " ms"); + } + } + } + catch (Exception e) + { + ErrorNotification.ErrorNotification.WriteErrorToFile(e, item.FilePath); + ErrorNotification.ErrorNotification.WriteErrorToOutputWindow(e, item.FilePath); + ProcessActions.GeeksProductivityToolsProcess(); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/ActionsOnCSharp/CSharpAction.cs b/TidyCSharp.Cli/Menus/ActionsOnCSharp/CSharpAction.cs new file mode 100644 index 0000000..7aab18b --- /dev/null +++ b/TidyCSharp.Cli/Menus/ActionsOnCSharp/CSharpAction.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; + +namespace TidyCSharp.Cli.Menus.ActionsOnCSharp; + +public class CSharpAction +{ + public delegate Task TargetAction(Document item, CleanupOptions cleanupOptions); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CSharpSyntaxUpgrade.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CSharpSyntaxUpgrade.cs new file mode 100644 index 0000000..a0125b3 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CSharpSyntaxUpgrade.cs @@ -0,0 +1,165 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class CSharpSyntaxUpgrade : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return ChangeMethodHelper(initialSourceNode); + } + + private SyntaxNode ChangeMethodHelper(SyntaxNode initialSourceNode) + { + if (initialSourceNode is null) return null; + + if (ProjectItemDetails.SemanticModel is null) return null; + + var syntaxRewriter = new NewExpressionRewriter(ProjectItemDetails.SemanticModel + , IsReportOnlyMode, Options); + + if (syntaxRewriter is null) return null; + + var modifiedSyntaxNode = syntaxRewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(syntaxRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSyntaxNode; + } + + private class NewExpressionRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public NewExpressionRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) + : base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitVariableDeclaration(VariableDeclarationSyntax node) + { + if (node.Type.IsVar) return node; + return base.VisitVariableDeclaration(node); + } + + public override SyntaxNode VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) + { + if (((CSharpCompilation)_semanticModel.Compilation).LanguageVersion.MapSpecifiedToEffectiveVersion() != LanguageVersion.CSharp9) + return base.VisitObjectCreationExpression(node); + + if (node.NewKeyword == null + || node.Parent.IsKind(SyntaxKind.LocalDeclarationStatement) + || node.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression) + || node.Parent.IsKind(SyntaxKind.UsingStatement)) + return base.VisitObjectCreationExpression(node); + + //if (node.Parent.IsKind(SyntaxKind.LocalDeclarationStatement)) + // return base.VisitObjectCreationExpression(node); + + //if (node.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + // return base.VisitObjectCreationExpression(node); + + //if (node.Parent.IsKind(SyntaxKind.UsingStatement)) + // return base.VisitObjectCreationExpression(node); + + var newNode = node.WithType(SyntaxFactory.ParseTypeName("")) + .WithNewKeyword(node.NewKeyword.WithoutWhiteSpaceTrivia()) + .WithArgumentList(node.ArgumentList ?? SyntaxFactory.ParseArgumentList("()")); + + var nodeTypeinfo = CSharpExtensions.GetTypeInfo(_semanticModel, node); + var parentSymbol = _semanticModel.GetSymbolInfo(node.Parent).Symbol; + + if (parentSymbol?.Kind == SymbolKind.Method && + (parentSymbol as IMethodSymbol)?.MethodKind == MethodKind.AnonymousFunction) + return base.VisitObjectCreationExpression(node); + + if (node.Parent.IsKind(SyntaxKind.ReturnStatement)) + { + var methodDeclaration = node.FirstAncestorOrSelf(); + var propertyDeclaration = node.FirstAncestorOrSelf(); + + if (methodDeclaration != null) + { + if (methodDeclaration.ReturnType.ToString() != nodeTypeinfo.Type.Name) + return base.VisitObjectCreationExpression(node); + + var symbol = CSharpExtensions.GetDeclaredSymbol(_semanticModel, methodDeclaration); + + if (symbol.ReturnType.TypeKind != TypeKind.Class) + return base.VisitObjectCreationExpression(node); + } + else if (propertyDeclaration != null) + { + var symbol = CSharpExtensions.GetDeclaredSymbol(_semanticModel, propertyDeclaration); + + if (symbol.GetMethod.ReturnType.TypeKind != TypeKind.Class) + return base.VisitObjectCreationExpression(node); + } + } + else if (node.Parent.IsKind(SyntaxKind.Argument)) + { + var methodInvocation = node.FirstAncestorOrSelf(); + + if (methodInvocation != null) + { + var methodSymbol = CSharpExtensions.GetSymbolInfo(_semanticModel, methodInvocation).Symbol; + + var countofMethod = methodSymbol?.ContainingType?.GetMembers() + .Count(x => x.Name == + (methodInvocation.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) ? + methodInvocation.GetRightSideNameSyntax().ToString() : + methodInvocation.Expression.ToString())); + + if (countofMethod > 1) + return base.VisitObjectCreationExpression(node); + + var indexOfMethod = node.FirstAncestorOrSelf().Arguments + .IndexOf(node.AncestorsAndSelf().FirstOrDefault(x => x.Parent.IsKind(SyntaxKind.ArgumentList)) as ArgumentSyntax); + + if ((methodSymbol as IMethodSymbol)?.OriginalDefinition.Parameters[indexOfMethod].IsParams == true) + return base.VisitObjectCreationExpression(node); + + if ((methodSymbol as IMethodSymbol)?.OriginalDefinition.IsGenericMethod == true && + (methodInvocation.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) ? + !methodInvocation.GetRightSideNameSyntax().IsKind(SyntaxKind.GenericName) : + !methodInvocation.Expression.IsKind(SyntaxKind.GenericName))) + return base.VisitObjectCreationExpression(node); + } + else + return base.VisitObjectCreationExpression(node); + } + else if (node.Parent.IsKind(SyntaxKind.ArrayInitializerExpression)) + { + if (node.Parent.Parent.IsKind(SyntaxKind.ImplicitArrayCreationExpression)) + return base.VisitObjectCreationExpression(node); + } + + if (nodeTypeinfo.ConvertedType.ToString() == nodeTypeinfo.Type.ToString()) + { + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Object Creation new Syntax in c# v9", + Generator = nameof(CSharpSyntaxUpgrade) + }); + } + + return newNode; + } + + return base.VisitObjectCreationExpression(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/CamelCasedFields.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/CamelCasedFields.cs new file mode 100644 index 0000000..c81d206 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/CamelCasedFields.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields; + +public class CamelCasedFields : VariableRenamingBase +{ + protected override SyntaxNode GetWorkingNode(SyntaxNode initialSourceNode, SyntaxAnnotation annotationForSelectedNodes) + { + return + initialSourceNode + .DescendantNodes() + .OfType() + .FirstOrDefault(m => !m.HasAnnotation(annotationForSelectedNodes)); + } + + protected override VariableRenamingBaseRewriter GetRewriter(Document workingDocument) + { + return new Rewriter(workingDocument, IsReportOnlyMode, Options); + } + + private class Rewriter : VariableRenamingBaseRewriter + { + public Rewriter(Document workingDocument, bool isReportOnlyMode, ICleanupOption options) : base(workingDocument, isReportOnlyMode, options) { } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + ClassDeclarationSyntax classDeclarationSyntax = null; + + Task.Run(async delegate + { + classDeclarationSyntax = await RenameDeclarationsAsync(node); + }).GetAwaiter().GetResult(); + + return base.VisitClassDeclaration(classDeclarationSyntax as ClassDeclarationSyntax); + } + + private async Task RenameDeclarationsAsync(ClassDeclarationSyntax classNode) + { + if (CheckOption((int)CleanupTypes.NormalFields)) + { + var renamingResult = await new FieldRenamer(WorkingDocument).RenameDeclarationsAsync(classNode); + + if (renamingResult != null && renamingResult.Node != null) + { + classNode = renamingResult.Node as ClassDeclarationSyntax; + WorkingDocument = renamingResult.Document; + } + + if (renamingResult != null) + { + var lineSpan = classNode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(classNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Camel Cased Fields", + Generator = nameof(CamelCasedFields), + }); + } + } + + if (CheckOption((int)CleanupTypes.ConstFields)) + { + var renamingResult = await new ConstRenamer(WorkingDocument).RenameDeclarationsAsync(classNode); + + if (renamingResult != null && renamingResult.Node != null) + { + classNode = renamingResult.Node as ClassDeclarationSyntax; + WorkingDocument = renamingResult.Document; + } + + if (renamingResult != null) + { + var lineSpan = classNode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(classNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "CamelCasedFields", + Generator = nameof(CamelCasedFields) + }); + } + } + + return classNode; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/Option/CleanupTypes.cs new file mode 100644 index 0000000..ef806a1 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/Option/CleanupTypes.cs @@ -0,0 +1,13 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Class fields: Change _something -> Something or something")] + NormalFields = 0x01, + + [CleanupItem(Title = "Const fields: USE_THIS_FORMAT")] + ConstFields = 0x02, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/Option/Options.cs new file mode 100644 index 0000000..0d8ec5f --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedClassFields/Option/Options.cs @@ -0,0 +1,11 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() => CodeCleanerType.CamelCasedFields; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/CamelCasedLocalVariable.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/CamelCasedLocalVariable.cs new file mode 100644 index 0000000..3d6304c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/CamelCasedLocalVariable.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable; + +public class CamelCasedLocalVariable : VariableRenamingBase +{ + protected override SyntaxNode GetWorkingNode(SyntaxNode initialSourceNode, SyntaxAnnotation annotationForSelectedNodes) + { + return + initialSourceNode + .DescendantNodes() + .OfType() + .FirstOrDefault(m => !m.HasAnnotation(annotationForSelectedNodes)); + } + + protected override VariableRenamingBaseRewriter GetRewriter(Document workingDocument) + { + return new Rewriter(workingDocument, IsReportOnlyMode, Options); + } + + private class Rewriter : VariableRenamingBaseRewriter + { + public Rewriter(Document workingDocument, bool isReportOnlyMode, ICleanupOption options) : base(workingDocument, isReportOnlyMode, options) { } + + public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) + { + MethodDeclarationSyntax methodDeclarationSyntax = null; + + Task.Run(async delegate + { + methodDeclarationSyntax = await RenameDeclarationsAsync(node); + }).GetAwaiter().GetResult(); + + return base.VisitMethodDeclaration(methodDeclarationSyntax as MethodDeclarationSyntax); + } + + private async Task RenameDeclarationsAsync(MethodDeclarationSyntax methodNode) + { + if (CheckOption((int)CleanupTypes.LocalVariable)) + { + var renamingResult = await new VariableRenamer(WorkingDocument).RenameDeclarationsAsync(methodNode); + + if (renamingResult != null && renamingResult.Node != null) + { + methodNode = renamingResult.Node as MethodDeclarationSyntax; + WorkingDocument = renamingResult.Document; + } + + if (renamingResult != null) + { + var lineSpan = methodNode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(methodNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Camel Cased Methods", + Generator = nameof(CamelCasedLocalVariable) + }); + } + } + + if (CheckOption((int)CleanupTypes.MethodParameter)) + { + var renamingResult = await new ParameterRenamer(WorkingDocument).RenameDeclarationsAsync(methodNode); + + if (renamingResult != null && renamingResult.Node != null) + { + methodNode = renamingResult.Node as MethodDeclarationSyntax; + WorkingDocument = renamingResult.Document; + } + + if (renamingResult != null) + { + var lineSpan = methodNode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(methodNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Camel Cased Methods", + Generator = nameof(CamelCasedLocalVariable) + }); + } + } + + return methodNode; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/Option/CleanupTypes.cs new file mode 100644 index 0000000..2a30034 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/Option/CleanupTypes.cs @@ -0,0 +1,13 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Method parameters")] + MethodParameter = 0x01, + + [CleanupItem(Title = "Local variable declarations")] + LocalVariable = 0x02, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/Option/Options.cs new file mode 100644 index 0000000..d850ef8 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CamelCasedMethodVariable/Option/Options.cs @@ -0,0 +1,14 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() + { + return CodeCleanerType.CamelCasedMethodVariable; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CompactSmallIfElseStatements.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CompactSmallIfElseStatements.cs new file mode 100644 index 0000000..cd5d175 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/CompactSmallIfElseStatements.cs @@ -0,0 +1,216 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class CompactSmallIfElseStatements : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var syntaxRewriter = new Rewriter(initialSourceNode, IsReportOnlyMode, Options); + var modifiedSourceNode = syntaxRewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(syntaxRewriter.GetReport()); + return initialSourceNode; + } + + if (modifiedSourceNode != initialSourceNode && TidyCSharpPackage.Instance != null) + { + initialSourceNode = Formatter.Format(modifiedSourceNode, TidyCSharpPackage.Instance.Solution.Workspace); + } + + return initialSourceNode; + } + + private static SyntaxTrivia _endOfLineTrivia = default(SyntaxTrivia); + private const int MaxIfLineLength = 50, MaxReturnStatementLength = 10; + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + public Rewriter(SyntaxNode initialSource, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) + { + _endOfLineTrivia = + initialSource + .SyntaxTree + .GetRoot() + .DescendantTrivia(descendIntoTrivia: true) + .FirstOrDefault(x => x.IsKind(SyntaxKind.EndOfLineTrivia)); + } + + public override SyntaxNode VisitIfStatement(IfStatementSyntax mainIFnode) + { + var newIfNode = Cleanup(mainIFnode); + + if (!string.Equals(newIfNode.ToFullString().Trim(), mainIFnode.ToFullString().Trim(), StringComparison.Ordinal)) + { + var nextToken = mainIFnode.GetLastToken().GetNextToken(); + + var lineSpan = mainIFnode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(mainIFnode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Compact small if-else statements", + Generator = nameof(CompactSmallIfElseStatements) + }); + + if + ( + nextToken != null && + mainIFnode.GetTrailingTrivia().Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) == false && + nextToken.LeadingTrivia.Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) == false + ) + { + return newIfNode.WithTrailingTrivia(newIfNode.GetTrailingTrivia().Add(_endOfLineTrivia)); + } + + return newIfNode.WithTrailingTrivia(newIfNode.GetTrailingTrivia()); + } + + return mainIFnode; + } + + private SyntaxNode Cleanup(IfStatementSyntax originalIfNode) + { + if (originalIfNode.DescendantTrivia(descendIntoTrivia: true).HasNoneWhiteSpaceTrivia()) return base.VisitIfStatement(originalIfNode); + + var singleStatementInsideIf = GetInsideStatement(originalIfNode.Statement); + + if (singleStatementInsideIf == null || singleStatementInsideIf is IfStatementSyntax) return base.VisitIfStatement(originalIfNode); + + var newIf = GetNewIf(originalIfNode, singleStatementInsideIf); + + if (newIf == originalIfNode) return base.VisitIfStatement(originalIfNode); + if (newIf.Else == null) return newIf; + + var singleStatementInsideElse = GetInsideStatement(newIf.Else.Statement); + + if (singleStatementInsideElse == null) return base.VisitIfStatement(originalIfNode); + + if (singleStatementInsideElse is IfStatementSyntax ifSingleStatement) + { + singleStatementInsideElse = Cleanup(ifSingleStatement) as IfStatementSyntax; + } + + newIf = newIf.WithElse(GetNewIfWithElse(newIf.Else, singleStatementInsideElse)); + + return newIf; + } + + private IfStatementSyntax GetNewIf(IfStatementSyntax orginalIFnode, StatementSyntax singleStatementInsideIf) + { + var closeParenTrivia = orginalIFnode.CloseParenToken.WithoutWhiteSpaceTrivia(); + + var trailingTriviaList = + new SyntaxTriviaList() + .AddRange(closeParenTrivia.TrailingTrivia.Where(t => t.IsWhiteSpaceTrivia() == false)) + .AddRange(singleStatementInsideIf.GetTrailingTrivia().Where(t => t.IsWhiteSpaceTrivia() == false)); + + if (singleStatementInsideIf != orginalIFnode.Statement) + { + trailingTriviaList = trailingTriviaList.AddRange(orginalIFnode.Statement.GetTrailingTrivia().Where(t => t.IsWhiteSpaceTrivia() == false)); + } + + trailingTriviaList = trailingTriviaList.Add(_endOfLineTrivia); + + var newIf = + orginalIFnode + .WithIfKeyword(orginalIFnode.IfKeyword.WithTrailingTrivia(SyntaxFactory.Space)) + .WithOpenParenToken(orginalIFnode.OpenParenToken.WithoutWhiteSpaceTrivia()) + .WithCloseParenToken(SyntaxTokenExtensions.WithoutTrivia(closeParenTrivia)) + .WithCondition(orginalIFnode.Condition.WithoutWhiteSpaceTrivia()) + .WithStatement( + singleStatementInsideIf + .WithLeadingTrivia(SyntaxFactory.Space) + .WithTrailingTrivia(trailingTriviaList) + ); + + if (singleStatementInsideIf is ReturnStatementSyntax returnStatement) + { + if + ( + (returnStatement.Expression == null && newIf.WithElse(null).Span.Length <= 2 * MaxIfLineLength) || + ( + newIf.WithElse(null).Span.Length <= MaxIfLineLength + && + (returnStatement.Expression is LiteralExpressionSyntax || returnStatement.Expression.Span.Length <= MaxReturnStatementLength) + ) + ) + return newIf; + + if (singleStatementInsideIf.WithoutTrivia().DescendantTrivia().Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia))) return orginalIFnode; + if (newIf.WithElse(null).WithoutTrivia().Span.Length <= MaxIfLineLength) return newIf; + return orginalIFnode; + } + + if (singleStatementInsideIf.WithoutTrivia().DescendantTrivia().Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia))) return orginalIFnode; + if (newIf.WithElse(null).Span.Length > MaxIfLineLength) return orginalIFnode; + + return newIf; + } + + private ElseClauseSyntax GetNewIfWithElse(ElseClauseSyntax elseNode, StatementSyntax singleStatementInsideElse) + { + var newElse = + elseNode + .WithElseKeyword( + elseNode.ElseKeyword + .WithTrailingTrivia() + ) + .WithStatement( + singleStatementInsideElse + .WithLeadingTrivia(SyntaxFactory.Space) + //.WithTrailingTrivia(trailingTriviaList) + ); + + if (singleStatementInsideElse is ReturnStatementSyntax returnStatement) + { + if (returnStatement.Expression == null || returnStatement.Expression is LiteralExpressionSyntax || returnStatement.Expression.Span.Length <= MaxReturnStatementLength) + return newElse; + } + + if (singleStatementInsideElse is IfStatementSyntax == false) + { + if ( + singleStatementInsideElse.WithoutTrivia().DescendantTrivia().Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) + || + singleStatementInsideElse.Span.Length + 5 > MaxIfLineLength + ) + return elseNode.WithStatement(singleStatementInsideElse); + } + + return newElse; + } + + private StatementSyntax GetInsideStatement(BlockSyntax block) + { + if (block.Statements.Count != 1) return null; + if (block.HasNoneWhiteSpaceTrivia()) return null; + var firstStatement = block.Statements.FirstOrDefault(); + if (firstStatement is IfStatementSyntax) return firstStatement; + if (firstStatement.Span.Length <= Whitespace.Option.Options.BlockSingleStatementMaxLength) return firstStatement; + return null; + } + + private StatementSyntax GetInsideStatement(StatementSyntax singleStatement) + { + if (singleStatement is BlockSyntax newBlockStatement) + { + if ((singleStatement = GetInsideStatement(newBlockStatement)) == null) return null; + } + + if (singleStatement.HasNoneWhiteSpaceTrivia()) return null; + + return singleStatement; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertFullNameTypesToBuiltInTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertFullNameTypesToBuiltInTypes.cs new file mode 100644 index 0000000..a41c07c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertFullNameTypesToBuiltInTypes.cs @@ -0,0 +1,89 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class ConvertFullNameTypesToBuiltInTypes : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var syntaxRewriter = new Rewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode + , Options); + + var modifiedSourceNode = syntaxRewriter.ConvertFullNameTypesToBuiltInTypesHelper(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(syntaxRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public Rewriter(SemanticModel semanticModel, + bool isReportOnlyMode, ICleanupOption options) : base(isReportOnlyMode, options) + { + _semanticModel = semanticModel; + } + + public SyntaxNode ConvertFullNameTypesToBuiltInTypesHelper(SyntaxNode initialSource) + { + var builtInTypesMapDic = TypesMapItem.GetBuiltInTypesDic(); + + var selectedTokensList = + initialSource + .DescendantNodes() + .Where + ( + n => + (n is IdentifierNameSyntax || n is QualifiedNameSyntax) + && + builtInTypesMapDic.ContainsKey(n.WithoutTrivia().ToFullString()) + ); + + return initialSource.ReplaceNodes( + selectedTokensList, + (oldNode1, oldNode2) => + { + if (oldNode1.Parent is QualifiedNameSyntax) return oldNode1; + + if (oldNode1.Parent is MemberAccessExpressionSyntax) + { + if ((oldNode1.Parent as MemberAccessExpressionSyntax).Expression != oldNode1) return oldNode1; + var symbol = _semanticModel.GetSymbolInfo(oldNode1).Symbol; + if (symbol != null && symbol.Kind != SymbolKind.NamedType) return oldNode1; + } + else if (oldNode1 is IdentifierNameSyntax == false && oldNode1 is QualifiedNameSyntax == false) return oldNode1; + else + { + var symbol = _semanticModel.GetSymbolInfo(oldNode1).Symbol; + if (symbol != null && symbol.Kind != SymbolKind.NamedType) return oldNode1; + } + + var lineSpan = oldNode1.GetFileLinePosSpan(); + + AddReport(new ChangesReport(oldNode1) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Convert full name types to built in Types", + Generator = nameof(ConvertFullNameTypesToBuiltInTypes) + }); + + return + builtInTypesMapDic[oldNode1.WithoutTrivia().ToFullString()] + .NewNode + .WithLeadingTrivia(oldNode1.GetLeadingTrivia()) + .WithTrailingTrivia(oldNode1.GetTrailingTrivia()); + } + ); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/ConvertMembersToExpressionBodied.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/ConvertMembersToExpressionBodied.cs new file mode 100644 index 0000000..c0b13f1 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/ConvertMembersToExpressionBodied.cs @@ -0,0 +1,291 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.ConvertMembersToExpressionBodied; + +public class ConvertMembersToExpressionBodied : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return ConvertMembersToExpressionBodiedHelper(initialSourceNode, Options); + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + public Rewriter(bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) + { } + public override SyntaxNode Visit(SyntaxNode node) + { + if (node == null) return base.Visit(node); + + var message = ""; + + if (node is MethodDeclarationSyntax && node.Parent is ClassDeclarationSyntax) + { + if (CheckOption((int)Option.CleanupTypes.ConvertMethods)) + { + node = ConvertToExpressionBodiedHelper(node as MethodDeclarationSyntax); + message = "Method Declaration should be Converted to expression bodied"; + } + } + else if (node is PropertyDeclarationSyntax) + { + if (CheckOption((int)Option.CleanupTypes.ConvertReadOnlyProperty)) + { + node = ConvertToExpressionBodiedHelper(node as PropertyDeclarationSyntax); + message = "Property Declaration should be Converted to expression bodied"; + } + } + else if (node is ConstructorDeclarationSyntax) + { + if (CheckOption((int)Option.CleanupTypes.ConvertConstructors)) + { + node = ConvertToExpressionBodiedHelper(node as ConstructorDeclarationSyntax); + message = "Constructor should be Converted to expression bodied"; + } + } + + if (!string.IsNullOrEmpty(message)) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = message, + Generator = nameof(ConvertMembersToExpressionBodied) + }); + } + + return base.Visit(node); + } + } + + private static SyntaxTrivia[] _spaceTrivia = { SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ") }; + public SyntaxNode ConvertMembersToExpressionBodiedHelper(SyntaxNode initialSourceNode, ICleanupOption options) + { + var rewriter = new Rewriter(IsReportOnlyMode, options); + var modifiedSourceNode = rewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(rewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + public static MethodDeclarationSyntax ConvertToExpressionBodiedHelper(MethodDeclarationSyntax methodDeclaration) + { + var expression = AnalyzeMethods(methodDeclaration); + + if (expression == null) return methodDeclaration; + + var closeParen = methodDeclaration.DescendantTokens() + .FirstOrDefault(x => x.IsKind(SyntaxKind.CloseParenToken)); + + if (closeParen != null) + { + methodDeclaration = + methodDeclaration.ReplaceToken(closeParen, closeParen.WithTrailingTrivia(_spaceTrivia)); + } + + var newMethod = + methodDeclaration + .WithLeadingTrivia(methodDeclaration.GetLeadingTrivia()) + .WithExpressionBody( + SyntaxFactory.ArrowExpressionClause(expression.WithLeadingTrivia(_spaceTrivia))) + .WithBody(null) + .WithSemicolonToken(GetSemicolon(methodDeclaration.Body)) + .WithAdditionalAnnotations(Formatter.Annotation); + + return newMethod; + } + + private static ExpressionSyntax AnalyzeMethods(MethodDeclarationSyntax method) + { + if ((method.Parent is ClassDeclarationSyntax) == false) return null; + if (method.Body == null) return null; + if (method.Body.Statements.Count != 1) return null; + if (method.Body.ContainsDirectives) return null; + + var singleStatement = method.Body.Statements.FirstOrDefault(); + if (singleStatement is IfStatementSyntax) return null; + if (singleStatement is ThrowStatementSyntax) return null; + if (singleStatement is YieldStatementSyntax) return null; + if (singleStatement is ReturnStatementSyntax == false && singleStatement is ExpressionStatementSyntax == false) return null; + + if (singleStatement.HasLeadingTrivia) + { + if (HasNoneWhitespaceTrivia(singleStatement.GetLeadingTrivia()) == false) return null; + } + + if (singleStatement.HasTrailingTrivia) + { + if (HasNoneWhitespaceTrivia(singleStatement.GetTrailingTrivia()) == false) return null; + } + + if (method.Body.CloseBraceToken.HasLeadingTrivia) + { + if (HasNoneWhitespaceTrivia(method.Body.CloseBraceToken.LeadingTrivia) == false) return null; + } + + if (method.Body.OpenBraceToken.HasLeadingTrivia) + { + if (HasNoneWhitespaceTrivia(method.Body.OpenBraceToken.LeadingTrivia) == false) return null; + } + + var expression = + ( + (singleStatement is ReturnStatementSyntax) + ? (singleStatement as ReturnStatementSyntax).Expression + : (singleStatement as ExpressionStatementSyntax).Expression + ) + .WithoutLeadingTrivia(); + + var length = expression.WithoutTrivia().Span.Length + method.Span.Length - method.Body.FullSpan.Length; + if (length > Option.Options.MaxExpressionBodiedMemberLength) return null; + if (method.Body.ChildNodes().OfType().Any()) return null; + + return expression; + } + + private static bool HasNoneWhitespaceTrivia(SyntaxTriviaList getLeadingTrivia) + { + return getLeadingTrivia.All(t => t.IsKind(SyntaxKind.EndOfLineTrivia) || t.IsKind(SyntaxKind.WhitespaceTrivia)); + } + + public static PropertyDeclarationSyntax ConvertToExpressionBodiedHelper(PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.AccessorList == null) return propertyDeclaration; + + var getNode = + propertyDeclaration.AccessorList.Accessors.FirstOrDefault( + x => x.Keyword.IsKind(SyntaxKind.GetKeyword)); + + var setNode = + propertyDeclaration.AccessorList.Accessors.FirstOrDefault( + x => x.Keyword.IsKind(SyntaxKind.SetKeyword)); + + if (setNode != null || getNode.Body == null) return propertyDeclaration; + if (getNode.Body == null) return propertyDeclaration; + if (getNode.Body.Statements.Count > 1) return propertyDeclaration; + if (getNode.Body.ContainsDirectives) return propertyDeclaration; + + var returnStatements = getNode.Body.Statements.OfType().ToList(); + if (returnStatements.Count() != 1) return propertyDeclaration; + var expression = returnStatements.FirstOrDefault().Expression.WithoutTrivia(); + + var length = + expression.Span.Length + + propertyDeclaration.Span.Length - + propertyDeclaration.AccessorList.FullSpan.Length; + + if (length >= 100) return propertyDeclaration; + + propertyDeclaration = + propertyDeclaration + .WithIdentifier(propertyDeclaration.Identifier.WithTrailingTrivia(_spaceTrivia)) + .WithLeadingTrivia(propertyDeclaration.GetLeadingTrivia()) + .WithExpressionBody( + SyntaxFactory.ArrowExpressionClause(expression.WithLeadingTrivia(_spaceTrivia))) + .WithAccessorList(null) + .WithSemicolonToken(GetSemicolon(propertyDeclaration.AccessorList)) + .WithAdditionalAnnotations(Formatter.Annotation); + + return propertyDeclaration; + } + + private static SyntaxToken GetSemicolon(BlockSyntax block) + { + var statement = block.Statements.FirstOrDefault(); + + var semicolon = + (statement is ExpressionStatementSyntax) + ? (statement as ExpressionStatementSyntax).SemicolonToken + : (statement as ReturnStatementSyntax).SemicolonToken; + + var trivia = semicolon.TrailingTrivia.AsEnumerable(); + trivia = trivia.Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)); + + var closeBraceTrivia = block.CloseBraceToken.TrailingTrivia.AsEnumerable(); + trivia = trivia.Concat(closeBraceTrivia); + + return semicolon.WithTrailingTrivia(trivia); + } + + private static SyntaxToken GetSemicolon(AccessorListSyntax accessorList) + { + var semicolon = ((ReturnStatementSyntax)accessorList.Accessors[0].Body.Statements[0]).SemicolonToken; + + var trivia = semicolon.TrailingTrivia.AsEnumerable(); + trivia = trivia.Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)); + + var closeBraceTrivia = accessorList.CloseBraceToken.TrailingTrivia.AsEnumerable(); + trivia = trivia.Concat(closeBraceTrivia); + + return semicolon.WithTrailingTrivia(trivia); + } + + public static ConstructorDeclarationSyntax ConvertToExpressionBodiedHelper(ConstructorDeclarationSyntax constructorDeclaration) + { + if (constructorDeclaration.Body == null) return constructorDeclaration; + if (constructorDeclaration.Body.Statements.Count != 1) return constructorDeclaration; + if (constructorDeclaration.Body.ContainsDirectives) return constructorDeclaration; + + var singleStatement = constructorDeclaration.Body.Statements.FirstOrDefault(); + if (singleStatement is IfStatementSyntax) return constructorDeclaration; + if (singleStatement is ThrowStatementSyntax) return constructorDeclaration; + if (singleStatement is YieldStatementSyntax) return constructorDeclaration; + if (singleStatement is ExpressionStatementSyntax == false) return constructorDeclaration; + + if (singleStatement.HasLeadingTrivia) + { + if (HasNoneWhitespaceTrivia(singleStatement.GetLeadingTrivia()) == false) return constructorDeclaration; + } + + if (singleStatement.HasTrailingTrivia) + { + if (HasNoneWhitespaceTrivia(singleStatement.GetTrailingTrivia()) == false) return constructorDeclaration; + } + + if (constructorDeclaration.Body.CloseBraceToken.HasLeadingTrivia) + { + if (HasNoneWhitespaceTrivia(constructorDeclaration.Body.CloseBraceToken.LeadingTrivia) == false) return constructorDeclaration; + } + + if (constructorDeclaration.Body.OpenBraceToken.HasLeadingTrivia) + { + if (HasNoneWhitespaceTrivia(constructorDeclaration.Body.OpenBraceToken.LeadingTrivia) == false) return constructorDeclaration; + } + + var expression = (singleStatement as ExpressionStatementSyntax).Expression + .WithoutLeadingTrivia(); + + var length = expression.WithoutTrivia().Span.Length + + constructorDeclaration.Span.Length - constructorDeclaration.Body.FullSpan.Length; + + if (length > Option.Options.MaxExpressionBodiedMemberLength) return constructorDeclaration; + if (constructorDeclaration.Body.ChildNodes().OfType().Any()) return constructorDeclaration; + + var newconstructorDeclaration = constructorDeclaration + .WithIdentifier(constructorDeclaration.Identifier.WithTrailingTrivia(_spaceTrivia)) + .WithBody(null) + .WithTrailingTrivia(_spaceTrivia) + .WithLeadingTrivia(constructorDeclaration.GetLeadingTrivia()) + .WithExpressionBody( + SyntaxFactory.ArrowExpressionClause(expression.WithLeadingTrivia(_spaceTrivia))) + .WithSemicolonToken(GetSemicolon(constructorDeclaration.Body)) + .WithAdditionalAnnotations(Formatter.Annotation); + + return newconstructorDeclaration; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/Option/CleanupTypes.cs new file mode 100644 index 0000000..645b90c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/Option/CleanupTypes.cs @@ -0,0 +1,16 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.ConvertMembersToExpressionBodied.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Convert Methods => Method with only a single return statement and lenth less than 100 chars(length of its signature and its single statement)")] + ConvertMethods = 0x01, + + [CleanupItem(Title = "Convert ReadOnly Property => ReadOnly Property with only a single return statement and lenth less than 100 chars(length of its Defenition and its single statement)")] + ConvertReadOnlyProperty = 0x02, + + [CleanupItem(Title = "Convert Constructors => Method with only a single return statement and lenth less than 100 chars(length of its signature and its single statement)")] + ConvertConstructors = 0x03, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/Option/Options.cs new file mode 100644 index 0000000..73899d2 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertMembersToExpressionBodied/Option/Options.cs @@ -0,0 +1,16 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.ConvertMembersToExpressionBodied.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public const int MaxExpressionBodiedMemberLength = 90; + + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() + { + return CodeCleanerType.ConvertMembersToExpressionBodied; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertPropertiesToAutoProperties.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertPropertiesToAutoProperties.cs new file mode 100644 index 0000000..9cab0f5 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ConvertPropertiesToAutoProperties.cs @@ -0,0 +1,393 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class ConvertPropertiesToAutoProperties : CodeCleanerCommandRunnerBase +{ + private Document _workingDocument, _orginalDocument; + private const string SelectedMethodAnnotationRename = "SELECTED_METHOD_ANNOTATION_RENAME"; + private const string SelectedMethodAnnotationRemove = "SELECTED_METHOD_ANNOTATION_REMOVE"; + + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + _orginalDocument = ProjectItemDetails.Document; + _workingDocument = ProjectItemDetails.Document; + + var walker = new MyWalker(ProjectItemDetails, Options); + walker.Visit(initialSourceNode); + + if (walker.VariablesToRemove.Any()) + { + var rewriter = new Rewriter(walker, ProjectItemDetails, Options); + initialSourceNode = rewriter.Visit(initialSourceNode); + _workingDocument = _workingDocument.WithSyntaxRoot(initialSourceNode); + + IEnumerable annotations; + + do + { + annotations = initialSourceNode.GetAnnotatedNodes(SelectedMethodAnnotationRename); + + if (annotations.Any() == false) break; + + var firstAnnotatedItem = annotations.FirstOrDefault(); + var annotationOfFirstAnnotatedItem = firstAnnotatedItem.GetAnnotations(SelectedMethodAnnotationRename).FirstOrDefault(); + + var renameResult = await Renamer.RenameSymbolAsync(_workingDocument, initialSourceNode, null, firstAnnotatedItem, annotationOfFirstAnnotatedItem.Data); + + _workingDocument = renameResult.Document; + initialSourceNode = await _workingDocument.GetSyntaxRootAsync(); + + firstAnnotatedItem = initialSourceNode.GetAnnotatedNodes(annotationOfFirstAnnotatedItem).FirstOrDefault(); + initialSourceNode = initialSourceNode.ReplaceNode(firstAnnotatedItem, firstAnnotatedItem.WithoutAnnotations(SelectedMethodAnnotationRename)); + + _workingDocument = _workingDocument.WithSyntaxRoot(initialSourceNode); + initialSourceNode = await _workingDocument.GetSyntaxRootAsync(); + } + while (annotations.Any()); + + var lineSpan = initialSourceNode.GetAnnotatedNodes(SelectedMethodAnnotationRemove) + .FirstOrDefault().GetFileLinePosSpan(); + + initialSourceNode = initialSourceNode.RemoveNodes(initialSourceNode.GetAnnotatedNodes(SelectedMethodAnnotationRemove), SyntaxRemoveOptions.KeepEndOfLine); + var t = initialSourceNode.DescendantNodes().OfType().Where(x => x.Declaration.Variables.Count == 0); + initialSourceNode = initialSourceNode.RemoveNodes(t, SyntaxRemoveOptions.KeepEndOfLine); + + _workingDocument = _workingDocument.WithSyntaxRoot(initialSourceNode); + + if (IsReportOnlyMode && + !IsEquivalentToUnModified(initialSourceNode)) + { + CollectMessages(new ChangesReport(await _orginalDocument.GetSyntaxRootAsync()) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "ConvertPropertiesToAutoProperties", + Generator = nameof(ConvertPropertiesToAutoProperties) + }); + + return initialSourceNode; + } + + return null; + } + + return initialSourceNode; + } + + protected override async Task SaveResultAsync(SyntaxNode initialSourceNode) + { + await base.SaveResultAsync((await _workingDocument.GetSyntaxTreeAsync()).GetRoot()); + } + + private class MyWalker : CSharpSyntaxWalker + { + private const string VarKeyword = "var"; + private readonly ProjectItemDetailsType _projectItemDetails; + private readonly SemanticModel _semanticModel; + + public MyWalker(ProjectItemDetailsType projectItemDetails, ICleanupOption options) + { + _projectItemDetails = projectItemDetails; + _semanticModel = projectItemDetails.SemanticModel; + } + + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax propertyDeclaration) + { + Task.Run(async delegate + { + await FindFullPropertyForConvertingAutoPropertyAsync(propertyDeclaration); + }).GetAwaiter().GetResult(); + } + + public List> VariablesToRemove = new(); + + private async Task FindFullPropertyForConvertingAutoPropertyAsync(PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.AccessorList == null) return null; + if (propertyDeclaration.AccessorList.Accessors.Count != 2) return null; + + var getNode = propertyDeclaration.AccessorList.Accessors.FirstOrDefault(x => x.Keyword.IsKind(SyntaxKind.GetKeyword)); + var setNode = propertyDeclaration.AccessorList.Accessors.FirstOrDefault(x => x.Keyword.IsKind(SyntaxKind.SetKeyword)); + + if (setNode == null || getNode == null) return null; + + var getIdentifier = HasJustReturnValue(getNode); + if (getIdentifier == null) return null; + + var setIdentifier = HasJustSetValue(setNode); + if (setIdentifier == null) return null; + + var setIdentifierSymbol = CSharpExtensions.GetSymbolInfo(_projectItemDetails.SemanticModel, setIdentifier).Symbol; + var getIdentifierSymbol = CSharpExtensions.GetSymbolInfo(_projectItemDetails.SemanticModel, getIdentifier).Symbol; + + if (SymbolEqualityComparer.Default.Equals(setIdentifierSymbol, getIdentifierSymbol) == false) return null; + //if (setIdentifierSymbol != getIdentifierSymbol) return null; + + var baseFieldSymbol = CSharpExtensions.GetSymbolInfo(_projectItemDetails.SemanticModel, setIdentifier).Symbol; + if (baseFieldSymbol is IFieldSymbol == false) return null; + + var propertyDelarationSymbol = CSharpExtensions.GetDeclaredSymbol(_projectItemDetails.SemanticModel, propertyDeclaration); + + if (SymbolEqualityComparer.Default.Equals(baseFieldSymbol.ContainingType, propertyDelarationSymbol.ContainingType) == false) return null; + //if (baseFieldSymbol.ContainingType != propertyDelarationSymbol.ContainingType) return null; + + var baseFieldVariableDeclaration = await baseFieldSymbol.DeclaringSyntaxReferences[0].GetSyntaxAsync() as VariableDeclaratorSyntax; + var baseFieldFieldDeclaration = baseFieldVariableDeclaration.Parent.Parent as FieldDeclarationSyntax; + + if (CheckVisibility(baseFieldFieldDeclaration, getNode, setNode, propertyDeclaration) == false) return null; + + var refrences = await SymbolFinder.FindReferencesAsync(baseFieldSymbol, TidyCSharpPackage.Instance.Solution); + + ReferencedSymbol references = null; + references = refrences.FirstOrDefault(); + + if (references == null) return null; + + if (baseFieldFieldDeclaration.HasNoneWhiteSpaceTrivia(new SyntaxKind[] { SyntaxKind.SingleLineCommentTrivia, SyntaxKind.MultiLineCommentTrivia })) return null; + + VariablesToRemove.Add( + new Tuple + ( + baseFieldVariableDeclaration, + propertyDeclaration, + references.Locations.Count() > 2 + ) + ); + + return propertyDeclaration; + } + + private bool CheckVisibility(FieldDeclarationSyntax baseFieldFieldDeclaration, AccessorDeclarationSyntax getNode, AccessorDeclarationSyntax setNode, PropertyDeclarationSyntax propertyDeclaration) + { + var methodResult = true; + if (baseFieldFieldDeclaration.IsPrivate()) + { + return methodResult; + } + + if (baseFieldFieldDeclaration.IsPublic()) + { + if (propertyDeclaration.IsPublic() == false || getNode.Modifiers.Any() || setNode.Modifiers.Any()) return false; + } + else if (baseFieldFieldDeclaration.IsProtected()) + { + if (propertyDeclaration.IsPrivate() == false) return false; + + if (propertyDeclaration.IsProtected() && (getNode.Modifiers.Any() || setNode.Modifiers.Any())) return false; + + else if (propertyDeclaration.IsPublic()) + { + if (baseFieldFieldDeclaration.IsInternal() == false) + { + if (getNode.Modifiers.Any(x => x.IsKind(SyntaxKind.PrivateKeyword) || x.IsKind(SyntaxKind.InternalKeyword))) return false; + if (setNode.Modifiers.Any(x => x.IsKind(SyntaxKind.PrivateKeyword) || x.IsKind(SyntaxKind.InternalKeyword))) return false; + } + else + { + if (getNode.Modifiers.Any(x => x.IsKind(SyntaxKind.PrivateKeyword))) return false; + if (setNode.Modifiers.Any(x => x.IsKind(SyntaxKind.PrivateKeyword))) return false; + } + } + } + else if (baseFieldFieldDeclaration.IsInternal()) + { + if (propertyDeclaration.IsPrivate() == false) return false; + + if (propertyDeclaration.IsInternal() && (getNode.Modifiers.Any() || setNode.Modifiers.Any())) return false; + + else if (propertyDeclaration.IsPublic()) + { + if (getNode.Modifiers.Any(x => x.IsKind(SyntaxKind.PrivateKeyword) || x.IsKind(SyntaxKind.ProtectedKeyword))) return false; + if (setNode.Modifiers.Any(x => x.IsKind(SyntaxKind.PrivateKeyword) || x.IsKind(SyntaxKind.ProtectedKeyword))) return false; + } + } + + return methodResult; + } + + private IdentifierNameSyntax HasJustSetValue(AccessorDeclarationSyntax setNode) + { + if (setNode.Body != null) + { + if (setNode.Body.Statements.Count > 1) return null; + var singleStatement = setNode.Body.Statements.Single() as ExpressionStatementSyntax; + if (singleStatement == null) return null; + + if (singleStatement.Expression is AssignmentExpressionSyntax assignMent) + { + if (assignMent.OperatorToken.ValueText != "=") return null; + + if (assignMent.Right is IdentifierNameSyntax rightIdentifier) + { + if (rightIdentifier.Identifier.ValueText != "value") return null; + + if (assignMent.Left is IdentifierNameSyntax leftIdentifier) + { + return leftIdentifier; + } + } + + return null; + } + + return null; + } + else // if(getNode) + { + } + + return null; + } + + private IdentifierNameSyntax HasJustReturnValue(AccessorDeclarationSyntax getNode) + { + if (getNode.Body != null) + { + if (getNode.Body.Statements.Count > 1) return null; + var singleStatement = getNode.Body.Statements.Single() as ReturnStatementSyntax; + if (singleStatement == null) return null; + + if (singleStatement.Expression is IdentifierNameSyntax identifierExpression) + { + return identifierExpression; + } + + return null; + } + else // if(getNode) + { + } + + return null; + } + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + private const string VarKeyword = "var"; + private readonly MyWalker _walker; + private readonly ProjectItemDetailsType _projectItemDetails; + private readonly SemanticModel _semanticModel; + private Document _workingDocument; + + public Rewriter(MyWalker walker, ProjectItemDetailsType projectItemDetails, ICleanupOption options) + : base(false, options) + { + _walker = walker; + _projectItemDetails = projectItemDetails; + _semanticModel = projectItemDetails.SemanticModel; + _workingDocument = projectItemDetails.Document; + } + + public override SyntaxNode VisitVariableDeclarator(VariableDeclaratorSyntax node) + { + var foundedItem = _walker.VariablesToRemove.FirstOrDefault(x => node == x.Item1); + + if (foundedItem != null) + { + if (foundedItem.Item3) + { + node = node.WithAdditionalAnnotations(new SyntaxAnnotation(SelectedMethodAnnotationRename, foundedItem.Item2.Identifier.ValueText)); + } + + node = node.WithAdditionalAnnotations(new SyntaxAnnotation(SelectedMethodAnnotationRemove)); + } + + return base.VisitVariableDeclarator(node); + } + + public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax propertyDeclaration) + { + var foundedItem = _walker.VariablesToRemove.FirstOrDefault(x => x.Item2 == propertyDeclaration); + + if (foundedItem != null) + { + propertyDeclaration = ConvertProperty(foundedItem.Item2, foundedItem.Item1.Parent.Parent as FieldDeclarationSyntax); + } + + return base.VisitPropertyDeclaration(propertyDeclaration); + } + + private PropertyDeclarationSyntax ConvertProperty(PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax relatedField) + { + var leadingList = new SyntaxTriviaList(); + + if (relatedField.Declaration.Variables.Count == 1) + { + leadingList = leadingList.AddRange(relatedField.GetLeadingTrivia()); + } + + var propertyDeclarationGetLeadingTrivia = propertyDeclaration.GetLeadingTrivia(); + + if (leadingList.Any() && propertyDeclarationGetLeadingTrivia.FirstOrDefault().IsKind(SyntaxKind.EndOfLineTrivia) == false) + { + var endOfLine = relatedField.GetLeadingTrivia().FirstOrDefault(x => x.IsKind(SyntaxKind.EndOfLineTrivia)); + + if (!endOfLine.IsKind(SyntaxKind.None)) + { + leadingList = leadingList.Add(endOfLine); + } + } + + leadingList = leadingList.AddRange(propertyDeclaration.GetLeadingTrivia()); + + var getNode = propertyDeclaration.AccessorList.Accessors.FirstOrDefault(x => x.Keyword.IsKind(SyntaxKind.GetKeyword)); + var setNode = propertyDeclaration.AccessorList.Accessors.FirstOrDefault(x => x.Keyword.IsKind(SyntaxKind.SetKeyword)); + + propertyDeclaration = + propertyDeclaration + .WithAccessorList + ( + propertyDeclaration.AccessorList.WithAccessors( + + new SyntaxList() + .Add( + getNode + .WithBody(null) + .WithTrailingTrivia() + .WithSemicolonToken( + SyntaxFactory.ParseToken(";") + .WithTrailingTrivia(SyntaxFactory.Space) + ) + .WithLeadingTrivia(SyntaxFactory.Space) + + ) + .Add( + setNode + .WithBody(null) + .WithTrailingTrivia() + .WithSemicolonToken( + SyntaxFactory.ParseToken(";") + .WithTrailingTrivia(SyntaxFactory.Space) + + ) + .WithLeadingTrivia(SyntaxFactory.Space) + ) + ) + .WithOpenBraceToken(propertyDeclaration.AccessorList.OpenBraceToken.WithLeadingTrivia().WithTrailingTrivia()) + .WithCloseBraceToken(propertyDeclaration.AccessorList.CloseBraceToken.WithLeadingTrivia()).WithoutTrailingTrivia() + ) + .WithLeadingTrivia(leadingList) + .WithIdentifier(propertyDeclaration.Identifier.WithTrailingTrivia(SyntaxFactory.Space)); + + if (relatedField.Declaration.Variables.FirstOrDefault().Initializer != null) + { + propertyDeclaration = + propertyDeclaration.WithInitializer( + relatedField.Declaration.Variables.FirstOrDefault().Initializer) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + } + + return propertyDeclaration; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpGeneralCleaner.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpGeneralCleaner.cs new file mode 100644 index 0000000..7b81e72 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpGeneralCleaner.cs @@ -0,0 +1,205 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class MSharpGeneralCleaner : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return await ChangeMethodHelperAsync(initialSourceNode); + } + + private async Task ChangeMethodHelperAsync(SyntaxNode initialSourceNode) + { + var csSyntaxRewriter = new CsMethodStringRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + var modifiedSourceNode = csSyntaxRewriter.Visit(initialSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + var multiLineSyntaxRewriter = new MultiLineExpressionRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = multiLineSyntaxRewriter.Visit(modifiedSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(csSyntaxRewriter.GetReport()); + CollectMessages(multiLineSyntaxRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + private class MultiLineExpressionRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public MultiLineExpressionRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitExpressionStatement(ExpressionStatementSyntax node) + { + if (node.ToString().Length < 110) + return base.VisitExpressionStatement(node); + + var m = node.Expression; + var rewritableToken = new List(); + var trivia = new SyntaxTriviaList(SyntaxFactory.EndOfLine("\n")); + + trivia = trivia.AddRange(node.GetLeadingTrivia().Reverse() + .TakeWhile(x => x.IsDirective ^ !x.IsKind(SyntaxKind.EndOfLineTrivia))); + + trivia = trivia.Add(SyntaxFactory.Whitespace(" ")); + var newExpression = SyntaxFactory.ParseExpression(""); + + while (m != null && m.ChildNodes().Any()) + { + var m2 = m.ChildNodes(); + + if (m2.FirstOrDefault() is MemberAccessExpressionSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + m = m2.FirstOrDefault().As()?.Expression; + + if (newExpression.ToString() == "") + newExpression = SyntaxFactory.InvocationExpression(methodName.Name, arguments) + .WithoutTrailingTrivia(); + else + newExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.InvocationExpression(methodName.Name, arguments).WithoutTrailingTrivia(), + SyntaxFactory.Token(SyntaxKind.DotToken).WithLeadingTrivia(trivia), SyntaxFactory.IdentifierName(newExpression.ToString())); + } + else if (m2.FirstOrDefault() is IdentifierNameSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var identifierName = m2.FirstOrDefault() as IdentifierNameSyntax; + var arguments = m2.LastOrDefault() as ArgumentListSyntax; + m = null; + + if (newExpression.ToString() == "") + newExpression = SyntaxFactory.InvocationExpression(identifierName, arguments).WithoutTrailingTrivia(); + else + newExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.InvocationExpression(identifierName.WithoutTrailingTrivia(), arguments.WithoutTrailingTrivia()), + SyntaxFactory.Token(SyntaxKind.DotToken).WithLeadingTrivia(trivia), SyntaxFactory.IdentifierName(newExpression.ToString())); + } + else + { + if (newExpression.ToString() == "") + newExpression = m.WithoutTrailingTrivia(); + else + newExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + m.WithoutTrailingTrivia(), SyntaxFactory.Token(SyntaxKind.DotToken).WithLeadingTrivia(trivia), SyntaxFactory.IdentifierName(newExpression.ToString())); + + m = null; + } + } + + if (m != null) + newExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + m, SyntaxFactory.IdentifierName(newExpression.ToString())); + + if (!newExpression.ToFullString().Equals(node.ToFullString())) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "your expression should be multi lined", + Generator = nameof(MultiLineExpressionRewriter) + }); + } + + return SyntaxFactory.ExpressionStatement(newExpression) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + } + + private class CsMethodStringRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public CsMethodStringRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitLiteralExpression(LiteralExpressionSyntax node) + { + if (node.Token.ValueText.StartsWith("c#:")) + { + var classDeclaration = node.FirstAncestorOrSelf(); + if (classDeclaration == null) return node; + + if (classDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)) + return node; + + var s = CSharpExtensions.GetDeclaredSymbol(_semanticModel, classDeclaration); + + if (!s.AllInterfaces.Any(x => x.Name == "IMSharpConcept")) + return node; + + var args = new SeparatedSyntaxList(); + + args = args.Add(SyntaxFactory.Argument( + SyntaxFactory.ParseExpression(node.Token.Text.Remove(node.Token.Text.IndexOf("c#:"), 3)))); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Please use cs() instead of C#)", + Generator = nameof(CsMethodStringRewriter) + }); + + return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("cs") + , SyntaxFactory.ArgumentList(args)); + } + + return base.VisitLiteralExpression(node); + } + + public override SyntaxNode VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node) + { + if (node.Contents.ToString().StartsWith("c#:")) + { + var classDeclaration = node.FirstAncestorOrSelf(); + if (classDeclaration == null) return node; + + if (classDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)) + return node; + + var s = CSharpExtensions.GetDeclaredSymbol(_semanticModel, classDeclaration); + + if (!s.AllInterfaces.Any(x => x.Name == "IMSharpConcept")) + return node; + + var args = new SeparatedSyntaxList(); + + args = args.Add(SyntaxFactory.Argument( + SyntaxFactory.ParseExpression(node.ToString().Remove(node.ToString().IndexOf("c#:"), 3)))); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "\"C#:\" --> cs(\"\")", + Generator = nameof(CsMethodStringRewriter) + }); + + return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("cs") + , SyntaxFactory.ArgumentList(args)); + } + + return base.VisitInterpolatedStringExpression(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpModelCleaner.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpModelCleaner.cs new file mode 100644 index 0000000..c085916 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpModelCleaner.cs @@ -0,0 +1,221 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class MSharpModelCleaner : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + if (ProjectItemDetails.Document.Project.Name == "#Model") + return await ChangeMethodHelperAsync(initialSourceNode); + + return initialSourceNode; + } + + private async Task ChangeMethodHelperAsync(SyntaxNode initialSourceNode) + { + var localTimeRewriter = new LocalTimeRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + var modifiedSourceNode = localTimeRewriter.Visit(initialSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var cascadeDeleteRewriter = new CascadeDeleteRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = cascadeDeleteRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var calculatedGetterRewriter = new CalculatedGetterRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = calculatedGetterRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var transientDatabaseModeRewriter = new TransientDatabaseModeRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = transientDatabaseModeRewriter.Visit(modifiedSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(localTimeRewriter.GetReport()); + CollectMessages(cascadeDeleteRewriter.GetReport()); + CollectMessages(calculatedGetterRewriter.GetReport()); + CollectMessages(transientDatabaseModeRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + private class LocalTimeRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public LocalTimeRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var methodSymbol = CSharpExtensions.GetSymbolInfo(_semanticModel, node).Symbol as IMethodSymbol; + var methodName = methodSymbol?.Name; + var methodType = methodSymbol?.ReturnType.OriginalDefinition?.ToString(); + + var acceptedArguments = new string[] + { + "\"c#:LocalTime.UtcNow\"","\"c#:LocalTime.Now\"", + "cs(\"LocalTime.UtcNow\")","cs(\"LocalTime.Now\")" + }; + + if (methodName == "Default" && methodType == "MSharp.DateTimeProperty") + { + if (node.ArgumentsCountShouldBe(1) && + node.FirstArgumentShouldBeIn(acceptedArguments)) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use DefaultToNow instead.", + Generator = nameof(LocalTimeRewriter) + }); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, node.GetLeftSideExpression(), SyntaxFactory.IdentifierName("DefaultToNow")), + SyntaxFactory.ArgumentList()); + } + } + + return base.VisitInvocationExpression(node); + } + } + + private class CascadeDeleteRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public CascadeDeleteRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var methodSymbol = (CSharpExtensions.GetSymbolInfo(_semanticModel, node).Symbol as IMethodSymbol); + var methodName = methodSymbol?.Name; + + if (node.ArgumentsCountShouldBe(1) && + node.FirstArgumentShouldBe("CascadeAction.CascadeDelete") && + methodName == "OnDelete") + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use CascadeDelete instead.", + Generator = nameof(CascadeDeleteRewriter) + }); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + node.GetLeftSideExpression(), + SyntaxFactory.IdentifierName("CascadeDelete")), + SyntaxFactory.ArgumentList()); + } + + return base.VisitInvocationExpression(node); + } + } + + private class CalculatedGetterRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public CalculatedGetterRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var calculatedInvocation = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "Calculated").FirstOrDefault(); + + var getterInvocation = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "Getter").FirstOrDefault(); + + if (calculatedInvocation == null || getterInvocation == null) + return base.VisitInvocationExpression(node); + + if ((calculatedInvocation.ArgumentsCountShouldBe(0) || + (calculatedInvocation.ArgumentsCountShouldBe(1) && + calculatedInvocation.FirstArgumentShouldBe("true"))) && + getterInvocation.ArgumentsCountShouldBe(1)) + { + var newNode = node.ReplaceNodes( + node.DescendantNodesAndSelfOfType() + .Where(x => x.MethodNameShouldBeIn(new string[] { "Getter", "Calculated" })), + (nde1, nde2) => + { + if (nde1.MethodNameShouldBe("Calculated")) + return nde1.GetLeftSideExpression(); + else if (nde1.MethodNameShouldBe("Getter")) + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + nde2.GetLeftSideExpression(), SyntaxFactory.IdentifierName("CalculatedFrom")), + nde1.ArgumentList); + else return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use CalculatedFrom instead.", + Generator = nameof(CalculatedGetterRewriter) + }); + + return newNode; + } + + return base.VisitInvocationExpression(node); + } + } + + private class TransientDatabaseModeRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public TransientDatabaseModeRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var methodSymbol = CSharpExtensions.GetSymbolInfo(_semanticModel, node).Symbol as IMethodSymbol; + var methodName = methodSymbol?.Name; + + if (node.ArgumentsCountShouldBe(1) && + node.FirstArgumentShouldBe("DatabaseOption.Transient") && + methodName == "DatabaseMode") + { + var newNode = node.ReplaceNode(node.ArgumentList, SyntaxFactory.ArgumentList()); + + newNode = newNode.ReplaceNode(newNode.DescendantNodesAndSelfOfType() + .FirstOrDefault(x => x.Identifier.ToString() == "DatabaseMode"), + SyntaxFactory.IdentifierName("Transient")); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use Transient method instead.", + Generator = nameof(TransientDatabaseModeRewriter) + }); + + return newNode; + } + + return base.VisitInvocationExpression(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpUICleaner.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpUICleaner.cs new file mode 100644 index 0000000..bb266bb --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/MSharpUICleaner.cs @@ -0,0 +1,1856 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; +using RoslynDocument = Microsoft.CodeAnalysis.Document; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class MSharpUiCleaner : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + if (ProjectItemDetails.Document.Project.Name == "#UI") + return await ChangeMethodHelperAsync(initialSourceNode); + + return initialSourceNode; + } + + private async Task ChangeMethodHelperAsync(SyntaxNode initialSourceNode) + { + var sendItemIdRewriter = new SendItemIdRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + var modifiedSourceNode = sendItemIdRewriter.Visit(initialSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var ifWorkFlowRewriter = new IfWorkFlowRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = ifWorkFlowRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var customColumnRewriter = new CustomColumnRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = customColumnRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var elementsNewSyntaxRewriter = new ElementsNewSyntaxRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = elementsNewSyntaxRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var formModuleWorkFlow = new FormModuleWorkFlowRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = formModuleWorkFlow.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var generalWorkFlowRewriter = new GeneralWorkFlowRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = generalWorkFlowRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var combinedExpressionsFormRewriter = new CombinedExpressionsFormRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = combinedExpressionsFormRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var listModuleWorkFlowRewriter = new ListModuleWorkFlowRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = listModuleWorkFlowRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var fullSearchRewriter = new FullSearchRewriter(ProjectItemDetails.SemanticModel + , ProjectItemDetails.Document.Project.Solution + , ProjectItemDetails.Document, IsReportOnlyMode, Options); + + modifiedSourceNode = fullSearchRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var customFieldRewriter = new CustomFieldRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = customFieldRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var onClickGoWorkFlowRewriter = new OnClickGoWorkFlowRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = onClickGoWorkFlowRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + var mergedUpRewriter = new MergedUpRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + modifiedSourceNode = mergedUpRewriter.Visit(modifiedSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(sendItemIdRewriter.GetReport()); + CollectMessages(ifWorkFlowRewriter.GetReport()); + CollectMessages(customColumnRewriter.GetReport()); + CollectMessages(elementsNewSyntaxRewriter.GetReport()); + CollectMessages(formModuleWorkFlow.GetReport()); + CollectMessages(generalWorkFlowRewriter.GetReport()); + CollectMessages(combinedExpressionsFormRewriter.GetReport()); + CollectMessages(listModuleWorkFlowRewriter.GetReport()); + CollectMessages(fullSearchRewriter.GetReport()); + CollectMessages(customFieldRewriter.GetReport()); + CollectMessages(onClickGoWorkFlowRewriter.GetReport()); + CollectMessages(mergedUpRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + // cancelsave,deletecancelsave + private class CombinedExpressionsFormRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public CombinedExpressionsFormRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + private class StatementType + { + public int Index { get; set; } + public ExpressionStatementSyntax Row { get; set; } + public string MethodName { get; set; } + public List RemovedIndexPositions { get; set; } + } + + private IEnumerable GetStatementTypesContainMethodName( + IEnumerable statementTypes, string methodName) => + statementTypes.Where(x => x.MethodName == methodName) + .Where(x => x.Row.DescendantNodesOfType().Count() == 1) + .Where(x => x.Row.IdentifierShouldBe("button")) + .Where(x => x.Row.MethodNameShouldBe(methodName)); + //{ + // return statementTypes.Where(x => x.MethodName == methodName) + // .Where(x => x.Row.DescendantNodesOfType().Count() == 1) + // .Where(x => x.Row.IdentifierShouldBe("button")) + // .Where(x => x.Row.MethodNameShouldBe(methodName)); + //} + + public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + var t = node.DescendantNodesOfType() + .Select((r, i) => new StatementType + { + Row = r, + Index = i, + MethodName = (CSharpExtensions.GetSymbolInfo(_semanticModel, r.Expression).Symbol as IMethodSymbol)?.Name + }); + + var deleteMethods = GetStatementTypesContainMethodName(t, "Delete"); + var cancelMethods = GetStatementTypesContainMethodName(t, "Cancel"); + var saveMethods = GetStatementTypesContainMethodName(t, "Save"); + + var deleteIndex = -1; + + if (deleteMethods.Any()) + { + deleteIndex = deleteMethods.FirstOrDefault().Index; + } + + if (!cancelMethods.Any() || !saveMethods.Any()) + { + return base.VisitConstructorDeclaration(node); + } + + var statementsToRemove = new List(); + + cancelMethods.Union(saveMethods) + .Union(deleteMethods) + .OrderByDescending(x => x.Index) + .Aggregate((arg1, arg2) => + { + if (arg1.MethodName == "Save" && arg2.MethodName == "Cancel") + { + statementsToRemove.Add(arg1.Index); + + return new StatementType + { + Index = arg2.Index, + Row = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName("CancelSave")), + SyntaxFactory.ArgumentList())), + MethodName = "CancelSave", + RemovedIndexPositions = new List() { arg1.Index } + }; + } + + if (arg1.MethodName == "CancelSave" && arg2.MethodName == "Delete") + { + statementsToRemove.AddRange(arg1.RemovedIndexPositions); + statementsToRemove.Add(arg1.Index); + + return new StatementType + { + Index = arg2.Index, + Row = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName("DeleteCancelSave")), + SyntaxFactory.ArgumentList())), + MethodName = "DeleteCancelSave", + RemovedIndexPositions = new List(arg1.RemovedIndexPositions) { arg1.Index } + }; + } + + return arg2; + }); + + if (!statementsToRemove.Any()) + return base.VisitConstructorDeclaration(node); + + var newNode = node.RemoveNodes(cancelMethods.Union(saveMethods).Union(deleteMethods) + .Where(x => statementsToRemove.Contains(x.Index)).Select(x => x.Row), + SyntaxRemoveOptions.KeepEndOfLine); + + t = newNode.DescendantNodesOfType() + .Select((r, i) => new StatementType + { + Row = r, + Index = i, + MethodName = r.FirstDescendantNode().Name.ToString() + }); + + deleteMethods = GetStatementTypesContainMethodName(t, "Delete"); + cancelMethods = GetStatementTypesContainMethodName(t, "Cancel"); + saveMethods = GetStatementTypesContainMethodName(t, "Save"); + + newNode = newNode.ReplaceNodes(cancelMethods.Union(saveMethods).Union(deleteMethods) + .Select(x => x.Row), + (arg1, arg2) => + { + if (arg1.MethodNameShouldBe("Delete")) + { + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName("DeleteCancelSave")), + SyntaxFactory.ArgumentList())); + } + + if (arg1.MethodNameShouldBe("Cancel")) + { + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName("CancelSave")), + SyntaxFactory.ArgumentList())); + } + + return arg2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use button.CancelSave or button.DeleteCancelSave shortcut method", + Generator = nameof(CombinedExpressionsFormRewriter) + }); + + return newNode; + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("FormModule")) + return base.VisitClassDeclaration(node); + + return node; + } + } + + private class IfWorkFlowRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public IfWorkFlowRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) + { + var ifNode = node.DescendantNodesOfType() + .Where(x => + x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + x.MethodNameShouldBe("If") && + x.LeftSideShouldBeIdentifier(false)) + .FirstOrDefault(); + + if (ifNode != null) + { + var newNode = node.ReplaceNodes(ifNode.DescendantNodesAndSelfOfType(), + (nde1, nde2) => + { + if (nde1.MethodNameShouldBe("If") && + nde1.LeftSideShouldBeIdentifier(false)) + { + return nde2.FirstDescendantNode(); + } + else if (nde1.MethodNameShouldBe("If") && + nde1.LeftSideShouldBeIdentifier()) + { + return nde2; + } + + if (nde1.LeftSideShouldBeIdentifier()) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression(nde1.GetLeftSideIdentifier()), + SyntaxFactory.IdentifierName("If")) + , SyntaxFactory.ArgumentList(ifNode.ArgumentList.Arguments)), nde1.GetRightSideNameSyntax()), + SyntaxFactory.ArgumentList(nde1.ArgumentList.Arguments)).WithLeadingTrivia(nde1.GetLeadingTrivia()); + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "if method should be first invocation", + Generator = nameof(IfWorkFlowRewriter) + }); + + return nde2; + }); + + return VisitSimpleLambdaExpression(newNode); + } + + return node; + } + } + + private class CustomColumnRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public CustomColumnRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var customColumnNode = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "CustomColumn").FirstOrDefault(); + + if (customColumnNode != null && + (node.GetArgumentsOfMethod("LabelText") != null || node.GetArgumentsOfMethod("HeaderTemplate") != null) && + node.GetArgumentsOfMethod("DisplayExpression") != null) + { + var newNode = node.ReplaceNodes(node.DescendantNodesAndSelfOfType(), + (nde1, nde2) => + { + if (nde1.Expression.As()?.Identifier.ValueText == "CustomColumn") + { + SeparatedSyntaxList args = new SeparatedSyntaxList(); + + var argsLabelText = node.GetArgumentsOfMethod("LabelText") + ?? node.GetArgumentsOfMethod("HeaderTemplate"); + + var argsDisplayExpr = node.GetArgumentsOfMethod("DisplayExpression"); + + if (argsLabelText?.Arguments.Count == 1) + args = args.Add(argsLabelText.Arguments.FirstOrDefault()); + + if (argsDisplayExpr?.Arguments.Count == 1) + args = args.Add(argsDisplayExpr.Arguments.FirstOrDefault()); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("column"), + SyntaxFactory.IdentifierName("Custom")), + SyntaxFactory.ArgumentList(args)) + .WithLeadingTrivia(nde1.GetLeadingTrivia()); + } + else if (nde1.MethodNameShouldBeIn(new string[] { "LabelText", "HeaderTemplate", "DisplayExpression" }) + && nde1.ArgumentsCountShouldBe(1)) + { + return nde2.FirstDescendantNode(); + } + + return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use column.Custom instead of long invocation", + Generator = nameof(CustomColumnRewriter) + }); + + return newNode; + } + + return base.VisitInvocationExpression(node); + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("ListModule")) + return base.VisitClassDeclaration(node); + + return node; + } + } + + private class SendItemIdRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public SendItemIdRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var methodSymbol = CSharpExtensions.GetSymbolInfo(_semanticModel, node).Symbol as IMethodSymbol; + var methodName = methodSymbol?.Name; + + if (methodName == "Send") + { + if (node.ArgumentsCountShouldBe(2) && + node.FirstArgumentShouldBe("\"item\"") && + node.LastArgumentShouldBe("\"item.ID\"")) + { + var member = node.FirstDescendantNode(); + + member = member + .WithTrailingTrivia(member.GetTrailingTrivia().Union(node.GetLeadingTrivia())); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Send Method Should Convert to SendItemId", + Generator = nameof(SendItemIdRewriter) + }); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, member, SyntaxFactory.IdentifierName("SendItemId")), + SyntaxFactory.ArgumentList()); + } + } + + return base.VisitInvocationExpression(node); + } + } + + // column.search.field. + private class ElementsNewSyntaxRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public ElementsNewSyntaxRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var methodSymbol = CSharpExtensions.GetSymbolInfo(_semanticModel, node).Symbol as IMethodSymbol; + var methodName = methodSymbol?.Name; + var methodType = methodSymbol?.ReturnType.OriginalDefinition?.ToString(); + + var methodDefTypes = new string[] { + "MSharp.PropertyFilterElement", + "MSharp.ViewElement", + "MSharp.ViewElement", + "MSharp.NumberFormElement", + "MSharp.BooleanFormElement", + "MSharp.DateTimeFormElement", + "MSharp.StringFormElement", + "MSharp.AssociationFormElement", + "MSharp.BinaryFormElement", + "MSharp.CommonFilterElement" + }; + + var methodDefNames = new string[] { "Field", "Column", "Search" }; + + if (methodDefTypes.Contains(methodType) && methodDefNames.Contains(methodName)) + { + if (node.ArgumentsCountShouldBe(1) && + node.ArgumentList.Arguments[0].Expression is SimpleLambdaExpressionSyntax && + node.ArgumentList.Arguments[0].Expression.As() + ?.Body is MemberAccessExpressionSyntax && + node.ArgumentList.Arguments[0].Expression.As() + .Body.As().LeftSideShouldBeIdentifier()) + { + var arg = node.ArgumentList.Arguments[0].Expression.As()? + .Body.As()?.Name; + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use new syntax of elements", + Generator = nameof(ElementsNewSyntaxRewriter) + }); + + return SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + node.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) ? + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + node.GetLeftSideExpression(), + SyntaxFactory.IdentifierName(node.GetRightSideNameSyntax()?.ToString().ToLower())) + : SyntaxFactory.ParseExpression(methodName.ToLower()), + SyntaxFactory.IdentifierName(arg.ToString()))) + .WithLeadingTrivia(node.GetLeadingTrivia()); + } + } + + if (methodType == "MSharp.ListButton" || methodType == "MSharp.ModuleButton") + { + SyntaxNode newNode = null; + + switch (methodName) + { + case "ButtonColumn": + newNode = node.WithExpression(SyntaxFactory.ParseExpression("column.Button")) + .WithLeadingTrivia(node.GetLeadingTrivia()); + break; + case "LinkColumn": + newNode = node.WithExpression(SyntaxFactory.ParseExpression("column.Link")) + .WithLeadingTrivia(node.GetLeadingTrivia()); + break; + case "SearchButton": + newNode = node.WithExpression(SyntaxFactory.ParseExpression("search.Button")) + .WithLeadingTrivia(node.GetLeadingTrivia()); + break; + } + + if (newNode != null) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use new syntax of elements", + Generator = nameof(ElementsNewSyntaxRewriter) + }); + + return newNode; + } + } + + return base.VisitInvocationExpression(node); + } + } + + // cancel/save/delete + private class FormModuleWorkFlowRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public FormModuleWorkFlowRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var s = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "OnClick").FirstOrDefault(); + + if (s == null) + return base.VisitInvocationExpression(node); + + var methodSymbol = (CSharpExtensions.GetSymbolInfo(_semanticModel, s).Symbol as IMethodSymbol); + + if (s.DescendantNodesOfType().Count() == 1 && + methodSymbol?.ReturnType.OriginalDefinition?.ToString() == "MSharp.ModuleButton") + { + var saveMethods = new string[] { "ReturnToPreviousPage", "SaveInDatabase" }; + var saveModalMethods = new string[] { "CloseModal(Refresh.Full)", "SaveInDatabase" }; + var cancelMethods = new string[] { "ReturnToPreviousPage" }; + var cancelModalMethods = new string[] { "CloseModal" }; + var deleteMethods = new string[] { "ReturnToPreviousPage", "DeleteItem" }; + var deleteModalMethods = new string[] { "CloseModal(Refresh.Full)", "DeleteItem" }; + var optionalMethods = new string[] { "GentleMessage(\"Deleted successfully.\")" }; + + var lambdaExpressionArgument = s.FirstDescendantNode(); + var invocations = lambdaExpressionArgument.DescendantNodesOfType(); + + if (invocations.Count() == 1 && + (invocations.All(x => cancelMethods.Any(y => x.ToString().Contains(y))) || + (invocations.All(x => cancelModalMethods.Any(y => x.ToString().Contains(y)))))) + { + var m = node; + var methodSelector = invocations.Any(x => x.ToString().Contains("ReturnToPreviousPage")); + + var newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName(methodSelector ? "Cancel" : "ModalCancel")), + SyntaxFactory.ArgumentList()); + + while (m != null && m.ChildNodes().Any()) + { + var m2 = m.ChildNodes(); + + if (m2.FirstOrDefault() is MemberAccessExpressionSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.MethodNameShouldBe("Icon") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("FA.Backward"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("CausesValidation") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("false"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("OnClick") && (!arguments.ArgumentsCountShouldBe(1) || + ((!arguments.FirstArgumentShouldContains("ReturnToPreviousPage")) && + (!arguments.FirstArgumentShouldContains("CloseModal"))))) + { + return base.VisitInvocationExpression(node); + } + else if (!methodName.MethodNameShouldBeIn(new string[] { "OnClick", "CausesValidation", "Icon" })) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + + m = m2.FirstOrDefault().As()?.Expression + .As(); + } + else if (m2.FirstOrDefault() is IdentifierNameSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.ToString() == "Button" && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"Cancel\""))) + { + return base.VisitInvocationExpression(node); + } + + m = m2.FirstOrDefault().As(); + } + else return node; + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use Cancel or ModalCancel method instead", + Generator = nameof(FormModuleWorkFlowRewriter) + }); + + return newExpression + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + else if ((invocations.Count() == 2 || invocations.Count() == 3) && + invocations.All(x => x.Expression is MemberAccessExpressionSyntax && + x.LeftSideShouldBeIdentifier()) && + (invocations.All(x => saveMethods.Any(y => x.ToString().Contains(y))) || + invocations.All(x => saveModalMethods.Any(y => x.ToString().Contains(y))))) + { + var m = node; + var methodSelector = invocations.Any(x => x.ToString().Contains("ReturnToPreviousPage")); + + var newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName(methodSelector ? "Save" : "ModalSave")), + SyntaxFactory.ArgumentList()); + + while (m != null && m.ChildNodes().Any()) + { + var m2 = m.ChildNodes(); + + if (m2.FirstOrDefault() is MemberAccessExpressionSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.MethodNameShouldBe("Icon") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("FA.Check"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("CausesValidation") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("true"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("OnClick") && ((!arguments.ArgumentsCountShouldBe(1)) || + !arguments.GetBlockSyntaxOfFirstArgument() + .Statements.Any(x => saveMethods.Any(y => x.ToString().Contains(y))) || + !arguments.GetBlockSyntaxOfFirstArgument() + .Statements.Any(x => saveModalMethods.Any(y => x.ToString().Contains(y))))) + { + return base.VisitInvocationExpression(node); + } + else if (!methodName.MethodNameShouldBeIn(new string[] { "OnClick", "CausesValidation", "Icon" })) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + + m = m2.FirstOrDefault().As()?.Expression + .As(); + } + else if (m2.FirstOrDefault() is IdentifierNameSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.ToString() == "Button" && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"Save\""))) + { + return base.VisitInvocationExpression(node); + } + + m = m2.FirstOrDefault().As(); + } + else return node; + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use Save or ModalSave method instead", + Generator = nameof(FormModuleWorkFlowRewriter) + }); + + return newExpression + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + else if ((invocations.Count() == 2 || invocations.Count() == 3) && + invocations.All(x => x.Expression is MemberAccessExpressionSyntax && + x.LeftSideShouldBeIdentifier()) && + (invocations.All(x => deleteMethods.Union(optionalMethods).Any(y => x.ToString().Contains(y))) || + invocations.All(x => deleteModalMethods.Union(optionalMethods).Any(y => x.ToString().Contains(y))))) + { + var m = node; + var methodSelector = invocations.Any(x => x.ToString().Contains("ReturnToPreviousPage")); + + var newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName(methodSelector ? "Delete" : "ModalDelete")), + SyntaxFactory.ArgumentList()); + + while (m != null && m.ChildNodes().Any()) + { + var m2 = m.ChildNodes(); + + if (m2.FirstOrDefault() is MemberAccessExpressionSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.MethodNameShouldBe("VisibleIf") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("CommonCriterion.IsEditMode_Item_IsNew"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("Icon") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("FA.Close"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("ConfirmQuestion") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"Are you sure you want to delete it?\""))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("OnClick") && ((!arguments.ArgumentsCountShouldBe(1)) || + !arguments.GetBlockSyntaxOfFirstArgument() + .Statements.Any(x => deleteMethods.Any(y => x.ToString().Contains(y))) || + !arguments.GetBlockSyntaxOfFirstArgument() + .Statements.Any(x => deleteModalMethods.Any(y => x.ToString().Contains(y))))) + { + return base.VisitInvocationExpression(node); + } + else if (!methodName.MethodNameShouldBeIn(new string[] { + "OnClick","VisibleIf","Icon","ConfirmQuestion"})) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + + m = m2.FirstOrDefault().As()?.Expression + .As(); + } + else if (m2.FirstOrDefault() is IdentifierNameSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.ToString() == "Button" && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"Delete\""))) + { + return base.VisitInvocationExpression(node); + } + + m = m2.FirstOrDefault().As(); + } + else return node; + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use Delete or ModalDelete method instead", + Generator = nameof(FormModuleWorkFlowRewriter) + }); + + return newExpression + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + } + + return base.VisitInvocationExpression(node); + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("FormModule")) + return base.VisitClassDeclaration(node); + + return node; + } + } + + // onlick-go -> go + private class GeneralWorkFlowRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public GeneralWorkFlowRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var s = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "OnClick").FirstOrDefault(); + + if (s == null) + return base.VisitInvocationExpression(node); + + var methodSymbol = (CSharpExtensions.GetSymbolInfo(_semanticModel, s).Symbol as IMethodSymbol); + + if (s.ArgumentsCountShouldBe(1) && + s.FirstArgument().Expression.IsKind(SyntaxKind.SimpleLambdaExpression) && + (s.FirstArgument().Expression.As().Block != null ? + s.FirstArgument().DescendantNodesOfType().Count() == 1 : + s.FirstArgument().Expression.As() + .ExpressionBody.IsKind(SyntaxKind.InvocationExpression)) && + s.FirstArgument() + .DescendantNodesOfType().Any(x => x.Identifier.ToString() == "Go")) + { + var goIdentifier = s.FirstArgument() + .DescendantNodesOfType() + .FirstOrDefault(x => x.Identifier.ToString() == "Go"); + + var newNode = node.ReplaceNodes(s.FirstArgument() + .DescendantNodesOfType(), + (nde1, nde2) => + { + if (nde1.Expression.As()?.Name is GenericNameSyntax && + nde1.MethodNameShouldBe("Go") && + nde1.ArgumentsCountShouldBe(0)) + { + if (nde1.LeftSideShouldBeIdentifier(false)) + return nde2.FirstDescendantNode(); + else if (!nde1.Parent.IsKind(SyntaxKind.ExpressionStatement)) + return nde2.FirstDescendantNode(); + else return SyntaxFactory.ParseExpression(""); + } + + return nde2; + }); + + newNode = newNode.ReplaceNode(newNode.DescendantNodesAndSelfOfType() + .FirstOrDefault(x => x.Identifier.ToString() == "OnClick"), + SyntaxFactory.GenericName(goIdentifier.Identifier, goIdentifier.TypeArgumentList)); + + var argument = newNode.DescendantNodesAndSelfOfType() + .FirstOrDefault(x => x.MethodNameShouldBe("Go")) + .FirstArgument().Expression.As(); + + if (argument != null && argument.Body.IsKind(SyntaxKind.IdentifierName)) + { + newNode = newNode.ReplaceNode(newNode.DescendantNodesAndSelfOfType() + .FirstOrDefault(x => x.MethodNameShouldBe("Go")).ArgumentList, SyntaxFactory.ArgumentList()); + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use Go method instead", + Generator = nameof(GeneralWorkFlowRewriter) + }); + + return newNode; + } + + return base.VisitInvocationExpression(node); + } + } + + private class ListModuleWorkFlowRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public ListModuleWorkFlowRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var s = node.DescendantNodesAndSelfOfType() + .Where(x => ((CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "Go" && + x.FirstAncestorOrSelf() == null) || + (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "OnClick" || + (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "Link").FirstOrDefault(); + + if (s == null) + return base.VisitInvocationExpression(node); + + var editRequiredArguments = new string[] { "SendItemId", "SendReturnUrl" }; + var newRequiredArguments = new string[] { "SendReturnUrl" }; + var methodSymbol = (CSharpExtensions.GetSymbolInfo(_semanticModel, s).Symbol as IMethodSymbol); + + if (s.MethodNameShouldBe("Go") && + s.DescendantNodesOfType().Count() == 1 && + s.ArgumentsCountShouldBe(1) && + editRequiredArguments.All(x => (s.FirstArgument().DescendantNodesOfType() + .Select(y => y.GetRightSideNameSyntax()?.ToString())).Any(y => y == x)) && + s.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.LeftSideShouldBeIdentifier() && + x.GetLeftSideExpression().As()?.Identifier.ToString() == "column").Count() == 1 && + node.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Icon") && + x.FirstArgumentShouldBe("FA.Edit")).Count() == 1 && + node.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Button") && + x.FirstArgumentShouldBe("\"Edit\"")).Count() == 1) + { + var m = node; + + var newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("column"), + SyntaxFactory.GenericName(SyntaxFactory.Identifier("Edit"), + s.Expression.As()?.Name.As()?.TypeArgumentList)), + SyntaxFactory.ArgumentList()); + + while (m != null && m.ChildNodes().Any()) + { + var m2 = m.ChildNodes(); + + if (m2.FirstOrDefault() is MemberAccessExpressionSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.MethodNameShouldBe("Go") && ((!arguments.ArgumentsCountShouldBe(1)) || + !(editRequiredArguments.All(x => arguments.FirstArgument() + .Expression.DescendantNodesOfType() + .FirstOrDefault().ToString().Contains(x))))) + { + return base.VisitInvocationExpression(node); + } + else if (methodName.MethodNameShouldBe("Icon") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("FA.Edit"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("NoText") && (!arguments.ArgumentsCountShouldBe(0))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("HeaderText") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"Actions\""))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("GridColumnCssClass") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"actions\""))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (methodName.MethodNameShouldBe("Button") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"Edit\""))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if ( + !methodName.MethodNameShouldBeIn(new string[] { + "Go","GridColumnCssClass","HeaderText","NoText", + "Icon" , "Button", "Edit" })) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + + m = m2.FirstOrDefault().As()?.Expression + .As(); + } + else return node; + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use column.Edit method instead", + Generator = nameof(ListModuleWorkFlowRewriter) + }); + + return newExpression + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + else if (s.MethodNameShouldBe("Go") && + s.DescendantNodes().OfType().Count() == 1 && + s.ArgumentsCountShouldBe(1) && + newRequiredArguments.All(x => (s.FirstArgument().DescendantNodesOfType() + .Select(y => y.GetRightSideNameSyntax()?.ToString())).Any(y => y == x)) && + s.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.LeftSideShouldBeIdentifier() && + x.GetLeftSideExpression().As()?.Identifier.ToString() == "column").Count() == 1 && + node.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Icon") && + x.FirstArgumentShouldBe("FA.Plus")).Count() == 1 && + node.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Button") && + x.FirstArgumentShouldBe("\"New\"")).Count() == 1) + { + var m = node; + + var newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("button"), + SyntaxFactory.GenericName(SyntaxFactory.Identifier("New"), + s.Expression.As()?.Name.As()?.TypeArgumentList)), + SyntaxFactory.ArgumentList()); + + while (m != null && m.ChildNodes().Any()) + { + var m2 = m.ChildNodes(); + + if (m2.FirstOrDefault() is MemberAccessExpressionSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.Name.Identifier.ToString() == "Go" && ((!arguments.ArgumentsCountShouldBe(1)) || + !(newRequiredArguments.All(x => arguments.FirstArgument() + .Expression.DescendantNodesOfType() + .FirstOrDefault().ToString().Contains(x))))) + { + return base.VisitInvocationExpression(node); + } + else if (methodName.MethodNameShouldBe("Icon") && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("FA.Plus"))) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + else if (!methodName.MethodNameShouldBeIn(new string[] { "Go", "Icon", "New" })) + { + newExpression = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, newExpression, methodName.Name), arguments); + } + + m = m2.FirstOrDefault().As()?.Expression + .As(); + } + else if (m2.FirstOrDefault() is IdentifierNameSyntax && + m2.LastOrDefault() is ArgumentListSyntax) + { + var methodName = m2.FirstOrDefault().As(); + var arguments = m2.LastOrDefault().As(); + + if (methodName.ToString() == "Button" && (!arguments.ArgumentsCountShouldBe(1) + || !arguments.FirstArgumentShouldBe("\"New\""))) + { + return base.VisitInvocationExpression(node); + } + + m = m2.FirstOrDefault().As(); + } + else return node; + } + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use button.New method instead", + Generator = nameof(ListModuleWorkFlowRewriter) + }); + + return newExpression + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + else if (s.ArgumentsCountShouldBe(1) && + (s.FirstArgument().Expression.As()?.ExpressionBody != null || + (s.FirstArgument().Expression.As()?.Block != null && + s.FirstArgument().Expression.As()?.Block.Statements.Count() == 1)) && + s.FirstArgument() + .DescendantNodesOfType().Any(x => x.Identifier.ToString() == "Export") && + s.DescendantNodesOfType().Where(x => x.Expression is IdentifierNameSyntax && + x.Expression.As()?.Identifier.ToString() == "Button" && + x.ArgumentsCountShouldBe(1) && + x.FirstArgumentShouldBe("\"Export\"")).Count() == 1) + { + var exportInvocation = s.DescendantNodesOfType() + .FirstOrDefault(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Export")); + + var neededArguments = exportInvocation.ArgumentList; + + var newNode = node.ReplaceNodes(s.DescendantNodesAndSelfOfType(), + (nde1, nde2) => + { + if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.GetRightSideNameSyntax() is IdentifierNameSyntax && + nde1.MethodNameShouldBe("OnClick") && + nde1.ArgumentsCountShouldBe(1)) + { + if (nde1.LeftSideShouldBeIdentifier(false)) + return nde2.FirstDescendantNode(); + else return nde2.FirstDescendantNode(); + } + else if (nde1.Expression is IdentifierNameSyntax && + ((IdentifierNameSyntax)nde1.Expression).Identifier.ToString() == "Button" && + nde1.ArgumentsCountShouldBe(1) && + nde1.FirstArgumentShouldBe("\"Export\"")) + { + return SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ParseExpression("button"), + SyntaxFactory.IdentifierName("Export")), neededArguments) + .WithLeadingTrivia(nde1.GetLeadingTrivia()) + .WithTrailingTrivia(nde1.GetTrailingTrivia()); + } + + return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use button.Export method instead", + Generator = nameof(ListModuleWorkFlowRewriter) + }); + + return newNode.WithoutTrailingTrivia(); + } + else if (s.ArgumentsCountShouldBe(1) && + (s.FirstArgument() + .DescendantNodesOfType().Count() == 1 || + s.FirstArgument() + .DescendantNodesOfType().Count() == 1) && + s.FirstArgument() + .DescendantNodesOfType().Any(x => x.Identifier.ToString() == "Reload") && + s.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("SearchButton") && + x.ArgumentsCountShouldBe(1) && + x.FirstArgumentShouldBe("\"Search\"")).Count() == 1 && + s.DescendantNodesOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Icon") && + x.ArgumentsCountShouldBe(1) && + x.FirstArgumentShouldBe("FA.Search")).Count() == 1) + { + var iconInvocation = s.DescendantNodesOfType() + .FirstOrDefault(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Icon")); + + var neededArguments = iconInvocation != null ? iconInvocation.ArgumentList : null; + + var newNode = node.ReplaceNodes(s.DescendantNodesAndSelfOfType(), + (nde1, nde2) => + { + if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.GetRightSideNameSyntax() is IdentifierNameSyntax && + nde1.MethodNameShouldBe("OnClick") && + nde1.ArgumentsCountShouldBe(1)) + { + if (nde1.LeftSideShouldBeIdentifier(false)) + return nde2.FirstDescendantNode(); + else return nde2.FirstDescendantNode(); + } + else if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.GetRightSideNameSyntax() is IdentifierNameSyntax && + nde1.MethodNameShouldBe("Icon") && + nde1.ArgumentsCountShouldBe(1)) + { + if (nde1.LeftSideShouldBeIdentifier(false)) + return nde2.FirstDescendantNode(); + else return nde2.FirstDescendantNode(); + } + else if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.GetRightSideNameSyntax() is IdentifierNameSyntax && + nde1.MethodNameShouldBe("NoText") && + nde1.ArgumentsCountShouldBe(1)) + { + if (nde1.LeftSideShouldBeIdentifier(false)) + return nde2.FirstDescendantNode(); + else return nde2.FirstDescendantNode(); + } + else if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.MethodNameShouldBe("SearchButton") && + nde1.ArgumentsCountShouldBe(1) && + nde1.FirstArgumentShouldBe("\"Search\"")) + { + SeparatedSyntaxList args = new SeparatedSyntaxList(); + args = args.AddRange(neededArguments.Arguments.Where(x => x.Expression.ToString() != "FA.Search")); + + return SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ParseExpression("search"), + SyntaxFactory.IdentifierName("Icon")), SyntaxFactory.ArgumentList(args)) + .WithLeadingTrivia(nde1.GetLeadingTrivia()) + .WithTrailingTrivia(nde1.GetTrailingTrivia()); + } + + return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use search.Icon method instead", + Generator = nameof(ListModuleWorkFlowRewriter) + }); + + return newNode; + } + else if (node.DescendantNodesAndSelfOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.LeftSideShouldBeIdentifier() && + x.GetLeftSideExpression().As()?.Identifier.ToString() == "column").Count() == 1 && + node.DescendantNodesAndSelfOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("HeaderText") && + x.ArgumentsCountShouldBe(1)).Count() == 1 && + node.DescendantNodesAndSelfOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Link") && + x.ArgumentsCountShouldBe(1)).Count() == 1) + { + var linkMethod = node.DescendantNodesAndSelfOfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("Link") && + x.ArgumentsCountShouldBe(1)).FirstOrDefault(); + + var headerTextMethod = node.DescendantNodesAndSelf().OfType().Where(x => x.Expression is MemberAccessExpressionSyntax && + x.MethodNameShouldBe("HeaderText") && + x.ArgumentsCountShouldBe(1)).FirstOrDefault(); + + if (linkMethod.FirstArgument().ToString() + .Contains("item." + headerTextMethod.ArgumentList.Arguments.FirstOrDefault() + .ToString().Trim('"'))) + { + var args = new SeparatedSyntaxList(); + + args = args.Add(SyntaxFactory.Argument( + SyntaxFactory.SimpleLambdaExpression(SyntaxFactory.Parameter( + SyntaxFactory.ParseToken("x")), null, + SyntaxFactory.MemberAccessExpression + (SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ParseExpression("x"), + SyntaxFactory.IdentifierName(headerTextMethod.ArgumentList.Arguments.FirstOrDefault() + .ToString().Trim('"')))))); + + var newNode = node.ReplaceNodes(node.DescendantNodesAndSelf().OfType(), + (nde1, nde2) => + { + if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.GetRightSideNameSyntax() is IdentifierNameSyntax && + nde1.GetRightSideNameSyntax().Identifier.ToString() == "HeaderText" && + nde1.ArgumentsCountShouldBe(1)) + { + if (nde1.LeftSideShouldBeIdentifier(false)) + return nde2.FirstDescendantNode(); + else return nde2.FirstDescendantNode(); + } + else if (nde1.Expression is MemberAccessExpressionSyntax && + nde1.MethodNameShouldBe("Link") && + nde1.ArgumentsCountShouldBe(1)) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression + , SyntaxFactory.ParseExpression("column"), + SyntaxFactory.IdentifierName("Link")), + SyntaxFactory.ArgumentList(args)) + .WithLeadingTrivia(nde1.GetLeadingTrivia()) + .WithTrailingTrivia(nde1.GetTrailingTrivia()); + } + + return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use column.Link method instead", + Generator = nameof(ListModuleWorkFlowRewriter) + }); + + return newNode; + } + } + + return base.VisitInvocationExpression(node); + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("ListModule")) + return base.VisitClassDeclaration(node); + + return node; + } + } + + private class FullSearchRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + private Solution _solution; + private RoslynDocument _roslynDocument; + + private bool _shouldConvertToFullSearch = false; + public FullSearchRewriter(SemanticModel semanticModel, + Solution solution, + RoslynDocument roslynDocument, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) + { + _semanticModel = semanticModel; + _solution = solution; + _roslynDocument = roslynDocument; + } + + public override SyntaxNode VisitVariableDeclaration(VariableDeclarationSyntax node) + { + var declarationSymbol = CSharpExtensions.GetSymbolInfo(_semanticModel, node.Type).Symbol; + var identifierName = CSharpExtensions.GetDeclaredSymbol(_semanticModel, node.Variables.FirstOrDefault()); + + if (declarationSymbol?.Name == "ModuleButton" && node.Variables.Count() == 1) + { + if (node.Variables.FirstOrDefault().Initializer.Value is InvocationExpressionSyntax && + node.Variables.FirstOrDefault() + .Initializer.Value.As().MethodNameShouldBe("Icon")) + { + var invocation = node.Variables.FirstOrDefault().Initializer.Value + .As(); + + IEnumerable result = null; + + Task.Run(async delegate + { + result = await GetReferencedSymbolsAsync(identifierName); + }).GetAwaiter().GetResult(); + + if (result.Count() == 1 && invocation.ArgumentsCountShouldBe(0)) + { + var nextNode = node.SyntaxTree.GetRoot().FindNode(result.FirstOrDefault().Locations + .FirstOrDefault().Location.SourceSpan); + + if (nextNode.Ancestors().OfType().Any()) + { + var isNextInvocOk = CheckNextInvocationExpression(nextNode.Ancestors() + .OfType().FirstOrDefault(), + identifierName.ToString()); + + if (isNextInvocOk) + { + _shouldConvertToFullSearch = true; + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use search.FullWithIcon method instead", + Generator = nameof(FullSearchRewriter) + }); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("search"), + SyntaxFactory.IdentifierName("FullWithIcon")), + SyntaxFactory.ArgumentList()) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + } + } + } + } + } + + return base.VisitVariableDeclaration(node); + } + + public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) + { + if (node.Declaration is VariableDeclarationSyntax) + { + var t = VisitVariableDeclaration(node.Declaration); + + if (t.IsKind(SyntaxKind.InvocationExpression)) + return SyntaxFactory.ExpressionStatement(t.As()) + .WithTrailingTrivia(node.GetTrailingTrivia()) + .WithLeadingTrivia(node.GetLeadingTrivia()); + } + + return base.VisitLocalDeclarationStatement(node); + } + + public override SyntaxNode VisitVariableDeclarator(VariableDeclaratorSyntax node) + { + return base.VisitVariableDeclarator(node); + } + + public bool CheckNextInvocationExpression(InvocationExpressionSyntax node + , string identifierString) + { + var s = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "AfterControlAddon").FirstOrDefault(); + + while (s.Expression != null) + { + if (s.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + { + var memberExpression = s.Expression.As(); + + if (memberExpression.MethodNameShouldBe("AfterControlAddon") && + s.ArgumentsCountShouldBe(1) && + s.FirstArgumentShouldBe($"{identifierString}.Ref")) + { + s = memberExpression.Expression.As(); + continue; + } + } + else if (s.Expression.IsKind(SyntaxKind.IdentifierName)) + { + var identifierExpression = s.Expression.As(); + + if (identifierExpression.Identifier.ToString() == "Search" && + s.ArgumentsCountShouldBe(1) && + s.FirstArgumentShouldBe("GeneralSearch.AllFields")) + { + return true; + } + } + + return false; + } + + return false; + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("ListModule")) + return base.VisitClassDeclaration(node); + + return node; + } + + public Task> GetReferencedSymbolsAsync(ISymbol symbol) + { + return SymbolFinder.FindReferencesAsync(symbol, + _solution, documents: + ImmutableHashSet.Create(_roslynDocument)); + } + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var s = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "AfterControlAddon").FirstOrDefault(); + + if (s != null && _shouldConvertToFullSearch) + { + _shouldConvertToFullSearch = false; + return SyntaxFactory.ParseExpression(""); + } + + return base.VisitInvocationExpression(node); + } + + public override SyntaxNode VisitExpressionStatement(ExpressionStatementSyntax node) + { + var result = base.VisitExpressionStatement(node); + if (result.ToString() == ";") return null; + return result; + } + } + + private class CustomFieldRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public CustomFieldRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var customColumnNode = node.DescendantNodesAndSelfOfType() + .Where(x => (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol)?.Name == "CustomField" + && !x.Ancestors().Contains(node.ArgumentList)).FirstOrDefault(); + + if (customColumnNode != null) + { + var newNode = node.ReplaceNodes(node.DescendantNodesAndSelfOfType(), + (nde1, nde2) => + { + if (nde1.ToString() == "CustomField()") + { + SeparatedSyntaxList args = new SeparatedSyntaxList(); + + var argsLabel = + node.DescendantNodesAndSelfOfType() + .Any(x => + x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + x.MethodNameShouldBe("Label")) + ? + node.DescendantNodesAndSelfOfType() + .Where(x => x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + x.MethodNameShouldBe("Label")).FirstOrDefault().ArgumentList : null; + + var argsControlType = + node.DescendantNodesAndSelfOfType() + .Any(x => + x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + x.MethodNameShouldBe("Control")) + ? + node.DescendantNodesAndSelfOfType() + .Where(x => + x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + x.MethodNameShouldBe("Control")).FirstOrDefault().ArgumentList + : null; + + var argsPropertyType = + node.DescendantNodesAndSelfOfType() + .Any(x => + x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + x.MethodNameShouldBe("PropertyType")) + ? node.DescendantNodesAndSelfOfType() + .Where(x => x.MethodNameShouldBe("PropertyType")).FirstOrDefault().ArgumentList + : null; + + if (argsLabel == null && argsControlType == null) + { + argsLabel = SyntaxFactory.ArgumentList(new SeparatedSyntaxList().Add( + SyntaxFactory.Argument( + SyntaxFactory.ParseExpression("\"\"")))); + } + + if (argsPropertyType != null && argsControlType == null) + { + argsControlType = SyntaxFactory.ArgumentList(new SeparatedSyntaxList().Add( + SyntaxFactory.Argument( + SyntaxFactory.ParseExpression("ControlType.Textbox")))); + } + + if (argsPropertyType != null && argsLabel == null) + { + argsLabel = SyntaxFactory.ArgumentList(new SeparatedSyntaxList().Add( + SyntaxFactory.Argument( + SyntaxFactory.ParseExpression("\"\"")))); + } + + if (argsLabel != null && argsLabel.ArgumentsCountShouldBe(1)) + args = args.Add(argsLabel.FirstArgument()); + + if (argsControlType != null && argsControlType.ArgumentsCountShouldBe(1)) + args = args.Add(argsControlType.FirstArgument()); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression("field"), + (argsPropertyType != null && + argsPropertyType.Arguments.FirstOrDefault() + .Expression.IsKind(SyntaxKind.StringLiteralExpression) ? + SyntaxFactory.GenericName("Custom") + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList(new SeparatedSyntaxList() + .Add(SyntaxFactory.ParseTypeName(argsPropertyType.FirstArgument() + .ToString().Trim('"'))))) as SimpleNameSyntax + : SyntaxFactory.IdentifierName("Custom") as SimpleNameSyntax)), + SyntaxFactory.ArgumentList(args)) + .WithLeadingTrivia(nde1.GetLeadingTrivia()); + } + else if (nde1.MethodNameShouldBe("Label") && + nde1.ArgumentsCountShouldBe(1)) + { + return node.MethodNameShouldBe("Label") ? + nde2.FirstDescendantNode().WithoutTrailingTrivia() : + nde2.FirstDescendantNode(); + } + else if (nde1.MethodNameShouldBe("Control") && + nde1.ArgumentsCountShouldBe(1)) + { + return node.MethodNameShouldBe("Control") ? + nde2.FirstDescendantNode().WithoutTrailingTrivia() : + nde2.FirstDescendantNode(); + } + else if (nde1.MethodNameShouldBe("PropertyType") && + nde1.ArgumentsCountShouldBe(1) && + nde1.GetArgumentsOfMethod("PropertyType").FirstArgument() + .Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + return + node.MethodNameShouldBe("PropertyType") ? + nde2.FirstDescendantNode().WithoutTrailingTrivia() : + nde2.FirstDescendantNode(); + } + + return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use column.Custom method instead", + Generator = nameof(CustomFieldRewriter) + }); + + return newNode; + } + + return base.VisitInvocationExpression(node); + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("FormModule")) + return base.VisitClassDeclaration(node); + + return node; + } + } + + // onclick,go method should be last invocation + private class OnClickGoWorkFlowRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public OnClickGoWorkFlowRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var listInvocations = new string[] { "OnClick" }; + var listInvocationsWithGo = new string[] { "OnClick", "Go" }; + if (node == null) return node; + + var invocation = node.DescendantNodesOfType() + .Where(x => + //x.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + ( + (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol) != null && + (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol).ReturnsVoid && + (CSharpExtensions.GetSymbolInfo(_semanticModel, x).Symbol as IMethodSymbol).Name == "Go") + || x.MethodNameShouldBeIn(listInvocations)) + .FirstOrDefault(); + + if (invocation != null && + invocation.Ancestors().FirstOrDefault(x => x.IsKind(SyntaxKind.ExpressionStatement)) != null) + { + var newNode = node.ReplaceNodes(invocation.Ancestors().FirstOrDefault(x => x.IsKind(SyntaxKind.ExpressionStatement)) + .DescendantNodesAndSelfOfType(), + (nde1, nde2) => + { + if (nde1.MethodNameShouldBeIn(listInvocationsWithGo) && + !nde1.Parent.IsKind(SyntaxKind.ExpressionStatement)) + { + return nde2.GetLeftSideExpression(); + } + else if (nde1.Parent.IsKind(SyntaxKind.ExpressionStatement) && + !nde1.MethodNameShouldBeIn(listInvocationsWithGo)) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + nde2, + invocation.GetRightSideNameSyntax().WithoutTrailingTrivia()), + invocation.ArgumentList.WithoutTrailingTrivia()); + } + + return nde2; + }); + + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use onclick -> Go should be converted to go method instead", + Generator = nameof(OnClickGoWorkFlowRewriter) + }); + + return newNode; + } + + return base.VisitInvocationExpression(node); + } + } + + private class MergedUpRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.ClassShouldHaveBase() && + node.ClassShouldHaveGenericBase() && + node.GenericClassShouldInheritFrom("ListModule")) + return base.VisitClassDeclaration(node); + + return node; + } + + public MergedUpRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) => _semanticModel = semanticModel; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var listInvocations = new string[] { "NeedsMerging", "SeperatorTemplate" }; + + if (node == null) + return base.VisitInvocationExpression(node); + + var hasBothIdentification = + listInvocations.All(a => node.DescendantNodesAndSelfOfType() + .Select(x => x.GetRightSideNameSyntax()?.Identifier.Text).Contains(a)); + + if (hasBothIdentification && + node.DescendantNodesAndSelfOfType() + .Any(x => x.MethodNameShouldBe("NeedsMerging") && + (x.ArgumentsCountShouldBe(0) || + (x.ArgumentsCountShouldBe(1) && x.FirstArgumentShouldBe("true"))))) + { + var invocations = node.DescendantNodesAndSelfOfType() + .Where(x => x.MethodNameShouldBeIn(listInvocations)); + + var newNode = node.ReplaceNodes(invocations, (invoc1, invoc2) => + { + if (invoc2.MethodNameShouldBeIn(listInvocations)) + { + return invoc2.GetLeftSideExpression(); + } + + return invoc2; + }); + + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "use MergeUp instead of NeedsMerging and SeperatorTemplate", + Generator = nameof(MergedUpRewriter) + }); + } + + return + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + newNode, SyntaxFactory.IdentifierName("MergeUp")), + node.GetArgumentsOfMethod("SeperatorTemplate")); + } + + return base.VisitInvocationExpression(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/Option/CleanupTypes.cs new file mode 100644 index 0000000..95fdc24 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/Option/CleanupTypes.cs @@ -0,0 +1,19 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Remove 'private' from nested classes")] + RemoveNestedClassPrivateModifier = 0x02, + + [CleanupItem(Title = "Remove 'private' from fields")] + RemoveClassFieldsPrivateModifier = 0x04, + + [CleanupItem(Title = "Remove 'private' from methods")] + RemoveClassMethodsPrivateModifier = 0x08, + + [CleanupItem(Title = "Remove 'private' from properties")] + RemoveClassPropertiesPrivateModifier = 0x10, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/Option/Options.cs new file mode 100644 index 0000000..303a7be --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/Option/Options.cs @@ -0,0 +1,11 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() => CodeCleanerType.PrivateAccessModifier; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/PrivateModifierRemover.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/PrivateModifierRemover.cs new file mode 100644 index 0000000..cedf818 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/PrivateModifierRemover/PrivateModifierRemover.cs @@ -0,0 +1,66 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover.Option; +using TidyCSharp.Cli.Menus.Cleanup.TokenRemovers; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover; + +public class PrivateModifierRemover : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return RemoveExplicitPrivateModifiers(initialSourceNode); + } + + private SyntaxNode RemoveExplicitPrivateModifiers(SyntaxNode actualSourceCode) + { + var modifiedSourceNode = actualSourceCode; + + if (CheckOption((int)CleanupTypes.RemoveClassMethodsPrivateModifier)) + { + var remover = new MethodTokenRemover(IsReportOnlyMode); + modifiedSourceNode = remover.Remove(modifiedSourceNode) ?? modifiedSourceNode; + + if (IsReportOnlyMode && !IsEquivalentToUnModified(modifiedSourceNode)) + { + CollectMessages(remover.GetReport()); + } + } + + if (CheckOption((int)CleanupTypes.RemoveClassFieldsPrivateModifier)) + { + var remover = new FieldTokenRemover(IsReportOnlyMode); + modifiedSourceNode = remover.Remove(modifiedSourceNode) ?? modifiedSourceNode; + + if (IsReportOnlyMode && !IsEquivalentToUnModified(modifiedSourceNode)) + { + CollectMessages(remover.GetReport()); + } + } + + if (CheckOption((int)CleanupTypes.RemoveClassPropertiesPrivateModifier)) + { + var remover = new PropertyTokenRemover(IsReportOnlyMode); + modifiedSourceNode = remover.Remove(modifiedSourceNode) ?? modifiedSourceNode; + + if (IsReportOnlyMode && !IsEquivalentToUnModified(modifiedSourceNode)) + { + CollectMessages(remover.GetReport()); + } + } + + if (CheckOption((int)CleanupTypes.RemoveNestedClassPrivateModifier)) + { + var remover = new NestedClassTokenRemover(IsReportOnlyMode); + modifiedSourceNode = remover.Remove(modifiedSourceNode) ?? modifiedSourceNode; + + if (IsReportOnlyMode && !IsEquivalentToUnModified(modifiedSourceNode)) + { + CollectMessages(remover.GetReport()); + } + } + + if (IsReportOnlyMode) return actualSourceCode; + return modifiedSourceNode; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveAttributeKeywork.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveAttributeKeywork.cs new file mode 100644 index 0000000..7f8708a --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveAttributeKeywork.cs @@ -0,0 +1,76 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Extensions; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class RemoveAttributeKeywork : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return RemoveAttributeKeyworkHelper(initialSourceNode, ProjectItemDetails.SemanticModel); + } + + private SyntaxNode RemoveAttributeKeyworkHelper(SyntaxNode initialSourceNode, SemanticModel semanticModel) + { + var syntaxRewriter = new Rewriter(semanticModel, IsReportOnlyMode, Options); + var modifiedSyntaxNode = syntaxRewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(syntaxRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSyntaxNode; + } + + private static string _attributeKeywork = SyntaxKind.Attribute.ToString(); + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + public Rewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) + : base(isReportOnlyMode, options) => _semanticModel = semanticModel; + + public override SyntaxNode VisitAttribute(AttributeSyntax node) + { + if (node.Name is IdentifierNameSyntax newNameNode) + { + if (newNameNode.Identifier.ValueText.EndsWith(_attributeKeywork)) + { + var orginalNodeTypeInfo = CSharpExtensions.GetTypeInfo(_semanticModel, node.Name); + + if (orginalNodeTypeInfo.Type == null) base.VisitAttribute(node); + + if (orginalNodeTypeInfo.Type.Name == newNameNode.Identifier.ValueText) + { + var newName = newNameNode.Identifier.ValueText.TrimEnd(_attributeKeywork); + + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Attributes should not ended with \"Attribute\"", + Generator = nameof(RemoveAttributeKeywork) + }); + } + + node = node.WithName(SyntaxFactory.IdentifierName(newName)); + } + } + } + + return base.VisitAttribute(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/Option/CleanupTypes.cs new file mode 100644 index 0000000..8268333 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/Option/CleanupTypes.cs @@ -0,0 +1,19 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Remove from method calls")] + RemoveFromMethodCall = 0x01, + + // [CleanupItem(Title = "Remove From virual Method Call")] + // Remove_From_virual_Method_Call = 0x02, + + [CleanupItem(Title = "Remove from field calls")] + RemoveFromFieldsCall = 0x04, + + [CleanupItem(Title = "Remove from property calls")] + RemoveFromPropertiesCall = 0x08, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/Option/Options.cs new file mode 100644 index 0000000..a36a69e --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/Option/Options.cs @@ -0,0 +1,16 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public const int MaxFieldDeclarationLength = 80; + + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() + { + return CodeCleanerType.RemoveExtraThisQualification; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/RemoveExtraThisQualification.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/RemoveExtraThisQualification.cs new file mode 100644 index 0000000..e5c32c5 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/RemoveExtraThisQualification/RemoveExtraThisQualification.cs @@ -0,0 +1,107 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification; + +public class RemoveExtraThisQualification : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var rewriter = new Rewriter(ProjectItemDetails, IsReportOnlyMode, Options); + var modifiedSourceNode = rewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(rewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + private readonly SemanticModel _semanticModel; + + public Rewriter(ProjectItemDetailsType projectItemDetails, bool isReportOnlyMode, ICleanupOption options) + : base(isReportOnlyMode, options) + { + _semanticModel = projectItemDetails.SemanticModel; + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node.Parent is ClassDeclarationSyntax == false) + { + node = Remove(node); + } + + return base.VisitClassDeclaration(node); + } + + private ClassDeclarationSyntax Remove(ClassDeclarationSyntax classNode) + { + var thises = classNode.DescendantNodes().OfType(); + var newItems = new Dictionary(); + + foreach (var thisItem in thises) + { + if (thisItem.Parent is MemberAccessExpressionSyntax thisItemAsMemberAccessException) + { + var newAccess = GetMemberAccessWithoutThis(thisItemAsMemberAccessException); + + if (newAccess != null) + { + newItems.Add(thisItemAsMemberAccessException, newAccess); + } + } + } + + if (newItems.Any()) + { + classNode = classNode.ReplaceNodes(newItems.Keys, (node1, node2) => + { + if (IsReportOnlyMode) + { + var lineSpan = node1.GetFileLinePosSpan(); + + AddReport(new ChangesReport(classNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "you can remove this Identifier", + Generator = nameof(RemoveExtraThisQualification) + }); + } + + return newItems[node1]; + }); + } + + return classNode; + } + + private SyntaxNode GetMemberAccessWithoutThis(MemberAccessExpressionSyntax thisItemAsMemberAccessException) + { + var thisItemAsMemberAccessExceptionSymbol = _semanticModel.GetSymbolInfo(thisItemAsMemberAccessException).Symbol; + + if (thisItemAsMemberAccessExceptionSymbol is IFieldSymbol && !CheckOption((int)CleanupTypes.RemoveFromFieldsCall)) return null; + if (thisItemAsMemberAccessExceptionSymbol is IPropertySymbol && !CheckOption((int)CleanupTypes.RemoveFromPropertiesCall)) return null; + if (thisItemAsMemberAccessExceptionSymbol is IMethodSymbol && !CheckOption((int)CleanupTypes.RemoveFromMethodCall)) return null; + + var right = thisItemAsMemberAccessException.Name; + var symbols = _semanticModel.LookupSymbols(thisItemAsMemberAccessException.SpanStart, name: right.Identifier.ValueText); + + if (symbols.Any(x => SymbolEqualityComparer.Default.Equals(x, thisItemAsMemberAccessExceptionSymbol))) + { + return right.WithLeadingTrivia(thisItemAsMemberAccessException.GetLeadingTrivia()); + } + + return null; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/Option/CleanupTypes.cs new file mode 100644 index 0000000..b11b8ae --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/Option/CleanupTypes.cs @@ -0,0 +1,16 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplifyClassFieldDeclarations.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Remove unnecessary explicit \"=0\" or \"=false\" from class fields.")] + RemoveClassFieldsInitializerLiteral = 0x01, + + [CleanupItem(Title = "Remove unnecessary explicit \"=null\" from class fields.")] + RemoveClassFieldsInitializerNull = 0x02, + + [CleanupItem(Title = "Declare multiple class fields [with the same type] on the same line (if total size < 80 chars)", SelectedByDefault = false)] + GroupAndMergeClassFields = 0x04, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/Option/Options.cs new file mode 100644 index 0000000..ec7dccc --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/Option/Options.cs @@ -0,0 +1,16 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplifyClassFieldDeclarations.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public const int MaxFieldDeclarationLength = 80; + + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() + { + return CodeCleanerType.SimplifyClassFieldDeclarations; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/SimplifyClassFieldDeclarations.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/SimplifyClassFieldDeclarations.cs new file mode 100644 index 0000000..cb28bba --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyClassFieldDeclarations/SimplifyClassFieldDeclarations.cs @@ -0,0 +1,287 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplifyClassFieldDeclarations.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplifyClassFieldDeclarations; + +public class SimplifyClassFieldDeclarations : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return SimplifyClassFieldDeclarationsHelper(initialSourceNode, IsReportOnlyMode, Options); + } + + public SyntaxNode SimplifyClassFieldDeclarationsHelper(SyntaxNode initialSourceNode, bool isReportOnlyMode, ICleanupOption options) + { + var rewriter = new Rewriter(isReportOnlyMode, options); + var modifiedSourceNode = rewriter.Visit(initialSourceNode); + + if (isReportOnlyMode) + { + CollectMessages(rewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + public Rewriter(bool isReportOnlyMode, ICleanupOption options) : + base(isReportOnlyMode, options) + { + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (CheckOption((int)CleanupTypes.GroupAndMergeClassFields)) + { + node = Apply(node) as ClassDeclarationSyntax; + return node; + } + + return base.VisitClassDeclaration(node); + } + + public override SyntaxNode VisitVariableDeclarator(VariableDeclaratorSyntax node) + { + if (node.Initializer == null) return base.VisitVariableDeclarator(node); + if (node.Parent is VariableDeclarationSyntax == false) return base.VisitVariableDeclarator(node); + if (node.Parent.Parent is FieldDeclarationSyntax == false) return base.VisitVariableDeclarator(node); + if ((node.Parent.Parent as FieldDeclarationSyntax).Modifiers.Any(x => x.ValueText == "const")) return base.VisitVariableDeclarator(node); + + var value = node.Initializer.Value; + + if + ( + !CheckOption((int)CleanupTypes.RemoveClassFieldsInitializerNull) && + !CheckOption((int)CleanupTypes.RemoveClassFieldsInitializerLiteral) + ) + return base.VisitVariableDeclarator(node); + + if (value is LiteralExpressionSyntax) + { + var variableTypeNode = GetSystemTypeOfTypeNode((node.Parent as VariableDeclarationSyntax)); + var valueObj = (value as LiteralExpressionSyntax).Token.Value; + + if (TypesMapItem.GetAllPredefinedTypesDic().ContainsKey(variableTypeNode)) + { + if (CheckOption((int)CleanupTypes.RemoveClassFieldsInitializerLiteral) == false) return base.VisitVariableDeclarator(node); + + var typeItem = TypesMapItem.GetAllPredefinedTypesDic()[variableTypeNode]; + + if ((typeItem.DefaultValue == null && valueObj != null) || (typeItem.DefaultValue != null && !typeItem.DefaultValue.Equals(valueObj))) + return base.VisitVariableDeclarator(node); + } + else + { + if (CheckOption((int)CleanupTypes.RemoveClassFieldsInitializerNull) == false) return base.VisitVariableDeclarator(node); + if (valueObj != null) return base.VisitVariableDeclarator(node); + } + + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Field initialize with \"= null;\" or \"= 0;\" can be removed", + Generator = nameof(SimplifyClassFieldDeclarations) + }); + } + + node = node.WithInitializer(null).WithoutTrailingTrivia(); + } + + return base.VisitVariableDeclarator(node); + } + + private SyntaxTrivia _spaceTrivia = SyntaxFactory.Whitespace(" "); + + private SyntaxNode Apply(ClassDeclarationSyntax classDescriptionNode) + { + var newDeclarationDic = new Dictionary(); + + var fieldDeclarations = + classDescriptionNode + .Members + .OfType() + .Where(fd => fd.AttributeLists.Any() == false) + .Where(fd => fd.HasStructuredTrivia == false) + .Where(fd => fd.DescendantTrivia().Any(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia) || t.IsKind(SyntaxKind.MultiLineCommentTrivia)) == false) + .Where(fd => fd.Declaration.Variables.All(x => x.Initializer == null || x.Initializer.Value is LiteralExpressionSyntax)) + .ToList(); + + foreach (var fieldDeclarationItem in fieldDeclarations) + { + var variableType = GetSystemTypeOfTypeNode(fieldDeclarationItem.Declaration); + + var key = GetKey(fieldDeclarationItem); + + if (newDeclarationDic.ContainsKey(key) == false) + { + newDeclarationDic + .Add + ( + key, + new NewFieldDeclarationDicItem + { + VariablesWithoutInitializer = new List(), + VariablesWithInitializer = new List(), + OldFieldDeclarations = new List() + } + ); + } + + var currentItem = newDeclarationDic[key]; + + currentItem.OldFieldDeclarations.Add(fieldDeclarationItem); + + var newDeclaration = VisitFieldDeclaration(fieldDeclarationItem) as FieldDeclarationSyntax; + + currentItem.VariablesWithoutInitializer + .AddRange(newDeclaration.Declaration.Variables.Where(v => v.Initializer == null)); + currentItem.VariablesWithInitializer + .AddRange(newDeclaration.Declaration.Variables.Where(v => v.Initializer != null)); + } + + var newDeclarationDicAllItems = newDeclarationDic.ToList(); + + newDeclarationDic.Clear(); + + foreach (var newDelarationItem in newDeclarationDicAllItems) + { + var finalList = newDelarationItem.Value.VariablesWithoutInitializer.Select(x => x.WithoutTrailingTrivia().WithLeadingTrivia(_spaceTrivia)).ToList(); + finalList.AddRange(newDelarationItem.Value.VariablesWithInitializer.Select(x => x.WithoutTrailingTrivia().WithLeadingTrivia(_spaceTrivia))); + + finalList[0] = finalList[0].WithoutLeadingTrivia(); + + newDelarationItem.Value.NewFieldDeclaration = + newDelarationItem.Value.FirstOldFieldDeclarations + .WithDeclaration( + newDelarationItem.Value.FirstOldFieldDeclarations + .Declaration + .WithVariables(SyntaxFactory.SeparatedList(finalList)) + ); + + if (newDelarationItem.Value.NewFieldDeclaration.Span.Length <= Option.Options.MaxFieldDeclarationLength) + { + newDeclarationDic.Add(newDelarationItem.Key, newDelarationItem.Value); + } + else + { + foreach (var item in newDelarationItem.Value.OldFieldDeclarations) + fieldDeclarations.Remove(item); + } + } + + var replaceList = newDeclarationDic.Select(x => x.Value.FirstOldFieldDeclarations).ToList(); + + var newClassDescriptionNode = + classDescriptionNode + .ReplaceNodes + ( + fieldDeclarations, + (node1, node2) => + { + if (replaceList.Contains(node1)) + { + var dicItem = newDeclarationDic[GetKey(node1 as FieldDeclarationSyntax)]; + + return + dicItem + .NewFieldDeclaration + .WithLeadingTrivia(dicItem.FirstOldFieldDeclarations.GetLeadingTrivia()) + .WithTrailingTrivia(dicItem.FirstOldFieldDeclarations.GetTrailingTrivia()); + } + + return null; + } + ); + + if (replaceList.Any() && IsReportOnlyMode) + { + var lineSpan = classDescriptionNode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(classDescriptionNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Field initialize can be in one line", + Generator = nameof(SimplifyClassFieldDeclarations) + }); + } + + return newClassDescriptionNode; + } + + private NewFieldDeclarationDicKey GetKey(FieldDeclarationSyntax fieldDeclarationItem) + { + var header = new NewFieldDeclarationDicKey + { + TypeName = GetSystemTypeOfTypeNode(fieldDeclarationItem.Declaration), + }; + + if (fieldDeclarationItem.Modifiers.Any()) + { + header.Modifiers = fieldDeclarationItem.Modifiers.Select(x => x.ValueText).ToArray(); + } + + return header; + } + + private string GetSystemTypeOfTypeNode(VariableDeclarationSyntax d) + { + if (d.Type is PredefinedTypeSyntax) + return TypesMapItem.GetAllPredefinedTypesDic()[(d.Type as PredefinedTypeSyntax).Keyword.ValueText].BuiltInName.Trim(); + + return (d.Type.ToFullString().Trim()); + } + + private struct NewFieldDeclarationDicKey : IEquatable + { + + public string TypeName { get; set; } + public string[] Modifiers { get; set; } + + public bool Equals(NewFieldDeclarationDicKey other) + { + return this == other; + } + + public static bool operator ==(NewFieldDeclarationDicKey left, NewFieldDeclarationDicKey right) + { + if (string.Compare(left.TypeName, right.TypeName) != 0) return false; + if (left.Modifiers == null && right.Modifiers == null) return true; + if (left.Modifiers == null || right.Modifiers == null) return false; + if (left.Modifiers.Length != right.Modifiers.Length) return false; + + foreach (var item in left.Modifiers) + { + if (right.Modifiers.Any(m => string.Compare(m, item) == 0) == false) return false; + } + + return true; + } + public static bool operator !=(NewFieldDeclarationDicKey left, NewFieldDeclarationDicKey right) + { + return !(left == right); + } + } + + private class NewFieldDeclarationDicItem + { + public List VariablesWithoutInitializer { get; set; } + public List VariablesWithInitializer { get; set; } + public FieldDeclarationSyntax FirstOldFieldDeclarations => OldFieldDeclarations.FirstOrDefault(); + public List OldFieldDeclarations { get; set; } + public FieldDeclarationSyntax NewFieldDeclaration { get; set; } + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyVariableDeclarations.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyVariableDeclarations.cs new file mode 100644 index 0000000..a488860 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplifyVariableDeclarations.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class SimplifyVariableDeclarations : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var syntaxRewriter = new Rewriter(ProjectItemDetails.SemanticModel, + IsReportOnlyMode, Options); + + var modifiedSyntaxNode = syntaxRewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(syntaxRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSyntaxNode; + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + private const string VarKeyword = "var"; + private SemanticModel _semanticModel; + public Rewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) + : base(isReportOnlyMode, options) + { + _semanticModel = semanticModel; + } + + public override SyntaxNode VisitVariableDeclaration(VariableDeclarationSyntax node) + { + return ConvertToVar(node) ?? node; + } + + private SyntaxNode ConvertToVar(VariableDeclarationSyntax node) + { + if (node.Parent is LocalDeclarationStatementSyntax == false) return null; + if ((node.Parent as LocalDeclarationStatementSyntax).IsConst) return null; + + if (node.Type is IdentifierNameSyntax varIdentifierNameSyntax) + { + if (varIdentifierNameSyntax.Identifier.ValueText == VarKeyword) return null; + } + + if (node.Variables.Count > 1) return null; + + var variable = node.Variables.FirstOrDefault(); + + if (variable.Initializer == null) return null; + + var typeOfInitializer = CSharpExtensions.GetTypeInfo(_semanticModel, variable.Initializer.Value); + + var typeOfTypeDef = CSharpExtensions.GetTypeInfo(_semanticModel, node.Type); + + if (typeOfInitializer.Type?.Name == typeOfTypeDef.Type?.Name) + { + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Should Convert To Var", + Generator = nameof(SimplifyVariableDeclarations) + }); + } + + node = + node + .WithType( + SyntaxFactory.ParseTypeName(VarKeyword) + .WithTrailingTrivia(SyntaxFactory.Space) + .WithLeadingTrivia(node.Type.GetLeadingTrivia()) + ); + } + + return base.VisitVariableDeclaration(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/Option/CleanupTypes.cs new file mode 100644 index 0000000..2eae704 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/Option/CleanupTypes.cs @@ -0,0 +1,10 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(Title = "Remove unnecessary async / await pair (simply return the task).")] + SingleExpression = 0x02, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/Option/Options.cs new file mode 100644 index 0000000..533d214 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/Option/Options.cs @@ -0,0 +1,11 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() => CodeCleanerType.SimplyAsyncCalls; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/SimplyAsyncCalls.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/SimplyAsyncCalls.cs new file mode 100644 index 0000000..269ece0 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SimplyAsyncCall/SimplyAsyncCalls.cs @@ -0,0 +1,139 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall; + +public class SimplyAsyncCalls : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var rewriter = new Rewriter(IsReportOnlyMode, Options); + var modifiedSourceNode = rewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(rewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + // return SimplyAsyncCallsHelper2(initialSourceNode); + } + + private class Rewriter : CleanupCSharpSyntaxRewriter + { + public Rewriter(bool isReportOnlyMode, ICleanupOption options) + : base(isReportOnlyMode, options) + { + } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (node == null) return base.Visit(node); + if (node is MethodDeclarationSyntax == false) return base.Visit(node); + if (node.Parent is ClassDeclarationSyntax == false) return base.Visit(node); + + var newNode = SimplyAsyncCallsHelper((MethodDeclarationSyntax)node, Options); + + if (!newNode.IsEquivalentTo(node) && IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "you can remove await/async modifiers", + Generator = nameof(SimplyAsyncCalls) + }); + } + + return base.Visit(newNode); + } + + private static SyntaxTrivia[] _spaceTrivia = { SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ") }; + + //public SyntaxNode SimplyAsyncCallsHelper(SyntaxNode initialSource, ICleanupOption options) + //{ + // return + // initialSource + // .ReplaceNodes + // ( + // initialSource + // .DescendantNodes() + // .Where(node => node is MethodDeclarationSyntax && node.Parent is ClassDeclarationSyntax) + // , (node1, node2) => SimplyAsyncCallsHelper((MethodDeclarationSyntax)node1, options) + // ); + //} + + public MethodDeclarationSyntax SimplyAsyncCallsHelper(MethodDeclarationSyntax method, ICleanupOption options) + { + if ((method.Parent is ClassDeclarationSyntax) == false) return method; + if (method.Modifiers.Any(x => x.IsKind(SyntaxKind.AsyncKeyword)) == false) return method; + if (method.Body == null) return method; + if (method.ReturnType.WithoutTrivia().ToFullString() == typeof(Task).Name) return method; + if (method.ReturnType.WithoutTrivia().ToFullString() == "void") return method; + if (method.Body.Statements.Count != 1) return method; + + var singleStatement = method.Body.Statements.FirstOrDefault(); + + if (singleStatement.DescendantNodesAndSelf() + .Count(x => x.IsKind(SyntaxKind.AwaitExpression)) > 1) + return method; + + AwaitExpressionSyntax awaitStatementExpression = null; + + if (singleStatement is ReturnStatementSyntax retSs) + { + awaitStatementExpression = retSs.Expression as AwaitExpressionSyntax; + } + else if (singleStatement is ExpressionStatementSyntax eSs) + { + awaitStatementExpression = eSs.Expression as AwaitExpressionSyntax; + } + + if (awaitStatementExpression == null) return method; + + if (awaitStatementExpression.Expression is InvocationExpressionSyntax invSs) + { + if (invSs.ArgumentList.Arguments.Any(a => a.Expression.IsKind(SyntaxKind.AwaitExpression))) + return method; + } + + var newStatement = singleStatement; + + if (options.Should((int)CleanupTypes.SingleExpression)) + { + if (singleStatement is ReturnStatementSyntax rss) + { + newStatement = rss.WithExpression(awaitStatementExpression.Expression); + } + else if (singleStatement is ExpressionStatementSyntax) + { + var newReturnStatement = + SyntaxFactory + .ReturnStatement(awaitStatementExpression.Expression) + .WithLeadingTrivia(singleStatement.GetLeadingTrivia()) + .WithTrailingTrivia(singleStatement.GetTrailingTrivia()); + + newStatement = + newReturnStatement.WithReturnKeyword( + newReturnStatement.ReturnKeyword.WithTrailingTrivia(_spaceTrivia)); + } + } + + return + method + .ReplaceNode(singleStatement, newStatement) + .WithModifiers( + method.Modifiers.Remove(method.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.AsyncKeyword)))) + .WithLeadingTrivia(method.GetLeadingTrivia()) + .WithTrailingTrivia(method.GetTrailingTrivia()); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SortClassMembers.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SortClassMembers.cs new file mode 100644 index 0000000..136d60c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/SortClassMembers.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class SortClassMembers : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var modifiedSourceNode = SortClassMembersHelper(initialSourceNode); + + if (IsReportOnlyMode && + !IsEquivalentToUnModified(modifiedSourceNode)) + { + CollectMessages(new ChangesReport(initialSourceNode) + { + LineNumber = 1, + Column = 1, + Message = "Sort Class Members", + Generator = nameof(SortClassMembers) + }); + + return initialSourceNode; + } + + return modifiedSourceNode; + } + + public static SyntaxNode SortClassMembersHelper(SyntaxNode initialSource) + { + var classes = + initialSource + .DescendantNodes() + .Where(x => x is ClassDeclarationSyntax) + .OfType(); + + var newClassesDic = new Dictionary(); + + foreach (var classNode in classes) + { + var newClassNode = SortClassMemebersHelper(classNode); + newClassesDic.Add(classNode, newClassNode); + } + + initialSource = + initialSource + .ReplaceNodes + ( + classes, + (oldNode1, oldNode2) => + { + var newClass = newClassesDic[oldNode1]; + if (oldNode1 != newClass) return newClass; + return oldNode1; + } + ); + + return initialSource; + } + + public static ClassDeclarationSyntax SortClassMemebersHelper(ClassDeclarationSyntax classNode) + { + var methods = classNode.Members.Where(x => x is MethodDeclarationSyntax).ToList(); + var firstMethod = methods.FirstOrDefault(); + if (firstMethod == null) return classNode; + + var methodAnnotation = new SyntaxAnnotation(); + var annotatedClassNode = classNode.ReplaceNode(firstMethod, firstMethod.WithAdditionalAnnotations(methodAnnotation)); + + var constructors = annotatedClassNode.Members.Where(x => x is ConstructorDeclarationSyntax).ToList(); + if (constructors.Any() == false) return classNode; + + var constructorsToMoveList = new List(); + + foreach (var constructorItem in constructors) + { + if (firstMethod.SpanStart < constructorItem.SpanStart) + { + constructorsToMoveList.Add(constructorItem); + } + } + + if (constructorsToMoveList.Any()) + { + annotatedClassNode = annotatedClassNode.RemoveNodes(constructorsToMoveList, SyntaxRemoveOptions.KeepNoTrivia); + var annotatedMethod = annotatedClassNode.GetAnnotatedNodes(methodAnnotation).FirstOrDefault(); + annotatedClassNode = annotatedClassNode.InsertNodesBefore(annotatedMethod, constructorsToMoveList); + + return annotatedClassNode; + } + + return classNode; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/UsingDirectiveOrganizer.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/UsingDirectiveOrganizer.cs new file mode 100644 index 0000000..2873d22 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/UsingDirectiveOrganizer.cs @@ -0,0 +1,88 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class UsingDirectiveOrganizer : CodeCleanerCommandRunnerBase +{ + private async static Task RemoveUnusedImportsAsync(Document document) + { + var compilation = await document.Project.GetCompilationAsync(); + var tree = await document.GetSyntaxTreeAsync(); + + if(compilation == null || tree == null) + { + return document; + } + + var root = tree.GetRoot(); + var unusedImportNodes = compilation.GetDiagnostics() + .Where(d => d.Id == "CS8019") + .Where(d => d.Location?.SourceTree == tree) + .Select(d => root.FindNode(d.Location.SourceSpan)) + .ToList(); + + if (!unusedImportNodes.Any()) + { + return document; + } + + return document.WithSyntaxRoot( + root.RemoveNodes(unusedImportNodes, SyntaxRemoveOptions.KeepNoTrivia)); + } + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + //TODO + var doc = await RemoveUnusedImportsAsync(ProjectItemDetails.Document); + return await doc.GetSyntaxRootAsync(); + + //var item = ProjectItemDetails.Document; + + //try + //{ + // item.Open(Constants.vsViewKindCode); + + // var document = Document; + // document.Activate(); + + // try { document.DTE.ExecuteCommand(UsingsCommands.RemoveAndSortCommandName); } + // catch (Exception ex) + // { + // if (ex.Message != "Command \"Edit.RemoveAndSort\" is not available.") throw; + + // document.Activate(); + // document.DTE.ExecuteCommand(UsingsCommands.RemoveAndSortCommandName); + // } + + // var doc = (EnvDTE.TextDocument)(document.Object("TextDocument")); + // var p = doc.StartPoint.CreateEditPoint(); + // var s = p.GetText(doc.EndPoint); + // var modified = SyntaxFactory.ParseSyntaxTree(s); + + // if (IsReportOnlyMode && + // !IsEquivalentToUnModified(await modified.GetRootAsync())) + // { + // CollectMessages(new ChangesReport(initialSourceNode) + // { + // LineNumber = 1, + // Column = 1, + // Message = "Your Using usage is not good", + // Generator = nameof(UsingDirectiveOrganizer) + // }); + + // document.Undo(); + // return initialSourceNode; + // } + + // document.Save(); + //} + //catch (Exception e) + //{ + // ErrorNotification.ErrorNotification.WriteErrorToFile(e, initialSourceNode.GetFilePath()); + // ErrorNotification.ErrorNotification.WriteErrorToOutputWindow(e, initialSourceNode.GetFilePath()); + // ProcessActions.GeeksProductivityToolsProcess(); + //} + + //return item.ToSyntaxNode(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/BlankLineRewriter.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/BlankLineRewriter.cs new file mode 100644 index 0000000..3ab7db8 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/BlankLineRewriter.cs @@ -0,0 +1,58 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; + +public class BlankLineRewriter : CSharpSyntaxRewriterBase +{ + private SemanticModel _semanticModel; + public BlankLineRewriter(SyntaxNode node, bool isReadOnlyMode, SemanticModel semanticModel) : + base(node, isReadOnlyMode, null) => _semanticModel = semanticModel; + public override SyntaxNode VisitBlock(BlockSyntax node) + { + var writeTrailing = false; + + node = node.ReplaceNodes(node.Statements, (nde1, nde2) => + { + if (nde1.WithoutLeadingTrivia().WithoutTrailingTrivia() + .DescendantTrivia().Any(x => x.IsKind(SyntaxKind.EndOfLineTrivia))) + { + var leading = nde1.GetLeadingTrivia(); + var trailing = nde1.GetTrailingTrivia(); + + var leadingTrivias = new SyntaxTriviaList(SyntaxFactory.EndOfLine("\n")); + + if (nde1.Equals(node.Statements.FirstOrDefault()) || writeTrailing || + nde1.GetLeadingTrivia().Count(x => x.IsKind(SyntaxKind.EndOfLineTrivia)) > 0) + { + leadingTrivias = new SyntaxTriviaList(); + writeTrailing = false; + } + + var trailingTrivias = new SyntaxTriviaList(); + + leadingTrivias = leadingTrivias.AddRange(leading); + + trailingTrivias = trailingTrivias.AddRange(trailing); + + if (!nde1.Equals(node.Statements.LastOrDefault()) && + (node.Statements.IndexOf(nde1) + 1 < node.Statements.Count() && + node.Statements[node.Statements.IndexOf(nde1) + 1] + .GetLeadingTrivia().Count(x => x.IsKind(SyntaxKind.EndOfLineTrivia)) < 1)) + { + writeTrailing = true; + trailingTrivias = trailingTrivias.Add(SyntaxFactory.EndOfLine("\n")); + } + + return nde2.WithTrailingTrivia(trailingTrivias) + .WithLeadingTrivia(leadingTrivias); + } + + writeTrailing = false; + return nde2; + }); + + return base.VisitBlock(node); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/BlockRewriter.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/BlockRewriter.cs new file mode 100644 index 0000000..bc8c8d9 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/BlockRewriter.cs @@ -0,0 +1,161 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; + +internal class BlockRewriter : CSharpSyntaxRewriterBase +{ + public BlockRewriter(SyntaxNode initialSource, bool isReadOnlyMode, Options options) + : base(initialSource, isReadOnlyMode, options) { } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (CheckOption((int)CleanupTypes.RemoveBracketsOfBlockThatHasOnlyOneStatementWithLengthShorterThan80Chars)) + { + if (node is BlockSyntax) + { + var newNode = ApplyNodeChange(node as BlockSyntax); + return base.Visit(newNode); + } + else if (node is SimpleLambdaExpressionSyntax) + { + var newNode = ApplyNodeChange(node as SimpleLambdaExpressionSyntax); + return base.Visit(newNode); + } + } + + return base.Visit(node); + } + + private SyntaxToken _lastBlockToken = default(SyntaxToken); + + private SyntaxNode ApplyNodeChange(SimpleLambdaExpressionSyntax lambdaNode) + { + if (lambdaNode.Block == null) return lambdaNode; + var blockSyntax = lambdaNode.Block as BlockSyntax; + + if (blockSyntax.Statements.Count == 1 && + blockSyntax.Statements.FirstOrDefault().ToString().Length <= 80) + { + var newNode = blockSyntax.Statements.FirstOrDefault() as StatementSyntax; + + if (IsReportOnlyMode) + { + var lineSpan = lambdaNode.GetFileLinePosSpan(); + + AddReport(new ChangesReport(lambdaNode) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "remove brackets from on line lambda expression", + Generator = nameof(BlockRewriter) + }); + } + + if (newNode.IsKind(SyntaxKind.ReturnStatement)) + { + var statementSyntax = newNode as ReturnStatementSyntax; + + if (statementSyntax != null) + { + return lambdaNode.WithBlock(null).WithExpressionBody(statementSyntax.Expression); + } + } + else + { + var expressionStatement = newNode as ExpressionStatementSyntax; + + if (expressionStatement != null) + { + return lambdaNode.WithBlock(null).WithExpressionBody(expressionStatement.Expression.WithoutTrivia() + .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia(" "))) + .WithArrowToken(SyntaxTokenExtensions.WithoutTrivia(lambdaNode.ArrowToken)); + } + } + + // return lambdaNode.WithBlock(null) + // .WithExpressionBody((newNode.IsKind(SyntaxKind.ReturnStatement) + // ? (newNode as ReturnStatementSyntax).Expression : + // (newNode as ExpressionStatementSyntax).Expression) + // .WithoutTrivia() + // .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia(" "))) + // .WithArrowToken(lambdaNode.ArrowToken.WithoutTrivia()); + } + + return lambdaNode; + } + + private SyntaxNode ApplyNodeChange(BlockSyntax blockNode) + { + if (blockNode.Parent is ConversionOperatorDeclarationSyntax) return blockNode; + if (blockNode.Parent is OperatorDeclarationSyntax) return blockNode; + if (blockNode.Parent is AccessorDeclarationSyntax) return blockNode; + if (blockNode.Parent is ConstructorDeclarationSyntax) return blockNode; + if (blockNode.Parent is DestructorDeclarationSyntax) return blockNode; + if (blockNode.Parent is TryStatementSyntax) return blockNode; + if (blockNode.Parent is CatchClauseSyntax) return blockNode; + if (blockNode.Parent is FinallyClauseSyntax) return blockNode; + if (blockNode.Parent is IfStatementSyntax) return blockNode; + if (blockNode.Parent is ElseClauseSyntax) return blockNode; + // if (blockNode.Parent is ParenthesizedExpressionSyntax) return blockNode; + if (blockNode.Parent is SimpleLambdaExpressionSyntax) return blockNode; + if (blockNode.Parent is ParenthesizedLambdaExpressionSyntax) return blockNode; + if (blockNode.Parent is AnonymousMethodExpressionSyntax) return blockNode; + if (blockNode.Parent is LocalFunctionStatementSyntax) return blockNode; + + if (blockNode.Parent is MethodDeclarationSyntax == false && blockNode.Statements.Count == 1) + { + var singleStatement = blockNode.Statements.FirstOrDefault(); + + if (singleStatement.Span.Length <= Option.Options.BlockSingleStatementMaxLength) + { + if (IsReportOnlyMode) + { + var lineSpan = singleStatement.GetFileLinePosSpan(); + + AddReport(new ChangesReport(singleStatement) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "remove brackets from on line lambda expression", + Generator = nameof(BlockRewriter) + }); + } + + singleStatement = + singleStatement + .WithTrailingTrivia( + CleanUpListWithExactNumberOfWhiteSpaces( + SyntaxFactory.TriviaList( + blockNode.CloseBraceToken.LeadingTrivia + .AddRange(singleStatement.GetTrailingTrivia()) + .AddRange(blockNode.GetTrailingTrivia()) + ), 1, null) + ); + + _lastBlockToken = singleStatement.GetLastToken(); + + return singleStatement; + } + } + + return blockNode; + } + + private SyntaxToken _lastToken = default(SyntaxToken); + public override SyntaxToken VisitToken(SyntaxToken token) + { + if (_lastBlockToken != default(SyntaxToken) && _lastBlockToken == _lastToken) + { + token = token.WithLeadingTrivia(CleanUpListWithExactNumberOfWhiteSpaces(token.LeadingTrivia, 1, null)); + _lastBlockToken = default(SyntaxToken); + } + + _lastToken = token; + return base.VisitToken(token); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/CSharpSyntaxRewriterBase.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/CSharpSyntaxRewriterBase.cs new file mode 100644 index 0000000..35f5ab4 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/CSharpSyntaxRewriterBase.cs @@ -0,0 +1,247 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; + +public class CSharpSyntaxRewriterBase : CleanupCSharpSyntaxRewriter +{ + protected SyntaxNode InitialSource { get; set; } + private static SyntaxTrivia _endOfLineTrivia = default(SyntaxTrivia); + + public CSharpSyntaxRewriterBase(SyntaxNode initialSource, + bool isReadOnlyMode, Options options) : + base(isReadOnlyMode, options) + { + InitialSource = initialSource; + + _endOfLineTrivia = + initialSource + .SyntaxTree + .GetRoot() + .DescendantTrivia(descendIntoTrivia: true) + .FirstOrDefault(x => x.IsKind(SyntaxKind.EndOfLineTrivia)); + } + + #region + + private SyntaxTriviaList CleanUpList(SyntaxTriviaList newList, CleanupTypes? option = null) + { + if (option.HasValue && CheckOption((int)option.Value) == false) return newList; + + var lineBreaksAtBeginning = newList.TakeWhile(t => t.IsKind(SyntaxKind.EndOfLineTrivia)).Count(); + + if (lineBreaksAtBeginning > 1) + { + newList = newList.Skip(lineBreaksAtBeginning - 1).ToSyntaxTriviaList(); + } + + return newList; + } + + private SyntaxTriviaList CleanUpList(SyntaxTriviaList syntaxTrivias, int exactNumberOfBlanks) + { + var lineBreaksAtBeginning = syntaxTrivias.TakeWhile(t => t.IsKind(SyntaxKind.EndOfLineTrivia)).Count(); + + if (lineBreaksAtBeginning > exactNumberOfBlanks) + { + syntaxTrivias = syntaxTrivias.Skip(lineBreaksAtBeginning - exactNumberOfBlanks) + .ToSyntaxTriviaList(); + } + else if (lineBreaksAtBeginning < exactNumberOfBlanks) + { + var newList = syntaxTrivias.ToList(); + + for (var i = lineBreaksAtBeginning; i < exactNumberOfBlanks; i++) + newList.Insert(0, _endOfLineTrivia); + + syntaxTrivias = new SyntaxTriviaList().AddRange(newList); + } + + return syntaxTrivias; + } + + protected SyntaxTriviaList CleanUpListWithNoWhiteSpaces(SyntaxTriviaList syntaxTrivias, CleanupTypes? options, bool itsForCloseBrace = false) + { + syntaxTrivias = ProcessSpecialTrivias(syntaxTrivias, itsForCloseBrace); + + if (CheckOption((int?)options)) + { + var specialTriviasCount = + syntaxTrivias + .Count(t => + !t.IsKind(SyntaxKind.EndOfLineTrivia) && !t.IsKind(SyntaxKind.WhitespaceTrivia) + ); + + if (specialTriviasCount > 0) + return CleanUpList(syntaxTrivias); + + return CleanUpList(syntaxTrivias, 0); + } + + return syntaxTrivias; + } + + protected SyntaxTriviaList CleanUpListWithDefaultWhiteSpaces(SyntaxTriviaList syntaxTrivias, CleanupTypes? options, bool itsForCloseBrace = false) + { + if (CheckOption((int?)options)) + syntaxTrivias = CleanUpList(syntaxTrivias); + + syntaxTrivias = ProcessSpecialTrivias(syntaxTrivias, itsForCloseBrace); + + return syntaxTrivias; + } + + protected SyntaxTriviaList CleanUpListWithExactNumberOfWhiteSpaces(SyntaxTriviaList syntaxTrivias, int exactNumberOfBlanks, CleanupTypes? options, bool itsForCloseBrace = false) + { + if (CheckOption((int?)options)) + syntaxTrivias = CleanUpList(syntaxTrivias, exactNumberOfBlanks); + + syntaxTrivias = ProcessSpecialTrivias(syntaxTrivias, itsForCloseBrace); + + return syntaxTrivias; + } + + protected SyntaxTriviaList ProcessSpecialTrivias(SyntaxTriviaList syntaxTrivias, bool itsForCloseBrace) + { + if (CheckShortSyntax(syntaxTrivias, itsForCloseBrace)) return syntaxTrivias; + var specialTriviasCount = syntaxTrivias.Count(t => !t.IsKind(SyntaxKind.EndOfLineTrivia) && !t.IsKind(SyntaxKind.WhitespaceTrivia)); + + var outputTriviasList = new List(); + var specialTiviasCount = 0; + var bAddedBlankLine = false; + + for (var i = 0; i < syntaxTrivias.Count; i++) + { + var countOfChars = 0; + + if (specialTiviasCount == specialTriviasCount) + { + if (itsForCloseBrace) + { + if (CheckOption((int)CleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets)) + { + i += RemoveBlankDuplication(syntaxTrivias, SyntaxKind.EndOfLineTrivia, i) + 1; + + if (RemoveBlankDuplication(syntaxTrivias, SyntaxKind.WhitespaceTrivia, i) != -1) + { + outputTriviasList.Add(syntaxTrivias[i]); + } + + i = syntaxTrivias.Count; + } + else + { + outputTriviasList.Add(syntaxTrivias[i]); + } + + continue; + } + } + + if + ( + ( + syntaxTrivias[i].IsKind(SyntaxKind.EndOfLineTrivia) || + syntaxTrivias[i].IsKind(SyntaxKind.WhitespaceTrivia) || + syntaxTrivias[i].IsKind(SyntaxKind.SingleLineCommentTrivia) || + syntaxTrivias[i].IsKind(SyntaxKind.MultiLineCommentTrivia) + ) == false + ) + { + outputTriviasList.Add(syntaxTrivias[i]); + specialTiviasCount++; + continue; + } + + if (syntaxTrivias[i].IsKind(SyntaxKind.SingleLineCommentTrivia) || syntaxTrivias[i].IsKind(SyntaxKind.MultiLineCommentTrivia)) + { + syntaxTrivias = Insert_Space_Before_Comment_Text(syntaxTrivias, syntaxTrivias[i]); + + outputTriviasList.Add(syntaxTrivias[i]); + i++; + + if (i < syntaxTrivias.Count && syntaxTrivias[i].IsKind(SyntaxKind.EndOfLineTrivia)) + { + outputTriviasList.Add(syntaxTrivias[i]); + } + + specialTiviasCount++; + continue; + } + + if (CheckOption((int)CleanupTypes.RemoveDuplicateInsideComments) == false) + { + outputTriviasList.Add(syntaxTrivias[i]); + + continue; + } + + if ((countOfChars = RemoveBlankDuplication(syntaxTrivias, SyntaxKind.EndOfLineTrivia, i)) != -1) + { + outputTriviasList.Add(syntaxTrivias[i]); + i += countOfChars + 1; + bAddedBlankLine = true; + } + + if ((countOfChars = RemoveBlankDuplication(syntaxTrivias, SyntaxKind.WhitespaceTrivia, i)) != -1) + { + outputTriviasList.Add(syntaxTrivias[i]); + i += countOfChars; + } + else if (bAddedBlankLine) i--; + + bAddedBlankLine = false; + } + + return outputTriviasList.ToSyntaxTriviaList(); + } + + private SyntaxTriviaList Insert_Space_Before_Comment_Text(SyntaxTriviaList syntaxTrivias, SyntaxTrivia currentTrivia) + { + if (CheckOption((int)CleanupTypes.InsertSpaceBeforeCommentText)) + { + if (currentTrivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) + { + var commentText = currentTrivia.ToFullString().Trim(); + + if (commentText.Length > 2 && commentText[2] != ' ') + { + commentText = $"{commentText.Substring(0, 2)} {commentText.Substring(2)}"; + syntaxTrivias = syntaxTrivias.Replace(currentTrivia, SyntaxFactory.Comment(commentText)); + } + } + } + + return syntaxTrivias; + } + + private bool CheckShortSyntax(SyntaxTriviaList syntaxTrivias, bool itsForCloseBrace) + { + if (itsForCloseBrace) return false; + if (syntaxTrivias.Count <= 1) return true; + if (syntaxTrivias.Count > 2) return false; + + if (syntaxTrivias[0].IsKind(SyntaxKind.EndOfLineTrivia) && + syntaxTrivias[1].IsKind(SyntaxKind.WhitespaceTrivia)) + return true; + + if (syntaxTrivias[0].IsKind(SyntaxKind.WhitespaceTrivia) && + syntaxTrivias[1].IsKind(SyntaxKind.EndOfLineTrivia)) + return true; + + return false; + } + + private int RemoveBlankDuplication(SyntaxTriviaList syntaxTrivias, SyntaxKind kind, int iterationIndex) + { + if (iterationIndex >= syntaxTrivias.Count) return -1; + + var lineBreaksAtBeginning = syntaxTrivias.Skip(iterationIndex).TakeWhile(t => t.IsKind(kind)).Count(); + + return lineBreaksAtBeginning - 1; + } + + #endregion +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/EndOFLineRewriter.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/EndOFLineRewriter.cs new file mode 100644 index 0000000..09bf870 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/EndOFLineRewriter.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; + +public class EndOfLineRewriter : CSharpSyntaxRewriterBase +{ + public EndOfLineRewriter(SyntaxNode initialSource, + bool isReadOnlyMode, Options options) + : base(initialSource, isReadOnlyMode, options) + { } + + public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia) + { + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia) && trivia.ToFullString() == "\r\n") + { + if (IsReportOnlyMode) + { + var lineSpan = trivia.GetFileLinePosSpan(); + + AddReport(new ChangesReport(trivia) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "\\r\\n should be \\n", + Generator = nameof(EndOfLineRewriter) + }); + } + + return base.VisitTrivia(SyntaxFactory.EndOfLine("\n")); + } + + return base.VisitTrivia(trivia); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/Option/CleanupTypes.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/Option/CleanupTypes.cs new file mode 100644 index 0000000..7a8b539 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/Option/CleanupTypes.cs @@ -0,0 +1,52 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; + +[Flags] +public enum CleanupTypes +{ + [CleanupItem(FirstOrder = 1, Title = "Trim the file")] + TrimTheFile = 0x80, + + [CleanupItem(FirstOrder = 1, Title = "No duplicate blank lines between namespace members")] + RemoveDuplicateBetweenNamespaceMembers = 0x04, + + [CleanupItem(FirstOrder = 1, Title = "No duplicate blank lines between class members")] + RemoveDuplicateBetweenClassMembers = 0x02, + + [CleanupItem(FirstOrder = 1, Title = "No duplicate blank lines between method statement")] + RemoveDuplicateBetweenMethodsStatements = 0x01, + + [CleanupItem(FirstOrder = 1, Title = "No duplicate blank lines between comments")] + RemoveDuplicateInsideComments = 0x08, + + [CleanupItem(FirstOrder = 1, Title = "No duplicate blank lines between \"Usings ..\"")] + RemoveDuplicateInsideUsings = 0x10, + + [CleanupItem(FirstOrder = 1, Title = "No blank lines immediately after { and immediately before }")] + RemoveBlankAfterOpenBracketAndBeforeCloseBrackets = 0x20, + + /// + /// Insert_one_space_between_the_comment_delimiter_and_the_comment_text, + /// + [CleanupItem(FirstOrder = 1, Title = "Space before comment text: //TODO: => // TODO:")] + InsertSpaceBeforeCommentText = 0x40, + + [CleanupItem(FirstOrder = 1, Title = "Blank line between methods.")] + AddingBlankAfterMethodCloseBracket = 0x100, + + [CleanupItem(FirstOrder = 1, Title = "Blank line between } and the next statement.")] + AddingBlankAfterBlockCloseBracket = 0x200, + + [CleanupItem(FirstOrder = 1, Title = "No { and } for short blocks (single statement and < 80 chars)")] + RemoveBracketsOfBlockThatHasOnlyOneStatementWithLengthShorterThan80Chars = 0x400, + + [CleanupItem(FirstOrder = 1, Title = "Add Blank line between Statements more that one line")] + AddBlankLineBetweenStatementsMoreThanOneLine = 0x500, + + [CleanupItem(FirstOrder = 1, Title = "Use \\n instead of \\r\\n")] + UseSlashInsteadOfSlashSlash = 0x600, + + [CleanupItem(FirstOrder = 1, Title = "Add an empty line after using statements")] + AddAnEmptyLineAfterUsingStatements = 0x700, +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/Option/Options.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/Option/Options.cs new file mode 100644 index 0000000..c433ed8 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/Option/Options.cs @@ -0,0 +1,13 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; + +public class Options : OptionsBase, ICleanupOption +{ + public const int BlockSingleStatementMaxLength = 80; + + public CleanupTypes? CleanupItems => (CleanupTypes?)CleanupItemsInteger; + + public override CodeCleanerType GetCodeCleanerType() => CodeCleanerType.NormalizeWhiteSpaces; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/WhiteSpaceNormalizer.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/WhiteSpaceNormalizer.cs new file mode 100644 index 0000000..bb1a1f6 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/WhiteSpaceNormalizer.cs @@ -0,0 +1,71 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Formatting; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; + +public class WhiteSpaceNormalizer : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + return await NormalizeWhiteSpaceHelperAsync(initialSourceNode, Options); + } + + public Options Options { get; set; } + + public async Task NormalizeWhiteSpaceHelperAsync(SyntaxNode initialSourceNode, Options options) + { + var modifiedSourceNode = initialSourceNode; + + if (TidyCSharpPackage.Instance != null) + { + modifiedSourceNode = Formatter.Format(modifiedSourceNode, TidyCSharpPackage.Instance.Solution.Workspace); + } + + var blockRewriter = new BlockRewriter(modifiedSourceNode, IsReportOnlyMode, options); + modifiedSourceNode = blockRewriter.Visit(modifiedSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(blockRewriter.GetReport()); + } + + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + if (CheckOption((int)CleanupTypes.UseSlashInsteadOfSlashSlash)) + { + var endoflineRewriter = new EndOfLineRewriter(modifiedSourceNode, IsReportOnlyMode, options); + modifiedSourceNode = endoflineRewriter.Visit(modifiedSourceNode); + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(endoflineRewriter.GetReport()); + } + } + + var whitespaceRewriter = new WhiteSpaceRewriter(modifiedSourceNode, IsReportOnlyMode, options); + modifiedSourceNode = whitespaceRewriter.Apply(); + + if (IsReportOnlyMode) + { + CollectMessages(whitespaceRewriter.GetReport()); + } + + if (CheckOption((int)CleanupTypes.AddBlankLineBetweenStatementsMoreThanOneLine)) + { + modifiedSourceNode = await RefreshResultAsync(modifiedSourceNode); + var blRewriter = new BlankLineRewriter(modifiedSourceNode, IsReportOnlyMode, ProjectItemDetails.SemanticModel); + modifiedSourceNode = blRewriter.Visit(modifiedSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(blRewriter.GetReport()); + } + } + + if (IsReportOnlyMode) return initialSourceNode; + return modifiedSourceNode; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/WhitespaceRewriter.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/WhitespaceRewriter.cs new file mode 100644 index 0000000..4ea4ee9 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/Whitespace/WhitespaceRewriter.cs @@ -0,0 +1,402 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; + +public class WhiteSpaceRewriter : CSharpSyntaxRewriterBase +{ + private SyntaxToken _lastToken = default(SyntaxToken); + private MemberDeclarationSyntax _lastMember; + private bool _lastTokenIsAOpenBrace, _lastTokenIsACloseBrace; + public WhiteSpaceRewriter(SyntaxNode initialSource, bool isReadOnlyMode, Options options) : + base(initialSource, isReadOnlyMode, options) + { } + + public SyntaxNode Apply() + { + TrimFile(); + AddNewLineAfterUsings(); + + return Visit(InitialSource); + } + + private void AddNewLineAfterUsings() + { + if (CheckOption((int)CleanupTypes.AddAnEmptyLineAfterUsingStatements)) + { + var list = InitialSource.DescendantNodesOfType() + .Select(x => new + { + StartLine = x.GetFileLinePosSpan().StartLinePosition.Line, + EndLine = x.GetFileLinePosSpan().EndLinePosition.Line, + EndPosition = x.FullSpan.End, + Directive = x + }); + + var join = list.Join(list, a => a.EndLine + 1, b => b.StartLine, (a, b) => a); + + var selectedDirectives = list.Except(join) + .Where(x => !InitialSource.FindTrivia(x.EndPosition, false) + .IsKind(SyntaxKind.EndOfLineTrivia)) + .Select(x => x.Directive); + + InitialSource = InitialSource.ReplaceNodes(selectedDirectives, (a, b) => + { + return b.WithTrailingTrivia( + b.GetTrailingTrivia().AddRange( + SyntaxFactory.ParseTrailingTrivia("\n"))); + }); + } + } + + private void TrimFile() + { + var newLeadingTriviaList = CleanUpListWithNoWhiteSpaces(InitialSource.GetLeadingTrivia(), CleanupTypes.TrimTheFile); + InitialSource = InitialSource.WithLeadingTrivia(newLeadingTriviaList); + + var endOfFileToken = InitialSource.DescendantTokens().SingleOrDefault(x => x.IsKind(SyntaxKind.EndOfFileToken)); + var beforeEndOfFileToken = endOfFileToken.GetPreviousToken(); + + var leadingTriviList = endOfFileToken.LeadingTrivia; + leadingTriviList = CleanUpListWithNoWhiteSpaces(leadingTriviList, CleanupTypes.TrimTheFile, itsForCloseBrace: true); + + if (CheckOption((int)CleanupTypes.TrimTheFile)) + { + if (leadingTriviList.Any() && leadingTriviList.Last().IsKind(SyntaxKind.EndOfLineTrivia)) + { + leadingTriviList = leadingTriviList.Take(leadingTriviList.Count - 1).ToSyntaxTriviaList(); + } + } + + var newEndOfFileToken = endOfFileToken.WithLeadingTrivia(leadingTriviList); + + newLeadingTriviaList = CleanUpListWithNoWhiteSpaces(beforeEndOfFileToken.TrailingTrivia, CleanupTypes.TrimTheFile); + + var newBeforeEndOfFileToken = beforeEndOfFileToken.WithTrailingTrivia(newLeadingTriviaList) + .WithTrailingTrivia((leadingTriviList != null && leadingTriviList.Any()) ? SyntaxFactory.CarriageReturn : SyntaxFactory.ElasticMarker); + + InitialSource = InitialSource.ReplaceTokens(new[] { endOfFileToken, beforeEndOfFileToken }, + (token1, token2) => + { + if (token1 == endOfFileToken) return newEndOfFileToken; + return newBeforeEndOfFileToken; + } + ); + } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (node == null) return base.Visit(node); + + var nodeLeadingTriviList = node.GetLeadingTrivia(); + SyntaxNode newNode = null; + + if (node is UsingDirectiveSyntax) + { + nodeLeadingTriviList = CleanUpListWithNoWhiteSpaces(nodeLeadingTriviList, CleanupTypes.RemoveDuplicateInsideUsings); + newNode = node.WithLeadingTrivia(nodeLeadingTriviList); + + _lastMember = null; + } + else if (node is NamespaceDeclarationSyntax) + { + newNode = ApplyNodeChange(node as NamespaceDeclarationSyntax); + } + else if (node is ClassDeclarationSyntax) + { + newNode = ApplyNodeChange(node as ClassDeclarationSyntax); + } + else if (node is StructDeclarationSyntax) + { + newNode = ApplyNodeChange(node as StructDeclarationSyntax); + } + else if (node is MemberDeclarationSyntax) + { + if (node is MethodDeclarationSyntax == false || node.Parent is ClassDeclarationSyntax) + newNode = ApplyNodeChange(node as MemberDeclarationSyntax); + } + else if (node is BlockSyntax) + { + newNode = ApplyNodeChange(node as BlockSyntax); + } + else if (node is StatementSyntax) + { + newNode = ApplyNodeChange(node as StatementSyntax); + } + else if (CheckInnerBlocks(node)) + { + nodeLeadingTriviList = CleanUpListWithDefaultWhiteSpaces(nodeLeadingTriviList, CleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets); + newNode = node.WithLeadingTrivia(nodeLeadingTriviList); + } + + if (newNode != null) + { + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Whitespaces should be normalized", + Generator = nameof(WhiteSpaceRewriter) + }); + } + + return base.Visit(newNode); + } + + return base.Visit(node); + } + + private BlockSyntax ApplyNodeChange(BlockSyntax mainNode) + { + var leadingTriviaList = ApplyOpenBracket(mainNode.OpenBraceToken).LeadingTrivia; + + var firstToken = mainNode.OpenBraceToken.GetNextToken(); + + var newCloseBraceToken = + firstToken == mainNode.CloseBraceToken ? + ApplyCloseBracket_OfEmptyBlock(mainNode.CloseBraceToken) : + ApplyCloseBracket(mainNode.CloseBraceToken); + + mainNode = + mainNode + .WithCloseBraceToken(newCloseBraceToken) + .WithLeadingTrivia(leadingTriviaList); + + firstToken = mainNode.OpenBraceToken.GetNextToken(); + + mainNode = mainNode.ReplaceToken(firstToken, ApplyOpenBracket(firstToken)); + + return mainNode; + } + + private MemberDeclarationSyntax ApplyNodeChange(MemberDeclarationSyntax statementNode) + { + var leadingTriviaList = statementNode.GetLeadingTrivia(); + var isCleanupDone = false; + + if (_lastMember is MethodDeclarationSyntax && IsStartWithSpecialDirective(leadingTriviaList) == false) + { + if (CheckOption((int)CleanupTypes.AddingBlankAfterMethodCloseBracket)) + { + if (leadingTriviaList.Count(x => x.IsKind(SyntaxKind.EndOfLineTrivia)) < 2) + { + leadingTriviaList = CleanUpListWithExactNumberOfWhiteSpaces(leadingTriviaList, 1, null); + isCleanupDone = true; + } + } + else if (CheckOption((int)CleanupTypes.RemoveDuplicateBetweenClassMembers)) + { + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, null); + isCleanupDone = true; + } + } + else if (CheckOption((int)CleanupTypes.RemoveDuplicateBetweenClassMembers)) + { + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, null); + isCleanupDone = true; + } + + if (!isCleanupDone) + { + leadingTriviaList = ProcessSpecialTrivias(leadingTriviaList, false); + } + + statementNode = statementNode.WithLeadingTrivia(leadingTriviaList); + + _lastMember = statementNode; + + return statementNode; + } + + private bool IsStartWithSpecialDirective(SyntaxTriviaList leadingTriviaList) + { + var firstDirective = leadingTriviaList.SkipWhile(x => x.IsWhiteSpaceTrivia()).FirstOrDefault(); + + if (firstDirective == default(SyntaxTrivia)) return false; + + if (firstDirective.IsDirective) + { + return + firstDirective.IsKind(SyntaxKind.ElseDirectiveTrivia) || + firstDirective.IsKind(SyntaxKind.EndIfDirectiveTrivia); + } + + return false; + } + + private StatementSyntax ApplyNodeChange(StatementSyntax statementNode) + { + var isCleanupDone = false; + var leadingTriviaList = statementNode.GetLeadingTrivia(); + + if (statementNode.IsKind(SyntaxKind.EmptyStatement)) // remove unused semicolons + return null; + + if (_lastTokenIsACloseBrace) + { + if (CheckOption((int)CleanupTypes.AddingBlankAfterBlockCloseBracket)) + { + if (leadingTriviaList.Count(x => x.IsKind(SyntaxKind.EndOfLineTrivia)) < 2) + { + leadingTriviaList = CleanUpListWithExactNumberOfWhiteSpaces(leadingTriviaList, 1, null); + isCleanupDone = true; + } + } + else if (CheckOption((int)CleanupTypes.RemoveDuplicateBetweenMethodsStatements)) + { + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, null); + isCleanupDone = true; + } + } + else if (CheckOption((int)CleanupTypes.RemoveDuplicateBetweenMethodsStatements)) + { + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, null); + isCleanupDone = true; + } + + if (!isCleanupDone) + { + leadingTriviaList = ProcessSpecialTrivias(leadingTriviaList, false); + } + + statementNode = statementNode.WithLeadingTrivia(leadingTriviaList); + + // _LastMember = null; + + return statementNode; + } + + private StructDeclarationSyntax ApplyNodeChange(StructDeclarationSyntax mainNode) + { + var leadingTriviaList = mainNode.GetLeadingTrivia(); + + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, CleanupTypes.RemoveDuplicateBetweenNamespaceMembers); + + mainNode = + mainNode + .WithOpenBraceToken(ApplyOpenBracket(mainNode.OpenBraceToken)) + .WithCloseBraceToken(ApplyCloseBracket(mainNode.CloseBraceToken)) + .WithLeadingTrivia(leadingTriviaList); + + var firstToken = mainNode.OpenBraceToken.GetNextToken(); + + mainNode = mainNode.ReplaceToken(firstToken, ApplyOpenBracket(firstToken)); + + return mainNode; + } + + private ClassDeclarationSyntax ApplyNodeChange(ClassDeclarationSyntax mainNode) + { + var leadingTriviaList = mainNode.GetLeadingTrivia(); + + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, CleanupTypes.RemoveDuplicateBetweenNamespaceMembers); + + mainNode = + mainNode + .WithOpenBraceToken(ApplyOpenBracket(mainNode.OpenBraceToken)) + .WithCloseBraceToken(ApplyCloseBracket(mainNode.CloseBraceToken)) + .WithLeadingTrivia(leadingTriviaList); + + var firstToken = mainNode.OpenBraceToken.GetNextToken(); + + mainNode = mainNode.ReplaceToken(firstToken, ApplyOpenBracket(firstToken)); + + _lastMember = null; + + return mainNode; + } + + private NamespaceDeclarationSyntax ApplyNodeChange(NamespaceDeclarationSyntax mainNode) + { + var leadingTriviaList = mainNode.GetLeadingTrivia(); + + leadingTriviaList = CleanUpListWithDefaultWhiteSpaces(leadingTriviaList, CleanupTypes.RemoveDuplicateBetweenNamespaceMembers); + + mainNode = + mainNode + .WithOpenBraceToken(ApplyOpenBracket(mainNode.OpenBraceToken)) + .WithCloseBraceToken(ApplyCloseBracket(mainNode.CloseBraceToken)) + .WithLeadingTrivia(leadingTriviaList); + + var firstToken = mainNode.OpenBraceToken.GetNextToken(); + + mainNode = mainNode.ReplaceToken(firstToken, ApplyOpenBracket(firstToken)); + + return mainNode; + } + + private SyntaxToken ApplyOpenBracket(SyntaxToken openBraceToken) + { + var x = + openBraceToken + .WithLeadingTrivia( + CleanUpListWithExactNumberOfWhiteSpaces(openBraceToken.LeadingTrivia, 0, CleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets, itsForCloseBrace: false)); + + return x; + } + + private SyntaxToken ApplyCloseBracket(SyntaxToken closeBraceToken) + { + return + closeBraceToken + .WithLeadingTrivia( + CleanUpListWithNoWhiteSpaces(closeBraceToken.LeadingTrivia, CleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets, itsForCloseBrace: true)); + } + + private SyntaxToken ApplyCloseBracket_OfEmptyBlock(SyntaxToken closeBraceToken) + { + return + closeBraceToken + .WithLeadingTrivia( + CleanUpListWithExactNumberOfWhiteSpaces(closeBraceToken.LeadingTrivia, 0, CleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets, itsForCloseBrace: true)); + } + + private bool CheckInnerBlocks(SyntaxNode node) + { + if (node is CatchClauseSyntax) return true; + if (node is FinallyClauseSyntax) return true; + if (node is ElseClauseSyntax) return true; + + return false; + } + + public override SyntaxToken VisitToken(SyntaxToken token) + { + if (default(SyntaxToken) == token) return base.VisitToken(token); + + _lastToken = token; + + var tokenKind = token.Kind(); + + _lastTokenIsAOpenBrace = tokenKind == SyntaxKind.OpenBraceToken; + _lastTokenIsACloseBrace = false; + + if (tokenKind == SyntaxKind.CloseBraceToken) + { + var triviasBetweenTokens = token.GetPreviousToken().TrailingTrivia.AddRange(token.LeadingTrivia); + + if (triviasBetweenTokens.Any(x => x.IsKind(SyntaxKind.EndOfLineTrivia))) + { + _lastTokenIsACloseBrace = true; + } + } + + return base.VisitToken(token); + } + + // SyntaxList ReWriteBlockStatements(SyntaxList blockStatements) + // { + // if (blockStatements.Any() == false) return blockStatements; + // var first = blockStatements[0]; + // var newFirst = first.WithLeadingTrivia(CleanUpListWithExactNumberOfWhitespaces(first.GetLeadingTrivia(), 0, null)); + // return blockStatements.Replace(first, newFirst); + // } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ZebbleCleaner.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ZebbleCleaner.cs new file mode 100644 index 0000000..f315ff6 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/ZebbleCleaner.cs @@ -0,0 +1,69 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners; + +public class ZebbleCleaner : CodeCleanerCommandRunnerBase +{ + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + if (ProjectItemDetails.SemanticModel is null) return initialSourceNode; + + var syntaxRewriter = new ReadOnlyRewriter(ProjectItemDetails.SemanticModel, IsReportOnlyMode, Options); + var modifiedSourceNode = syntaxRewriter.Visit(initialSourceNode); + + if (IsReportOnlyMode) + { + CollectMessages(syntaxRewriter.GetReport()); + return initialSourceNode; + } + + return modifiedSourceNode; + } + + private class ReadOnlyRewriter : CleanupCSharpSyntaxRewriter + { + private SemanticModel _semanticModel; + + public ReadOnlyRewriter(SemanticModel semanticModel, bool isReportOnlyMode, ICleanupOption options) + : base(isReportOnlyMode, options) + { + _semanticModel = semanticModel; + IsReportOnlyMode = isReportOnlyMode; + } + public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node) + { + if (node.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) return node; + if (CSharpExtensions.GetTypeInfo(_semanticModel, node.Declaration.Type).Type.Name != "Bindable") return node; + if (!node.Declaration.Type.IsKind(SyntaxKind.GenericName)) return node; + + if (CSharpExtensions.GetTypeInfo(_semanticModel, node.Declaration.Type).Type.ContainingNamespace.Name != "Zebble" && + CSharpExtensions.GetTypeInfo(_semanticModel, node.Declaration.Type).Type.ContainingNamespace.Name != "Olive") + return node; + + if (IsReportOnlyMode) + { + var lineSpan = node.GetFileLinePosSpan(); + + AddReport(new ChangesReport(node) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "Should Add Readonly Modifier", + Generator = nameof(ZebbleCleaner) + }); + + return node; + } + + return node.AddModifiers( + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword) + .WithTrailingTrivia(SyntaxFactory.ParseTrailingTrivia(" "))); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleaner.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleaner.cs new file mode 100644 index 0000000..9beabb4 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleaner.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public class CodeCleaner +{ + // TODO: By Alireza => To return Syntax node and pass syntaxNode no next clean up function and dont close windows for each cleanup , just for something like organize usings + // public ICodeCleaner Cleaner { get; private set; } + private ICodeCleaner _cleaner; + private Document _item; + + public CodeCleaner(ICodeCleaner cleaner, Document item) + { + _cleaner = cleaner; + _item = item; + } + + public Task RunAsync() => _cleaner.RunAsync(_item); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleanerCommandRunnerBase.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleanerCommandRunnerBase.cs new file mode 100644 index 0000000..28e770c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleanerCommandRunnerBase.cs @@ -0,0 +1,171 @@ +using System.Xml; +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Environment; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public abstract class CodeCleanerCommandRunnerBase : ICodeCleaner +{ + private static IList _changesReports; + public bool IsReportOnlyMode { get; set; } + + static CodeCleanerCommandRunnerBase() + { + _changesReports ??= new List(); + } + + public ProjectItemDetailsType ProjectItemDetails { get; private set; } + public ProjectItemDetailsType UnModifiedProjectItemDetails { get; protected set; } + + public virtual async Task RunAsync(Document item) + { + ProjectItemDetails = new ProjectItemDetailsType(item); + + if (IsReportOnlyMode) + { + await RefreshResultAsync(item.ToSyntaxNode()); + } + + UnModifiedProjectItemDetails = ProjectItemDetails; + + var initialSourceNode = CleanUpAsync(ProjectItemDetails.InitialSourceNode); + + if (!IsReportOnlyMode) + { + try + { + var result = await initialSourceNode; + await SaveResultAsync(result); + } + catch (Exception ex) + { + ErrorNotification.ErrorNotification.WriteErrorToFile(ex); + } + } + } + + protected async virtual Task RefreshResultAsync(SyntaxNode initialSourceNode) + { + if (UnModifiedProjectItemDetails != null && UnModifiedProjectItemDetails.InitialSourceNode.ToFullString().Replace("\r", "") + .Equals(initialSourceNode.ToFullString().Replace("\r", ""))) + { + ProjectItemDetails = new ProjectItemDetailsType(ProjectItemDetails.Document); + return ProjectItemDetails.InitialSourceNode; + } + + //if (ProjectItemDetails.Document != null) + { + var newDocument = ProjectItemDetails.Document.WithSyntaxRoot(initialSourceNode); + ProjectItemDetails.UpdateDocument(newDocument); + + // await TidyCSharpPackage.Instance.RefreshSolutionAsync(newDocument.Project.Solution); + } + + ProjectItemDetails = new ProjectItemDetailsType(ProjectItemDetails.Document); + return ProjectItemDetails.InitialSourceNode; + } + + protected virtual async Task SaveResultAsync(SyntaxNode initialSourceNode) + { + if (initialSourceNode == null || initialSourceNode == ProjectItemDetails.InitialSourceNode) return; + + if (UnModifiedProjectItemDetails.InitialSourceNode.ToFullString().Replace("\r", "") + .Equals(initialSourceNode.ToFullString().Replace("\r", ""))) + return; + + //if (ProjectItemDetails.Document == null) + { + initialSourceNode.WriteSourceTo(ProjectItemDetails.FilePath); + //return; + } + + var newDocument = ProjectItemDetails.Document.WithText(initialSourceNode.GetText()); + ProjectItemDetails.UpdateDocument(newDocument); + UnModifiedProjectItemDetails.UpdateDocument(newDocument); + //await TidyCSharpPackage.Instance.RefreshSolutionAsync(newDocument.Project.Solution); + } + + public virtual async Task CleanUpAsync(SyntaxNode initialSourceNode) => null; + + public bool IsEquivalentToUnModified(SyntaxNode initialSourceNode) + { + return initialSourceNode.IsEquivalentTo(UnModifiedProjectItemDetails.InitialSourceNode); + } + + public void CollectMessages(params ChangesReport[] changesReports) + { + foreach (var report in changesReports) + _changesReports.Add(report); + } + + public static void GenerateMessages(string outputPath) + { + var xmlWriterSettings = new XmlWriterSettings(); + xmlWriterSettings.Indent = true; + + using (var textWriter = XmlWriter.Create(outputPath, xmlWriterSettings)) + { + textWriter.WriteStartDocument(); + textWriter.WriteStartElement("Reports"); + + foreach (var change in _changesReports) + { + textWriter.WriteStartElement("Report"); + textWriter.WriteAttributeString("Generator", change.Generator); + textWriter.WriteElementString("LineNumber", change.LineNumber.ToString()); + textWriter.WriteElementString("Column", change.Column.ToString()); + textWriter.WriteElementString("FileName", change.FileName); + textWriter.WriteElementString("Message", change.Message); + textWriter.WriteEndElement(); + } + + textWriter.WriteEndElement(); + textWriter.WriteEndDocument(); + textWriter.Flush(); + } + _changesReports.Clear(); + } + + public ICleanupOption Options { get; set; } + + public bool CheckOption(int? optionItem) => Options.Should(optionItem); + + public class ProjectItemDetailsType + { + public Document Document { get; private set; } + + private SemanticModel _semanticModel; + public SemanticModel SemanticModel + { + get + { + if (_semanticModel == null && Document != null) + { + Task.Run(async delegate + { + _semanticModel = await Document.GetSemanticModelAsync(); + }).GetAwaiter().GetResult(); + } + + return _semanticModel; + } + } + public SyntaxNode InitialSourceNode { get; private set; } + + public string FilePath { get; private set; } + + public ProjectItemDetailsType(Document item) + { + FilePath = item.ToFullPathPropertyValue(); + Document = item; + + Task.Run(async delegate + { + InitialSourceNode = item != null ? await item.GetSyntaxRootAsync() : item.ToSyntaxNode(); + }).GetAwaiter().GetResult(); + } + + public void UpdateDocument(Document document)=>Document=document; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleanerHost.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleanerHost.cs new file mode 100644 index 0000000..b5d1367 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/CodeCleanerHost.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; +using TidyCSharp.Cli.Menus.Cleanup.Utils; +using TidyCSharp.Cli.Properties; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public class CodeCleanerHost +{ + public static async Task RunAsync(Document item, CodeCleanerType command, CleanupOptions cleanupOptions, bool isReportOnly = false) + { + Console.WriteLine($"Processing {Path.GetFileName(item.FilePath)} : {command}"); + if (!ActiveDocument.IsValid(item)) + ErrorNotification.ErrorNotification.EmailError(Resources.PrivateModifierCleanUpFailed); + + else + { + var instance = CodeCleanerFactory.Create(command, cleanupOptions, isReportOnly); + await new CodeCleaner(instance, item).RunAsync(); + } + } + + public static void GenerateMessages(string outputPath) => CodeCleanerCommandRunnerBase.GenerateMessages(outputPath); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICleanupOption.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICleanupOption.cs new file mode 100644 index 0000000..77ce55c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICleanupOption.cs @@ -0,0 +1,12 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public interface ICleanupOption +{ + int? CleanupItemsInteger { get; } + + void Accept(IMainCleanup mainCleanup); + + CodeCleanerType GetCodeCleanerType(); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICleanupOptionHelper.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICleanupOptionHelper.cs new file mode 100644 index 0000000..5486026 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICleanupOptionHelper.cs @@ -0,0 +1,13 @@ +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public static class CleanupOptionHelper +{ + public static bool Should(this ICleanupOption options, int? optionItem) + { + if (options == null) return true; + if (options.CleanupItemsInteger == null) return true; + if (optionItem == null) return true; + + return (options.CleanupItemsInteger & optionItem) == optionItem; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICodeCleaner.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICodeCleaner.cs new file mode 100644 index 0000000..2a2da5e --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/ICodeCleaner.cs @@ -0,0 +1,9 @@ + +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public interface ICodeCleaner +{ + Task RunAsync(Document item); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/OptionsBase.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/OptionsBase.cs new file mode 100644 index 0000000..9761543 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandRunners/_Infra/OptionsBase.cs @@ -0,0 +1,29 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +public abstract class OptionsBase : ICleanupOption +{ + public virtual int? CleanupItemsInteger { get; private set; } + + public abstract CodeCleanerType GetCodeCleanerType(); + + public void Accept(IMainCleanup mainCleanup) + { + if (mainCleanup.MainCleanupItemType == GetCodeCleanerType()) + { + var selectedItems = mainCleanup.GetSelectedSubitems().Select(x => x.CleanerType).ToArray(); + + CleanupItemsInteger = selectedItems.FirstOrDefault(); + + foreach (var item in selectedItems) + CleanupItemsInteger |= item; + } + } + + public override string ToString() + { + return $"{(CleanupItemsInteger ?? 0).ToString()}"; + // return $"{(int)GetCodeCleanerType()}:{(CleanupItemsInteger ?? 0).ToString()}"; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/ActionReadOnlyCodeCleanup.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/ActionReadOnlyCodeCleanup.cs new file mode 100644 index 0000000..45bd367 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/ActionReadOnlyCodeCleanup.cs @@ -0,0 +1,289 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra.ReadOnly; +using CamelCasedClassFieldsCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields.Option.CleanupTypes; +using CamelCasedMethodCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable.Option.CleanupTypes; +using MembersToExpressionBodiedCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.ConvertMembersToExpressionBodied.Option.CleanupTypes; +using NormalizeWhitespaceCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option.CleanupTypes; +using RemoveExtraThisCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification.Option.CleanupTypes; +using RemovePrivateModifierCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover.Option.CleanupTypes; +using SimplifyClassFieldDeclarationCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplifyClassFieldDeclarations.Option.CleanupTypes; +using SimplyAsyncCallCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall.Option.CleanupTypes; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; + +public class ActionReadOnlyCodeCleanup +{ + public CleanupOptions CleanupOptions { get; private set; } + public ActionReadOnlyCodeCleanup() + { + CleanupOptions = new CleanupOptions(); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertZebbleGeneralMethods)); + // CleanupOptions.Accept(new + // ReadOnlyMainCleanup(CodeCleanerType.OrganizeUsingDirectives)); + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.SortClassMembers)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.SimplifyVariableDeclarations)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.RemoveAttributeKeywork)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.UpgradeCSharpSyntax)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertPropertiesToAutoProperties)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertFullNameTypesToBuiltInTypes)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.CompactSmallIfElseStatements)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertMsharpGeneralMethods)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertMsharpUiMethods)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertMsharpModelMethods)); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.ConvertMembersToExpressionBodied, + new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)MembersToExpressionBodiedCleanupTypes.ConvertConstructors, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(MembersToExpressionBodiedCleanupTypes), MembersToExpressionBodiedCleanupTypes.ConvertConstructors).ToString() + }, + new() + { + CleanerType = (int)MembersToExpressionBodiedCleanupTypes.ConvertMethods, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(MembersToExpressionBodiedCleanupTypes), + MembersToExpressionBodiedCleanupTypes.ConvertMethods).ToString() + }, + new() + { + CleanerType = (int)MembersToExpressionBodiedCleanupTypes.ConvertReadOnlyProperty, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(MembersToExpressionBodiedCleanupTypes), MembersToExpressionBodiedCleanupTypes.ConvertReadOnlyProperty).ToString() + } + } + )); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.CamelCasedFields, new CleanerItemUiInfo[] + { + new() + { + CleanerType = (int)CamelCasedClassFieldsCleanupTypes.NormalFields, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(CamelCasedClassFieldsCleanupTypes), + CamelCasedClassFieldsCleanupTypes.NormalFields).ToString() + }, + new() + { + CleanerType = (int)CamelCasedClassFieldsCleanupTypes.ConstFields, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(CamelCasedClassFieldsCleanupTypes), + CamelCasedClassFieldsCleanupTypes.ConstFields).ToString() + }, + })); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.SimplyAsyncCalls, new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)SimplyAsyncCallCleanupTypes.SingleExpression, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(SimplyAsyncCallCleanupTypes), SimplyAsyncCallCleanupTypes.SingleExpression).ToString() + }, + })); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.PrivateAccessModifier, new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveClassFieldsPrivateModifier, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemovePrivateModifierCleanupTypes), RemovePrivateModifierCleanupTypes.RemoveClassMethodsPrivateModifier).ToString() + }, + new() + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveClassMethodsPrivateModifier, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemovePrivateModifierCleanupTypes), RemovePrivateModifierCleanupTypes.RemoveClassMethodsPrivateModifier).ToString() + }, + new() + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveClassPropertiesPrivateModifier, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemovePrivateModifierCleanupTypes), RemovePrivateModifierCleanupTypes.RemoveClassPropertiesPrivateModifier).ToString() + }, + new() + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveNestedClassPrivateModifier, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemovePrivateModifierCleanupTypes), RemovePrivateModifierCleanupTypes.RemoveNestedClassPrivateModifier).ToString() + }, + })); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.RemoveExtraThisQualification, new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)RemoveExtraThisCleanupTypes.RemoveFromFieldsCall, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemoveExtraThisCleanupTypes), RemoveExtraThisCleanupTypes.RemoveFromFieldsCall).ToString() + }, + new() + { + CleanerType = (int)RemoveExtraThisCleanupTypes.RemoveFromMethodCall, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemoveExtraThisCleanupTypes), RemoveExtraThisCleanupTypes.RemoveFromMethodCall).ToString() + }, + new() + { + CleanerType = (int)RemoveExtraThisCleanupTypes.RemoveFromPropertiesCall, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(RemoveExtraThisCleanupTypes), RemoveExtraThisCleanupTypes.RemoveFromPropertiesCall).ToString() + }, + })); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.CamelCasedMethodVariable, new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)CamelCasedMethodCleanupTypes.LocalVariable, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(CamelCasedMethodCleanupTypes), CamelCasedMethodCleanupTypes.LocalVariable).ToString() + }, + new() + { + CleanerType = (int)CamelCasedMethodCleanupTypes.MethodParameter, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(CamelCasedMethodCleanupTypes), CamelCasedMethodCleanupTypes.MethodParameter).ToString() + }, })); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.SimplifyClassFieldDeclarations, new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)SimplifyClassFieldDeclarationCleanupTypes.GroupAndMergeClassFields, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(SimplifyClassFieldDeclarationCleanupTypes), SimplifyClassFieldDeclarationCleanupTypes.GroupAndMergeClassFields).ToString() + }, + new() + { + CleanerType = (int)SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerLiteral, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(SimplifyClassFieldDeclarationCleanupTypes), SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerLiteral).ToString() + },new() + { + CleanerType = (int)SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerNull, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(SimplifyClassFieldDeclarationCleanupTypes), SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerNull).ToString() + }, + })); + + CleanupOptions.Accept(new + ReadOnlyMainCleanup(CodeCleanerType.NormalizeWhiteSpaces, new CleanerItemUiInfo[] { + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddingBlankAfterBlockCloseBracket, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.AddingBlankAfterBlockCloseBracket).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddingBlankAfterMethodCloseBracket, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.AddingBlankAfterMethodCloseBracket).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddBlankLineBetweenStatementsMoreThanOneLine, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.AddBlankLineBetweenStatementsMoreThanOneLine).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.InsertSpaceBeforeCommentText, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.InsertSpaceBeforeCommentText).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveBracketsOfBlockThatHasOnlyOneStatementWithLengthShorterThan80Chars, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveBracketsOfBlockThatHasOnlyOneStatementWithLengthShorterThan80Chars).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenClassMembers, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenClassMembers).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenMethodsStatements, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenMethodsStatements).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenNamespaceMembers, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenNamespaceMembers).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideComments, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideComments).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideUsings, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideUsings).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.TrimTheFile, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.TrimTheFile).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.UseSlashInsteadOfSlashSlash, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.UseSlashInsteadOfSlashSlash).ToString() + }, + new() + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddAnEmptyLineAfterUsingStatements, + ShouldBeSelectedByDefault = true, + Name = Enum.GetName(typeof(NormalizeWhitespaceCleanupTypes), NormalizeWhitespaceCleanupTypes.AddAnEmptyLineAfterUsingStatements).ToString() + }, + })); + } + + public void RunReadOnlyCleanUp() + { + ActionsOnCSharp.CSharpAction.TargetAction desiredAction = ActionsOnCSharp.ActionsCSharpOnFile.ReportOnlyDoNotCleanupAsync; + ActionsOnCSharp.ActionCSharpOnSolution.InvokeAsync(desiredAction, CleanupOptions); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/ActionSafeRulesCodeCleanup.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/ActionSafeRulesCodeCleanup.cs new file mode 100644 index 0000000..bb873d2 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/ActionSafeRulesCodeCleanup.cs @@ -0,0 +1,285 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; +using CamelCasedClassFieldsCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields.Option.CleanupTypes; +using CamelCasedMethodCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable.Option.CleanupTypes; +using NormalizeWhitespaceCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option.CleanupTypes; +using RemoveExtraThisCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification.Option.CleanupTypes; +using RemovePrivateModifierCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover.Option.CleanupTypes; +using SimplyAsyncCallCleanupTypes = TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall.Option.CleanupTypes; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; + +public class ActionSafeRulesCodeCleanup +{ + public CleanupOptions CleanupOptions { get; private set; } + public ActionSafeRulesCodeCleanup() + { + CleanupOptions = new CleanupOptions(); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.ConvertZebbleGeneralMethods)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.OrganizeUsingDirectives)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.SortClassMembers)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.SimplifyVariableDeclarations)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.RemoveAttributeKeywork)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.UpgradeCSharpSyntax)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.ConvertPropertiesToAutoProperties)); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.ConvertFullNameTypesToBuiltInTypes)); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.CompactSmallIfElseStatements)); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.ConvertMsharpGeneralMethods)); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.ConvertMsharpUIMethods)); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.ConvertMsharpModelMethods)); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.ConvertMembersToExpressionBodied, + // new[] { + // new CleanerItemUIInfo + // { + // CleanerType = (int)MembersToExpressionBodiedCleanupTypes.ConvertConstructors, + // ShouldBeSelectedByDefault = true, + // Name = nameof(MembersToExpressionBodiedCleanupTypes.ConvertConstructors) + // }, + // new CleanerItemUIInfo + // { + // CleanerType = (int)MembersToExpressionBodiedCleanupTypes.ConvertMethods, + // ShouldBeSelectedByDefault = true, + // Name = nameof(MembersToExpressionBodiedCleanupTypes.ConvertMethods) + // }, + // new CleanerItemUIInfo + // { + // CleanerType = (int)MembersToExpressionBodiedCleanupTypes.ConvertReadOnlyProperty, + // ShouldBeSelectedByDefault = true, + // Name = nameof(MembersToExpressionBodiedCleanupTypes.ConvertReadOnlyProperty) + // } + // } + // )); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.CamelCasedFields, new[] + { + new CleanerItemUiInfo + { + CleanerType = (int)CamelCasedClassFieldsCleanupTypes.NormalFields, + ShouldBeSelectedByDefault = true, + Name =nameof( CamelCasedClassFieldsCleanupTypes.NormalFields) + }, + new CleanerItemUiInfo + { + CleanerType = (int)CamelCasedClassFieldsCleanupTypes.ConstFields, + ShouldBeSelectedByDefault = true, + Name = nameof( CamelCasedClassFieldsCleanupTypes.ConstFields) + }, + })); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.SimplyAsyncCalls, new[] { + new CleanerItemUiInfo + { + CleanerType = (int)SimplyAsyncCallCleanupTypes.SingleExpression, + ShouldBeSelectedByDefault = true, + Name = nameof(SimplyAsyncCallCleanupTypes.SingleExpression) + }, + })); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.PrivateAccessModifier, new[] { + new CleanerItemUiInfo + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveClassFieldsPrivateModifier, + ShouldBeSelectedByDefault = true, + Name =nameof(RemovePrivateModifierCleanupTypes.RemoveClassMethodsPrivateModifier) + }, + new CleanerItemUiInfo + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveClassMethodsPrivateModifier, + ShouldBeSelectedByDefault = true, + Name =nameof(RemovePrivateModifierCleanupTypes.RemoveClassMethodsPrivateModifier) + }, + new CleanerItemUiInfo + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveClassPropertiesPrivateModifier, + ShouldBeSelectedByDefault = true, + Name =nameof(RemovePrivateModifierCleanupTypes.RemoveClassPropertiesPrivateModifier) + }, + new CleanerItemUiInfo + { + CleanerType = (int)RemovePrivateModifierCleanupTypes.RemoveNestedClassPrivateModifier, + ShouldBeSelectedByDefault = true, + Name = nameof(RemovePrivateModifierCleanupTypes.RemoveNestedClassPrivateModifier) + }, + })); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.RemoveExtraThisQualification, new[] { + new CleanerItemUiInfo + { + CleanerType = (int)RemoveExtraThisCleanupTypes.RemoveFromFieldsCall, + ShouldBeSelectedByDefault = true, + Name = nameof(RemoveExtraThisCleanupTypes.RemoveFromFieldsCall) + }, + new CleanerItemUiInfo + { + CleanerType = (int)RemoveExtraThisCleanupTypes.RemoveFromMethodCall, + ShouldBeSelectedByDefault = true, + Name = nameof(RemoveExtraThisCleanupTypes.RemoveFromMethodCall) + }, + new CleanerItemUiInfo + { + CleanerType = (int)RemoveExtraThisCleanupTypes.RemoveFromPropertiesCall, + ShouldBeSelectedByDefault = true, + Name = nameof(RemoveExtraThisCleanupTypes.RemoveFromPropertiesCall) + }, + })); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.CamelCasedMethodVariable, new[] { + new CleanerItemUiInfo + { + CleanerType = (int)CamelCasedMethodCleanupTypes.LocalVariable, + ShouldBeSelectedByDefault = true, + Name = nameof(CamelCasedMethodCleanupTypes.LocalVariable) + }, + new CleanerItemUiInfo + { + CleanerType = (int)CamelCasedMethodCleanupTypes.MethodParameter, + ShouldBeSelectedByDefault = true, + Name = nameof(CamelCasedMethodCleanupTypes.MethodParameter) + }, })); + + //CleanupOptions.Accept(new + // SafeRuleMainCleanup(CodeCleanerType.SimplifyClassFieldDeclarations, new[] { + // new CleanerItemUIInfo + // { + // CleanerType = (int)SimplifyClassFieldDeclarationCleanupTypes.GroupAndMergeClassFields, + // ShouldBeSelectedByDefault = true, + // Name = nameof(SimplifyClassFieldDeclarationCleanupTypes.GroupAndMergeClassFields) + // }, + // new CleanerItemUIInfo + // { + // CleanerType = (int)SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerLiteral, + // ShouldBeSelectedByDefault = true, + // Name = nameof(SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerLiteral) + // },new CleanerItemUIInfo + // { + // CleanerType = (int)SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerNull, + // ShouldBeSelectedByDefault = true, + // Name = nameof(SimplifyClassFieldDeclarationCleanupTypes.RemoveClassFieldsInitializerNull) + // }, + // })); + + CleanupOptions.Accept(new + SafeRuleMainCleanup(CodeCleanerType.NormalizeWhiteSpaces, new[] { + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddingBlankAfterBlockCloseBracket, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.AddingBlankAfterBlockCloseBracket) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddingBlankAfterMethodCloseBracket, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.AddingBlankAfterMethodCloseBracket) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddBlankLineBetweenStatementsMoreThanOneLine, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.AddBlankLineBetweenStatementsMoreThanOneLine) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.InsertSpaceBeforeCommentText, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.InsertSpaceBeforeCommentText) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveBlankAfterOpenBracketAndBeforeCloseBrackets) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveBracketsOfBlockThatHasOnlyOneStatementWithLengthShorterThan80Chars, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveBracketsOfBlockThatHasOnlyOneStatementWithLengthShorterThan80Chars) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenClassMembers, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenClassMembers) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenMethodsStatements, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenMethodsStatements) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenNamespaceMembers, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveDuplicateBetweenNamespaceMembers) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideComments, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideComments) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideUsings, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.RemoveDuplicateInsideUsings) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.TrimTheFile, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.TrimTheFile) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.UseSlashInsteadOfSlashSlash, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.UseSlashInsteadOfSlashSlash) + }, + new CleanerItemUiInfo + { + CleanerType = (int)NormalizeWhitespaceCleanupTypes.AddAnEmptyLineAfterUsingStatements, + ShouldBeSelectedByDefault = true, + Name = nameof(NormalizeWhitespaceCleanupTypes.AddAnEmptyLineAfterUsingStatements) + }, + })); + } + + public async Task RunSafeRulesCleanUpAsync() + { + ActionsOnCSharp.CSharpAction.TargetAction desiredAction = ActionsOnCSharp.ActionsCSharpOnFile.DoCleanupAsync; + await ActionsOnCSharp.ActionCSharpOnSolution.InvokeAsync(desiredAction, CleanupOptions); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/CleanupOptions.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/CleanupOptions.cs new file mode 100644 index 0000000..9dcf35c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/CleanupOptions.cs @@ -0,0 +1,79 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields.Option; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; + +public class CleanupOptions +{ + private List _optionItems = new(); + + public const string ToStringSeprator2 = ":", ToStringSeprator = ";"; + public List ActionTypes { get; private set; } = new(); + + public CleanupOptions() + { + _optionItems.Add(CamelCasedFields = new CommandRunners.CamelCasedClassFields.Option.Options()); + _optionItems.Add(CamelCasedLocalVariable = new CommandRunners.CamelCasedMethodVariable.Option.Options()); + _optionItems.Add(ConvertMembersToExpressionBodied = new CommandRunners.ConvertMembersToExpressionBodied.Option.Options()); + _optionItems.Add(PrivateModifierRemover = new CommandRunners.PrivateModifierRemover.Option.Options()); + _optionItems.Add(RemoveExtraThisQualification = new CommandRunners.RemoveExtraThisQualification.Option.Options()); + _optionItems.Add(SimplifyClassFieldDeclarations = new CommandRunners.SimplifyClassFieldDeclarations.Option.Options()); + _optionItems.Add(SimplyAsyncCall = new CommandRunners.SimplyAsyncCall.Option.Options()); + _optionItems.Add(WhiteSpaceNormalizer = new CommandRunners.Whitespace.Option.Options()); + } + + public void Accept(IMainCleanup mainCleanup) + { + foreach (var item in _optionItems) + item.Accept(mainCleanup); + + if (mainCleanup.IsMainObjectSelected) + { + ActionTypes.Add(mainCleanup.MainCleanupItemType); + } + } + + public CommandRunners.CamelCasedClassFields.Option.Options CamelCasedFields { get; private set; } + public CommandRunners.CamelCasedMethodVariable.Option.Options CamelCasedLocalVariable { get; private set; } + public CommandRunners.ConvertMembersToExpressionBodied.Option.Options ConvertMembersToExpressionBodied { get; private set; } + public CommandRunners.PrivateModifierRemover.Option.Options PrivateModifierRemover { get; private set; } + public CommandRunners.RemoveExtraThisQualification.Option.Options RemoveExtraThisQualification { get; private set; } + public CommandRunners.SimplifyClassFieldDeclarations.Option.Options SimplifyClassFieldDeclarations { get; private set; } + public CommandRunners.SimplyAsyncCall.Option.Options SimplyAsyncCall { get; private set; } + public CommandRunners.Whitespace.Option.Options WhiteSpaceNormalizer { get; private set; } + + public string SerializeValues() + { + var values = Enum.GetValues(typeof(CodeCleanerType)).Cast(); + + return string.Join(ToStringSeprator, values + .Select(enumValueAsObject => + { + var optionItem = _optionItems.FirstOrDefault(o => (int)o.GetCodeCleanerType() == enumValueAsObject); + + var isParentSelected = ActionTypes.Contains((CodeCleanerType)enumValueAsObject); + + if (optionItem == null) return $"{enumValueAsObject}{ToStringSeprator2}{isParentSelected}{ToStringSeprator2}{-1}"; + + if (optionItem.CleanupItemsInteger.HasValue == false) + return $"{enumValueAsObject}{ToStringSeprator2}{isParentSelected}{ToStringSeprator2}{0}"; + + return $"{enumValueAsObject}{ToStringSeprator2}{isParentSelected}{ToStringSeprator2}{optionItem.CleanupItemsInteger.Value}"; + }) + ); + } + + public override string ToString() + { + return string.Join(ToStringSeprator, ActionTypes + .Select(x => + { + var optionItem = _optionItems.FirstOrDefault(o => o.GetCodeCleanerType() == x); + if (optionItem == null) return $"{(int)x}{ToStringSeprator2}{-1}"; + if (optionItem.CleanupItemsInteger.HasValue == false) return $"{(int)x}{ToStringSeprator2}{0}"; + return $"{(int)x}{ToStringSeprator2}{optionItem.CleanupItemsInteger.Value}"; + }) + ); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/CodeCleanerFactory.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/CodeCleanerFactory.cs new file mode 100644 index 0000000..ad55e29 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/CodeCleanerFactory.cs @@ -0,0 +1,64 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedClassFields; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.CamelCasedMethodVariable; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.ConvertMembersToExpressionBodied; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.PrivateModifierRemover; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.RemoveExtraThisQualification; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplifyClassFieldDeclarations; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.SimplyAsyncCall; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; + +public class CodeCleanerFactory +{ + public static ICodeCleaner Create(CodeCleanerType type, CleanupOptions cleanupOptions, bool isReportOnly = false) + { + switch (type) + { + case CodeCleanerType.NormalizeWhiteSpaces: + return new WhiteSpaceNormalizer() { Options = cleanupOptions.WhiteSpaceNormalizer, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertMembersToExpressionBodied: + return new ConvertMembersToExpressionBodied() { Options = cleanupOptions.ConvertMembersToExpressionBodied, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertFullNameTypesToBuiltInTypes: + return new ConvertFullNameTypesToBuiltInTypes() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.SortClassMembers: + return new SortClassMembers() { IsReportOnlyMode = isReportOnly }; ; + case CodeCleanerType.SimplyAsyncCalls: + return new SimplyAsyncCalls() { Options = cleanupOptions.SimplyAsyncCall, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.SimplifyClassFieldDeclarations: + return new SimplifyClassFieldDeclarations() { Options = cleanupOptions.SimplifyClassFieldDeclarations, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.RemoveAttributeKeywork: + return new RemoveAttributeKeywork() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.CompactSmallIfElseStatements: + return new CompactSmallIfElseStatements() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.RemoveExtraThisQualification: + return new RemoveExtraThisQualification() { Options = cleanupOptions.RemoveExtraThisQualification, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.CamelCasedMethodVariable: + return new CamelCasedLocalVariable() { Options = cleanupOptions.CamelCasedLocalVariable, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.CamelCasedFields: + return new CamelCasedFields() { Options = cleanupOptions.CamelCasedFields, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.PrivateAccessModifier: + return new PrivateModifierRemover() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.OrganizeUsingDirectives: + return new UsingDirectiveOrganizer() { Options = cleanupOptions.PrivateModifierRemover, IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.SimplifyVariableDeclarations: + return new SimplifyVariableDeclarations() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertPropertiesToAutoProperties: + return new ConvertPropertiesToAutoProperties() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertMsharpUiMethods: + return new MSharpUiCleaner() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertMsharpModelMethods: + return new MSharpModelCleaner() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertMsharpGeneralMethods: + return new MSharpGeneralCleaner() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.ConvertZebbleGeneralMethods: + return new ZebbleCleaner() { IsReportOnlyMode = isReportOnly }; + case CodeCleanerType.UpgradeCSharpSyntax: + return new CSharpSyntaxUpgrade() { IsReportOnlyMode = isReportOnly }; + default: return null; // TODO + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CleanerItemUIInfo.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CleanerItemUIInfo.cs new file mode 100644 index 0000000..d0dec55 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CleanerItemUIInfo.cs @@ -0,0 +1,12 @@ +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +public class CleanerItemUiInfo +{ + public string Name { get; set; } + public int CleanerType { get; set; } + public int Order { get; set; } + + public bool ShouldBeSelectedByDefault { get; set; } + + public override string ToString() => Name; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CleanupItemAttribute.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CleanupItemAttribute.cs new file mode 100644 index 0000000..d4cb88c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CleanupItemAttribute.cs @@ -0,0 +1,16 @@ +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)] +public sealed class CleanupItemAttribute : Attribute +{ + public string Title { get; set; } = null; + public int FirstOrder { get; set; } = int.MaxValue; + public bool SelectedByDefault { get; set; } = true; + + public int? Order => FirstOrder == int.MaxValue ? (int?)null : FirstOrder; + public Type SubitemType { get; set; } + + public CleanupItemAttribute() + { + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CodeCleanerType.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CodeCleanerType.cs new file mode 100644 index 0000000..d4816af --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/CodeCleanerType.cs @@ -0,0 +1,67 @@ +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners.Whitespace.Option; + +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +[Flags] +public enum CodeCleanerType //CleanerMainType +{ + [CleanupItem(Title = "Remove and sort Usings", FirstOrder = 0)] + OrganizeUsingDirectives = 0x04, + + [CleanupItem(Title = "Normalize white spaces", FirstOrder = 1, SubitemType = typeof(CleanupTypes))] + NormalizeWhiteSpaces = 0x01, + + [CleanupItem(Title = "Remove unnecessary explicit 'private' where it's the default", FirstOrder = 2, SubitemType = typeof(CommandRunners.PrivateModifierRemover.Option.CleanupTypes))] + PrivateAccessModifier = 0x02, + + [CleanupItem(Title = "Small methods properties -> Expression bodied", FirstOrder = 3, SubitemType = typeof(CommandRunners.ConvertMembersToExpressionBodied.Option.CleanupTypes))] + ConvertMembersToExpressionBodied = 0x08, + + [CleanupItem(Title = "Simplify async calls", FirstOrder = 4, SubitemType = typeof(CommandRunners.SimplyAsyncCall.Option.CleanupTypes))] + SimplyAsyncCalls = 0x20, + + [CleanupItem(Title = "Compact multiple class field declarations into one line", FirstOrder = 5, SubitemType = typeof(CommandRunners.SimplifyClassFieldDeclarations.Option.CleanupTypes))] + SimplifyClassFieldDeclarations = 0x80, + + [CleanupItem(Title = "Use 'var' for variable declarations", FirstOrder = 5)] + SimplifyVariableDeclarations = 0x8000, + + [CleanupItem(Title = "Convert traditional properties to auto-properties", FirstOrder = 5)] + ConvertPropertiesToAutoProperties = 0x10000, + + [CleanupItem(Title = "Remove unnecessary \"this.\"", FirstOrder = 6, SubitemType = typeof(CommandRunners.RemoveExtraThisQualification.Option.CleanupTypes))] + RemoveExtraThisQualification = 0x400, + + [CleanupItem(Title = "Use camelCase for...", FirstOrder = 7, SubitemType = typeof(CommandRunners.CamelCasedMethodVariable.Option.CleanupTypes))] + CamelCasedMethodVariable = 0x800, + + [CleanupItem(Title = "Class field and const casing...", FirstOrder = 8, SubitemType = typeof(CommandRunners.CamelCasedClassFields.Option.CleanupTypes))] + CamelCasedFields = 0x1000, + + [CleanupItem(Title = "Move constructors before methods", FirstOrder = 9)] + SortClassMembers = 0x40, + + [CleanupItem(Title = "Remove unnecessary 'Attribute' (e.g. [SomethingAttribute] -> [Something])", FirstOrder = 10)] + RemoveAttributeKeywork = 0x100, + + [CleanupItem(Title = "Compact small if/else blocks", FirstOrder = 11, SelectedByDefault = false)] + CompactSmallIfElseStatements = 0x200, + + [CleanupItem(Title = "Use C# alias type names (e.g. System.Int32 -> int)", FirstOrder = 11)] + ConvertFullNameTypesToBuiltInTypes = 0x10, + + [CleanupItem(Title = "Renew M# UI methods", FirstOrder = 12)] + ConvertMsharpUiMethods = 0x25, + + [CleanupItem(Title = "Renew M# Model methods", FirstOrder = 13)] + ConvertMsharpModelMethods = 0x26, + + [CleanupItem(Title = "Renew M# General Statements", FirstOrder = 14)] + ConvertMsharpGeneralMethods = 0x27, + + [CleanupItem(Title = "Renew Zebble General Statements", FirstOrder = 15)] + ConvertZebbleGeneralMethods = 0x28, + + [CleanupItem(Title = "Upgrade C# Syntax", FirstOrder = 16)] + UpgradeCSharpSyntax = 0x29 +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/IMainCleanup.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/IMainCleanup.cs new file mode 100644 index 0000000..4ef6d76 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/IMainCleanup.cs @@ -0,0 +1,12 @@ +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +public interface IMainCleanup +{ + bool IsMainObjectSelected { get; } + CodeCleanerType MainCleanupItemType { get; } + bool HasSubitems { get; } + CleanerItemUiInfo[] GetSelectedSubitems(); + void SetItemsCheckState(int value, bool checkedState); + void SetMainItemCheckState(bool isSelected); + void ResetItemsCheckState(); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/ReadOnly/ReadOnlyMainCleanup.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/ReadOnly/ReadOnlyMainCleanup.cs new file mode 100644 index 0000000..e818e2d --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/ReadOnly/ReadOnlyMainCleanup.cs @@ -0,0 +1,38 @@ +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra.ReadOnly; + +public class ReadOnlyMainCleanup : IMainCleanup +{ + public bool IsMainObjectSelected { get; private set; } + public CodeCleanerType MainCleanupItemType { get; private set; } + public bool HasSubitems { get; private set; } + public CleanerItemUiInfo[] CleanerItemUiInfos { get; private set; } + + public ReadOnlyMainCleanup(CodeCleanerType mainCleanupItemType, + bool isMainObjectSelected = true) + { + MainCleanupItemType = mainCleanupItemType; + IsMainObjectSelected = isMainObjectSelected; + } + + public ReadOnlyMainCleanup(CodeCleanerType mainCleanupItemType, CleanerItemUiInfo[] cleanerItemUiInfos, + bool isMainObjectSelected = true) + { + MainCleanupItemType = mainCleanupItemType; + CleanerItemUiInfos = cleanerItemUiInfos; + IsMainObjectSelected = isMainObjectSelected; + } + + public CleanerItemUiInfo[] GetSelectedSubitems() => CleanerItemUiInfos; + + public void ResetItemsCheckState() + { + throw new NotImplementedException(); + } + + public void SetItemsCheckState(int value, bool checkedState) + { + throw new NotImplementedException(); + } + + public void SetMainItemCheckState(bool isSelected) => IsMainObjectSelected = isSelected; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/SafeRuleMainCleanup.cs b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/SafeRuleMainCleanup.cs new file mode 100644 index 0000000..9954a30 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/CommandsHandlers/Infra/SafeRuleMainCleanup.cs @@ -0,0 +1,38 @@ +namespace TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers.Infra; + +public class SafeRuleMainCleanup : IMainCleanup +{ + public bool IsMainObjectSelected { get; private set; } + public CodeCleanerType MainCleanupItemType { get; private set; } + public bool HasSubitems { get; private set; } + public CleanerItemUiInfo[] CleanerItemUiInfos { get; private set; } + + public SafeRuleMainCleanup(CodeCleanerType mainCleanupItemType, + bool isMainObjectSelected = true) + { + MainCleanupItemType = mainCleanupItemType; + IsMainObjectSelected = isMainObjectSelected; + } + + public SafeRuleMainCleanup(CodeCleanerType mainCleanupItemType, CleanerItemUiInfo[] cleanerItemUiInfos, + bool isMainObjectSelected = true) + { + MainCleanupItemType = mainCleanupItemType; + CleanerItemUiInfos = cleanerItemUiInfos; + IsMainObjectSelected = isMainObjectSelected; + } + + public CleanerItemUiInfo[] GetSelectedSubitems() => CleanerItemUiInfos; + + public void ResetItemsCheckState() + { + throw new NotImplementedException(); + } + + public void SetItemsCheckState(int value, bool checkedState) + { + throw new NotImplementedException(); + } + + public void SetMainItemCheckState(bool isSelected) => IsMainObjectSelected = isSelected; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/ArgumentListSyntaxExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/ArgumentListSyntaxExtractor.cs new file mode 100644 index 0000000..8188be6 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/ArgumentListSyntaxExtractor.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public static class ArgumentListSyntaxExtractor +{ + public static BlockSyntax GetBlockSyntaxOfFirstArgument(this ArgumentListSyntax node) + { + return node.Arguments.FirstOrDefault() + .Expression.As() + ?.Body.As(); + } + + public static ArgumentSyntax FirstArgument(this ArgumentListSyntax node) + { + return node.Arguments.FirstOrDefault(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/FieldExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/FieldExtractor.cs new file mode 100644 index 0000000..e7bca03 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/FieldExtractor.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public class FieldExtractor : ISyntaxTreeMemberExtractor +{ + public List Extraxt(SyntaxNode root, SyntaxKind kind) + { + return root.DescendantNodes().OfType() + .Where(fld => fld.Modifiers.Any(m => m.IsKind(kind))) + .SelectMany(method => method.Modifiers.Where(m => m.IsKind(kind))) + .ToList(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/ISyntaxTreeMemberExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/ISyntaxTreeMemberExtractor.cs new file mode 100644 index 0000000..003a5c9 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/ISyntaxTreeMemberExtractor.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public interface ISyntaxTreeMemberExtractor +{ + List Extraxt(SyntaxNode root, SyntaxKind kind); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/InvocationExpressionExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/InvocationExpressionExtractor.cs new file mode 100644 index 0000000..5019a35 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/InvocationExpressionExtractor.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public static class InvocationExpressionExtractor +{ + public static string GetLeftSideIdentifier(this InvocationExpressionSyntax node) + { + return node.Expression.As()?.Expression.ToString(); + } + + public static ExpressionSyntax GetLeftSideExpression(this InvocationExpressionSyntax node) + { + return node.Expression.As()?.Expression; + } + + public static SimpleNameSyntax GetRightSideNameSyntax(this InvocationExpressionSyntax node) + { + return node.Expression.As()?.Name; + } + + public static ArgumentListSyntax GetArgumentsOfMethod(this InvocationExpressionSyntax node, string methodName) + { + // var d1 = node.DescendantNodesAndSelfOfType(); + // var d2 = d1.Where(x => x.MethodNameShouldBe(methodName)); + // var d3 = d2.FirstOrDefault().ArgumentList; + return node.DescendantNodesAndSelfOfType() + .Where(x => x.MethodNameShouldBe(methodName))?.FirstOrDefault()?.ArgumentList; + } + + public static ArgumentSyntax FirstArgument(this InvocationExpressionSyntax node) + { + return node.ArgumentList.Arguments.FirstOrDefault(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/MethodExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/MethodExtractor.cs new file mode 100644 index 0000000..c4d1905 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/MethodExtractor.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public class MethodExtractor : ISyntaxTreeMemberExtractor +{ + public List Extraxt(SyntaxNode root, SyntaxKind kind) + { + return root.DescendantNodes().OfType() + .Where(mth => mth.Modifiers.Any(m => m.IsKind(kind))) + .SelectMany(method => method.Modifiers.Where(m => m.IsKind(kind))) + .ToList(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/NestedClassExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/NestedClassExtractor.cs new file mode 100644 index 0000000..31d1b50 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/NestedClassExtractor.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public class NestedClassExtractor : ISyntaxTreeMemberExtractor +{ + public List Extraxt(SyntaxNode root, SyntaxKind kind) + { + return root.DescendantNodes().OfType() + .Where(cls => cls.Modifiers.Any(m => m.IsKind(kind))) + .SelectMany(method => method.Modifiers.Where(m => m.IsKind(kind))) + .ToList(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/PropertyExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/PropertyExtractor.cs new file mode 100644 index 0000000..fe66172 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/PropertyExtractor.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public class PropertyExtractor : ISyntaxTreeMemberExtractor +{ + public List Extraxt(SyntaxNode root, SyntaxKind kind) + { + return root.DescendantNodes().OfType() + .Where(prp => prp.Modifiers.Any(m => m.IsKind(kind))) + .SelectMany(method => method.Modifiers.Where(m => m.IsKind(kind))) + .ToList(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/SyntaxNodeExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/SyntaxNodeExtractor.cs new file mode 100644 index 0000000..0d95de8 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/SyntaxNodeExtractor.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public static class SyntaxNodeExtractor +{ + public static IEnumerable DescendantNodesOfType(this SyntaxNode node) where T : class + { + return node.DescendantNodes().OfType(); + } + + public static IEnumerable DescendantNodesAndSelfOfType(this SyntaxNode node) where T : class + { + return node.DescendantNodesAndSelf().OfType(); + } + + public static T FirstDescendantNode(this SyntaxNode node) where T : class + { + return node.DescendantNodes().OfType().FirstOrDefault(); + } + + public static FileLinePositionSpan GetFileLinePosSpan(this SyntaxNode node) + { + return node.SyntaxTree.GetLineSpan(new TextSpan(node.Span.Start, node.Span.Length)); + } + + public static string GetFileName(this SyntaxNode node) + { + return Path.GetFileName(node.SyntaxTree.FilePath); + } + + public static string GetFilePath(this SyntaxNode node) => node.SyntaxTree.FilePath; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/SyntaxTriviaExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/SyntaxTriviaExtractor.cs new file mode 100644 index 0000000..47c8839 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/SyntaxTriviaExtractor.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public static class SyntaxTriviaExtractor +{ + public static string GetFilePath(this SyntaxTrivia trivia) => trivia.SyntaxTree.FilePath; + + public static FileLinePositionSpan GetFileLinePosSpan(this SyntaxTrivia trivia) + { + return trivia.SyntaxTree.GetLineSpan(new TextSpan(trivia.Span.Start, trivia.Span.Length)); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/VariableDeclaratorSyntaxExtractor.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/VariableDeclaratorSyntaxExtractor.cs new file mode 100644 index 0000000..2d48a3f --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeExtractors/VariableDeclaratorSyntaxExtractor.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +public static class VariableDeclaratorSyntaxExtractor +{ + public static FileLinePositionSpan GetFileLinePosSpan(this VariableDeclaratorSyntax node) + { + return node.SyntaxTree.GetLineSpan(new TextSpan(node.Span.Start, node.Span.Length)); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeTypeConverter/SyntaxNodeConverter.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeTypeConverter/SyntaxNodeConverter.cs new file mode 100644 index 0000000..476f0ac --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeTypeConverter/SyntaxNodeConverter.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; + +public static class SyntaxNodeConverter +{ + public static T As(this SyntaxNode node) where T : class + => node as T; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ArgumentListValidator.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ArgumentListValidator.cs new file mode 100644 index 0000000..7bac325 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ArgumentListValidator.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; + +public static class ArgumentListValidator +{ + public static bool ArgumentsCountShouldBe(this ArgumentListSyntax node, int count) + { + return node.Arguments.Count() == count; + } + + public static bool FirstArgumentShouldBe(this ArgumentListSyntax node, string checkArg) + { + return checkArg.Equals(node.Arguments.FirstOrDefault().Expression.ToString()); + } + + public static bool FirstArgumentShouldContains(this ArgumentListSyntax node, string checkArg) + { + return node.Arguments.FirstOrDefault().Expression.ToString().Contains(checkArg); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ClassDeclarationValidator.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ClassDeclarationValidator.cs new file mode 100644 index 0000000..f8c535d --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ClassDeclarationValidator.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; + +public static class ClassDeclarationValidator +{ + public static bool ClassShouldHaveBase(this ClassDeclarationSyntax node) + { + return node.BaseList != null; + } + + public static bool ClassShouldHaveGenericBase(this ClassDeclarationSyntax node) + { + return node.BaseList.Types.Any(x => x.Type.IsKind(SyntaxKind.GenericName)); + } + + public static bool GenericClassShouldInheritFrom(this ClassDeclarationSyntax node, string baseClass) + { + return node.BaseList.Types.Any(x => x.As()? + .Type.As()? + .Identifier.Text == baseClass); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ExpressionStatementValidator.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ExpressionStatementValidator.cs new file mode 100644 index 0000000..11bc350 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/ExpressionStatementValidator.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; + +public static class ExpressionStatementValidator +{ + public static bool MethodNameShouldBe(this ExpressionStatementSyntax node, string checkArg) + { + return checkArg.Equals(node.Expression.As()?.Name.ToString()); + } + + public static bool IdentifierShouldBe(this ExpressionStatementSyntax node, string checkArg) + { + return checkArg.Equals(node.FirstDescendantNode().Expression.ToString()); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/InvocationExpressionValidator.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/InvocationExpressionValidator.cs new file mode 100644 index 0000000..74ae126 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/InvocationExpressionValidator.cs @@ -0,0 +1,45 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeTypeConverter; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; + +public static class InvocationExpressionValidator +{ + public static bool ArgumentsCountShouldBe(this InvocationExpressionSyntax node, int count) + { + return node.ArgumentList.Arguments.Count() == count; + } + + public static bool FirstArgumentShouldBeIn(this InvocationExpressionSyntax node, string[] checkArgs) + { + return checkArgs.Any(x => + x.Equals(node.ArgumentList.Arguments.FirstOrDefault().ToString())); + } + + public static bool FirstArgumentShouldBe(this InvocationExpressionSyntax node, string checkArg) + { + return checkArg.Equals(node.ArgumentList.Arguments.FirstOrDefault().ToString()); + } + + public static bool LastArgumentShouldBe(this InvocationExpressionSyntax node, string checkArg) + { + return checkArg.Equals(node.ArgumentList.Arguments.LastOrDefault().ToString()); + } + + public static bool MethodNameShouldBeIn(this InvocationExpressionSyntax node, string[] checkArgs) + { + return checkArgs.Any(x => + x.Equals(node.Expression.As()?.Name.Identifier.ToString())); + } + + public static bool MethodNameShouldBe(this InvocationExpressionSyntax node, string checkArg) + { + return checkArg.Equals(node.Expression.As()?.Name.Identifier.Text); + } + + public static bool LeftSideShouldBeIdentifier(this InvocationExpressionSyntax node, bool shouldBe = true) + { + return node.Expression.As()?.Expression + is IdentifierNameSyntax == shouldBe; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/MemberAccessExpressionValidator.cs b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/MemberAccessExpressionValidator.cs new file mode 100644 index 0000000..aed55bb --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/SyntaxNodeValidators/MemberAccessExpressionValidator.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeValidators; + +public static class MemberAccessExpressionValidator +{ + public static bool MethodNameShouldBe(this MemberAccessExpressionSyntax node, string checkArg) + { + return checkArg.Equals(node.Name.Identifier.ToString()); + } + + public static bool MethodNameShouldBeIn(this MemberAccessExpressionSyntax node, string[] checkArgs) + { + return checkArgs.Any(x => + x.Equals(node.Name.Identifier.ToString())); + } + + public static bool LeftSideShouldBeIdentifier(this MemberAccessExpressionSyntax node, bool shouldBe = true) + { + return node.Expression + is IdentifierNameSyntax == shouldBe; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/FieldTokenRemover.cs b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/FieldTokenRemover.cs new file mode 100644 index 0000000..6d8963f --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/FieldTokenRemover.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.TokenRemovers; + +public class FieldTokenRemover : CleanupCSharpSyntaxRewriter, IPrivateModiferTokenRemover +{ + public FieldTokenRemover(bool isReportOnlyMode) + : base(isReportOnlyMode, null) + { } + public SyntaxNode Remove(SyntaxNode root) + { + var fields = new FieldExtractor().Extraxt(root, SyntaxKind.PrivateKeyword); + + if (IsReportOnlyMode) + { + foreach (var field in fields) + { + var lineSpan = field.GetFileLinePosSpan(); + + AddReport(new ChangesReport(root) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "private fields --> private can be removed", + Generator = nameof(FieldTokenRemover) + }); + } + } + + // TODO: 1. Fix the issue with touching the namespaces + // TODO: 2. Remove the conditional operator + return fields.Count == 0 ? null : root.RemovePrivateTokens(fields); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/IPrivateModiferTokenRemover.cs b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/IPrivateModiferTokenRemover.cs new file mode 100644 index 0000000..3e9a6ec --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/IPrivateModiferTokenRemover.cs @@ -0,0 +1,8 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.TokenRemovers; + +public interface IPrivateModiferTokenRemover +{ + SyntaxNode Remove(SyntaxNode root); +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/MethodTokenRemover.cs b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/MethodTokenRemover.cs new file mode 100644 index 0000000..8f52355 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/MethodTokenRemover.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.TokenRemovers; + +public class MethodTokenRemover : CleanupCSharpSyntaxRewriter, IPrivateModiferTokenRemover +{ + public MethodTokenRemover(bool isReportOnlyMode) + : base(isReportOnlyMode, null) + { } + public SyntaxNode Remove(SyntaxNode root) + { + var methods = new MethodExtractor().Extraxt(root, SyntaxKind.PrivateKeyword); + + if (IsReportOnlyMode) + { + foreach (var method in methods) + { + var lineSpan = method.GetFileLinePosSpan(); + + AddReport(new ChangesReport(root) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "private method --> private can be removed", + Generator = nameof(MethodTokenRemover) + }); + } + } + + // TODO: 1. Fix the issue with touching the namespaces + // TODO: 2. Remove the conditional operator + return methods.Count == 0 ? null : root.RemovePrivateTokens(methods); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/NestedClassTokenRemover.cs b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/NestedClassTokenRemover.cs new file mode 100644 index 0000000..5435731 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/NestedClassTokenRemover.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.TokenRemovers; + +public class NestedClassTokenRemover : CleanupCSharpSyntaxRewriter, IPrivateModiferTokenRemover +{ + public NestedClassTokenRemover(bool isReportOnlyMode) + : base(isReportOnlyMode, null) + { } + public SyntaxNode Remove(SyntaxNode root) + { + var nestedClasses = new NestedClassExtractor().Extraxt(root, SyntaxKind.PrivateKeyword); + + if (IsReportOnlyMode) + { + foreach (var nestedClass in nestedClasses) + { + var lineSpan = nestedClass.GetFileLinePosSpan(); + + AddReport(new ChangesReport(root) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "private nested class --> private can be removed", + Generator = nameof(NestedClassTokenRemover) + }); + } + } + + // TODO: 1. Fix the issue with touching the namespaces + // TODO: 2. Remove the conditional operator + return nestedClasses.Count == 0 ? null : root.RemovePrivateTokens(nestedClasses); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/PropertyTokenRemover.cs b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/PropertyTokenRemover.cs new file mode 100644 index 0000000..fdf3ad2 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/TokenRemovers/PropertyTokenRemover.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli.Menus.Cleanup.TokenRemovers; + +public class PropertyTokenRemover : CleanupCSharpSyntaxRewriter, IPrivateModiferTokenRemover +{ + public PropertyTokenRemover(bool isReportOnlyMode) + : base(isReportOnlyMode, null) + { } + public SyntaxNode Remove(SyntaxNode root) + { + var properties = new PropertyExtractor().Extraxt(root, SyntaxKind.PrivateKeyword); + + if (IsReportOnlyMode) + { + foreach (var prop in properties) + { + var lineSpan = prop.GetFileLinePosSpan(); + + AddReport(new ChangesReport(root) + { + LineNumber = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + Message = "private property --> private can be removed", + Generator = nameof(PropertyTokenRemover) + }); + } + } + + // TODO: 1. Fix the issue with touching the namespaces + // TODO: 2. Remove the conditional operator + return properties.Count == 0 ? null : root.RemovePrivateTokens(properties); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/ActiveDocument.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/ActiveDocument.cs new file mode 100644 index 0000000..d7da28f --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/ActiveDocument.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils; + +public class ActiveDocument +{ + public static bool IsValid(Document item) + { + if (item == null) return false; + + var path = item.FilePath; + + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + return false; + + return true; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/ChangesReport.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/ChangesReport.cs new file mode 100644 index 0000000..804bf10 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/ChangesReport.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.SyntaxNodeExtractors; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils; + +public class ChangesReport +{ + public ChangesReport(SyntaxNode node) => FileName = node.GetFilePath(); + + public ChangesReport(SyntaxTrivia trivia) => FileName = trivia.GetFilePath(); + public string Message { get; set; } + public long LineNumber { get; set; } + public string FileName { get; private set; } + public string Generator { get; set; } + public long Column { get; set; } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/CleanupCSharpSyntaxRewriter.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/CleanupCSharpSyntaxRewriter.cs new file mode 100644 index 0000000..bfee1d9 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/CleanupCSharpSyntaxRewriter.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis.CSharp; +using TidyCSharp.Cli.Extensions; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils; + +public abstract class CleanupCSharpSyntaxRewriter : CSharpSyntaxRewriter +{ + protected ICleanupOption Options { get; private set; } + protected IEnumerable ChangesReport { get; set; } + protected bool IsReportOnlyMode { get; set; } + + protected bool CheckOption(int? o) => Options.Should(o); + + protected CleanupCSharpSyntaxRewriter(bool isReportOnlyMode, ICleanupOption options) + { + IsReportOnlyMode = isReportOnlyMode; + ChangesReport = new List(); + Options = options; + } + public virtual ChangesReport[] GetReport() => ChangesReport.ToArray(); + + public void AddReport(ChangesReport changesReports) + { + if (changesReports.FileName.HasValue()) + ChangesReport = ChangesReport.Append(changesReports); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/CONSTRenamer.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/CONSTRenamer.cs new file mode 100644 index 0000000..52be748 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/CONSTRenamer.cs @@ -0,0 +1,75 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +internal class ConstRenamer : Renamer +{ + public ConstRenamer(Document document) : base(document) + { + } + + protected override IEnumerable GetItemsToRename(SyntaxNode currentNode) + { + var output = new List(); + + { + var selectedFields = + (currentNode as ClassDeclarationSyntax) + .Members.OfType() + .Where( + x => + x.Modifiers.Any(m => m.IsKind(SyntaxKind.ConstKeyword)) && + IsPrivate(x) + ); + + foreach (var item in selectedFields) + output.AddRange(item.Declaration.Variables); + } + + { + var selectedFields = + (currentNode as ClassDeclarationSyntax) + .DescendantNodes().OfType() + .Where( + x => + x.Modifiers.Any(m => m.IsKind(SyntaxKind.ConstKeyword)) + ); + + foreach (var item in selectedFields) + output.AddRange(item.Declaration.Variables); + } + + return output.Select(x => x.Identifier); + } + + protected override string[] GetNewName(string currentName) + { + const char underline = '_'; + + var newNameBuilder = new StringBuilder(); + var lastCharIsLowwer = false; + + foreach (var c in currentName) + { + if (char.IsUpper(c)) + { + if (lastCharIsLowwer) + { + newNameBuilder.Append(underline); + } + + lastCharIsLowwer = false; + } + else if (c != underline) lastCharIsLowwer = true; + + newNameBuilder.Append(c); + } + + if (string.Compare(currentName, currentName.ToUpper(), false) == 0) return null; + + return new[] { newNameBuilder.ToString().ToUpper() }; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/FieldRenamer.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/FieldRenamer.cs new file mode 100644 index 0000000..7dd5827 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/FieldRenamer.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +internal class FieldRenamer : Renamer +{ + public FieldRenamer(Document document) : base(document) + { + } + + protected override IEnumerable GetItemsToRename(SyntaxNode currentNode) + { + var output = new List(); + + var selectedFields = + (currentNode as ClassDeclarationSyntax) + .Members.OfType() + .Where( + x => + IsPrivate(x) && + x.Modifiers.Any(m => m.IsKind(SyntaxKind.ConstKeyword)) == false + ); + + foreach (var item in selectedFields) + output.AddRange(item.Declaration.Variables); + + return output.Select(x => x.Identifier); + } + + protected override string[] GetNewName(string currentName) + { + if (currentName.StartsWith("_")) + { + currentName = currentName.TrimStart('_'); + + if (char.IsLetter(currentName[0])) + { + return new[] { GetCamelCased(currentName), GetPascalCased(currentName) }; + } + } + + return null; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/Extensions.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/Extensions.cs new file mode 100644 index 0000000..4330a7c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/Extensions.cs @@ -0,0 +1,149 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +public static class Extensions +{ + public static string GetFullNamespace(this ISymbol symbol) + { + if ((symbol.ContainingNamespace == null) || + (string.IsNullOrEmpty(symbol.ContainingNamespace.Name))) + { + return null; + } + + // get the rest of the full namespace string + var restOfResult = symbol.ContainingNamespace.GetFullNamespace(); + + var result = symbol.ContainingNamespace.Name; + + if (restOfResult != null) + // if restOfResult is not null, append it after a period + result = restOfResult + '.' + result; + + return result; + } + + public static string GetFullTypeString(this INamedTypeSymbol type) + { + var result = type.Name; + + if (type.TypeArguments.Count() > 0) + { + result += "<"; + + var isFirstIteration = true; + + foreach (INamedTypeSymbol typeArg in type.TypeArguments) + { + if (isFirstIteration) isFirstIteration = false; + else result += ", "; + + result += typeArg.GetFullTypeString(); + } + + result += ">"; + } + + return result; + } + + public static string ConvertAccessabilityToString(this Accessibility accessability) + { + switch (accessability) + { + case Accessibility.Internal: + return "internal"; + case Accessibility.Private: + return "private"; + case Accessibility.Protected: + return "protected"; + case Accessibility.Public: + return "public"; + case Accessibility.ProtectedAndInternal: + return "protected internal"; + default: + return "private"; + } + } + + public static string GetMethodSignature(this IMethodSymbol methodSymbol) + { + var result = methodSymbol.DeclaredAccessibility.ConvertAccessabilityToString(); + + if (methodSymbol.IsAsync) result += " async"; + + if (methodSymbol.IsAbstract) + result += " abstract"; + + if (methodSymbol.IsVirtual) result += " virtual"; + + if (methodSymbol.IsStatic) result += " static"; + + if (methodSymbol.IsOverride) + { + result += " override"; + } + + if (methodSymbol.ReturnsVoid) + { + result += " void"; + } + else + { + result += " " + (methodSymbol.ReturnType as INamedTypeSymbol).GetFullTypeString(); + } + + result += " " + methodSymbol.Name + "("; + + var isFirstParameter = true; + + foreach (IParameterSymbol parameter in methodSymbol.Parameters) + { + if (isFirstParameter) isFirstParameter = false; + else result += ", "; + + if (parameter.RefKind == RefKind.Out) + { + result += "out "; + } + else if (parameter.RefKind == RefKind.Ref) + { + result += "ref "; + } + + var parameterTypeString = + (parameter.Type as INamedTypeSymbol).GetFullTypeString(); + + result += parameterTypeString; + + result += " " + parameter.Name; + + if (parameter.HasExplicitDefaultValue) + { + result += " = " + parameter.ExplicitDefaultValue.ToString(); + } + } + + result += ")"; + + return result; + } + + public static object GetAttributeConstructorValueByParameterName(this AttributeData attributeData, string argName) + { + // Get the parameter + var parameterSymbol = attributeData.AttributeConstructor + .Parameters + .Where((constructorParam) => constructorParam.Name == argName).FirstOrDefault(); + + // get the index of the parameter + var parameterIdx = attributeData.AttributeConstructor.Parameters.IndexOf(parameterSymbol); + + // get the construct argument corresponding to this parameter + var constructorArg = attributeData.ConstructorArguments[parameterIdx]; + + // return the value passed to the attribute + return constructorArg.Value; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/ISyntaxWrapper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/ISyntaxWrapper.cs new file mode 100644 index 0000000..268037d --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/ISyntaxWrapper.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal interface ISyntaxWrapper + where T : SyntaxNode +{ + /// + /// Gets the wrapped syntax node. + /// + /// + /// The wrapped syntax node. + /// + T SyntaxNode + { + get; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/LightupHelpers.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/LightupHelpers.cs new file mode 100644 index 0000000..9023421 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/LightupHelpers.cs @@ -0,0 +1,265 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal static class LightupHelpers +{ + private static readonly ConcurrentDictionary> SupportedWrappers = new(); + + public static bool SupportsCSharp7 { get; } + = Enum.GetNames(typeof(SyntaxKind)).Contains(nameof(SyntaxKindEx.IsPatternExpression)); + + internal static bool CanWrapNode(SyntaxNode node, Type underlyingType) + { + if (node == null) + { + // The wrappers support a null instance + return true; + } + + if (underlyingType == null) + { + // The current runtime doesn't define the target type of the conversion, so no instance of it can exist + return false; + } + + var wrappedSyntax = SupportedWrappers.GetOrAdd(underlyingType, _ => new ConcurrentDictionary()); + + // Avoid creating the delegate if the value already exists + bool canCast; + + if (!wrappedSyntax.TryGetValue((SyntaxKind)node.RawKind, out canCast)) + { + canCast = wrappedSyntax.GetOrAdd( + (SyntaxKind)node.RawKind, + kind => underlyingType.GetTypeInfo().IsAssignableFrom(node.GetType().GetTypeInfo())); + } + + return canCast; + } + + internal static Func CreateSyntaxPropertyAccessor(Type type, string propertyName) + { + Func fallbackAccessor = + syntax => + { + if (syntax == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + return default(TProperty); + }; + + if (type == null) return fallbackAccessor; + + if (!typeof(TSyntax).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var property = type.GetTypeInfo().GetDeclaredProperty(propertyName); + if (property == null) return fallbackAccessor; + + if (!typeof(TProperty).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var syntaxParameter = Expression.Parameter(typeof(TSyntax), "syntax"); + + var instance = + type.GetTypeInfo().IsAssignableFrom(typeof(TSyntax).GetTypeInfo()) + ? (Expression)syntaxParameter + : Expression.Convert(syntaxParameter, type); + + var expression = + Expression.Lambda>( + Expression.Call(instance, property.GetMethod), + syntaxParameter); + + return expression.Compile(); + } + + internal static Func> CreateSeparatedSyntaxListPropertyAccessor(Type type, string propertyName) + { + Func> fallbackAccessor = + syntax => + { + if (syntax == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + return SeparatedSyntaxListWrapper.UnsupportedEmpty; + }; + + if (type == null) return fallbackAccessor; + + if (!typeof(TSyntax).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var property = type.GetTypeInfo().GetDeclaredProperty(propertyName); + if (property == null) return fallbackAccessor; + + if (property.PropertyType.GetGenericTypeDefinition() != typeof(SeparatedSyntaxList<>)) + { + throw new InvalidOperationException(); + } + + var propertySyntaxType = property.PropertyType.GenericTypeArguments[0]; + + var syntaxParameter = Expression.Parameter(typeof(TSyntax), "syntax"); + + var instance = + type.GetTypeInfo().IsAssignableFrom(typeof(TSyntax).GetTypeInfo()) + ? (Expression)syntaxParameter + : Expression.Convert(syntaxParameter, type); + + Expression propertyAccess = Expression.Call(instance, property.GetMethod); + + var unboundWrapperType = typeof(SeparatedSyntaxListWrapper<>.AutoWrapSeparatedSyntaxList<>); + var boundWrapperType = unboundWrapperType.MakeGenericType(typeof(TProperty), propertySyntaxType); + var constructorInfo = boundWrapperType.GetTypeInfo().DeclaredConstructors.Single(); + + var expression = + Expression.Lambda>>( + Expression.New(constructorInfo, propertyAccess), + syntaxParameter); + + return expression.Compile(); + } + + internal static Func CreateSyntaxWithPropertyAccessor(Type type, string propertyName) + { + Func fallbackAccessor = + (syntax, newValue) => + { + if (syntax == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + if (Equals(newValue, default(TProperty))) + { + return syntax; + } + + throw new NotSupportedException(); + }; + + if (type == null) return fallbackAccessor; + + if (!typeof(TSyntax).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var property = type.GetTypeInfo().GetDeclaredProperty(propertyName); + if (property == null) return fallbackAccessor; + + if (!typeof(TProperty).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var methodInfo = type.GetTypeInfo().GetDeclaredMethods("With" + propertyName) + .Single(m => !m.IsStatic && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.Equals(property.PropertyType)); + + var syntaxParameter = Expression.Parameter(typeof(TSyntax), "syntax"); + var valueParameter = Expression.Parameter(typeof(TProperty), methodInfo.GetParameters()[0].Name); + + var instance = + type.GetTypeInfo().IsAssignableFrom(typeof(TSyntax).GetTypeInfo()) + ? (Expression)syntaxParameter + : Expression.Convert(syntaxParameter, type); + + var value = + property.PropertyType.GetTypeInfo().IsAssignableFrom(typeof(TProperty).GetTypeInfo()) + ? (Expression)valueParameter + : Expression.Convert(valueParameter, property.PropertyType); + + var expression = + Expression.Lambda>( + Expression.Call(instance, methodInfo, value), + syntaxParameter, + valueParameter); + + return expression.Compile(); + } + + internal static Func, TSyntax> CreateSeparatedSyntaxListWithPropertyAccessor(Type type, string propertyName) + { + Func, TSyntax> fallbackAccessor = + (syntax, newValue) => + { + if (syntax == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + if (ReferenceEquals(newValue, null)) + { + return syntax; + } + + throw new NotSupportedException(); + }; + + if (type == null) return fallbackAccessor; + + if (!typeof(TSyntax).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var property = type.GetTypeInfo().GetDeclaredProperty(propertyName); + if (property == null) return fallbackAccessor; + + if (property.PropertyType.GetGenericTypeDefinition() != typeof(SeparatedSyntaxList<>)) + { + throw new InvalidOperationException(); + } + + var propertySyntaxType = property.PropertyType.GenericTypeArguments[0]; + + var methodInfo = type.GetTypeInfo().GetDeclaredMethods("With" + propertyName) + .Single(m => !m.IsStatic && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.Equals(property.PropertyType)); + + var syntaxParameter = Expression.Parameter(typeof(TSyntax), "syntax"); + var valueParameter = Expression.Parameter(typeof(SeparatedSyntaxListWrapper), methodInfo.GetParameters()[0].Name); + + var instance = + type.GetTypeInfo().IsAssignableFrom(typeof(TSyntax).GetTypeInfo()) + ? (Expression)syntaxParameter + : Expression.Convert(syntaxParameter, type); + + var underlyingListProperty = typeof(SeparatedSyntaxListWrapper).GetTypeInfo().GetDeclaredProperty(nameof(SeparatedSyntaxListWrapper.UnderlyingList)); + + Expression value = Expression.Convert( + Expression.Call(valueParameter, underlyingListProperty.GetMethod), + property.PropertyType); + + var expression = + Expression.Lambda, TSyntax>>( + Expression.Call(instance, methodInfo, value), + syntaxParameter, + valueParameter); + + return expression.Compile(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/LocalFunctionStatementSyntaxWrapper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/LocalFunctionStatementSyntaxWrapper.cs new file mode 100644 index 0000000..96e2138 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/LocalFunctionStatementSyntaxWrapper.cs @@ -0,0 +1,180 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal struct LocalFunctionStatementSyntaxWrapper : ISyntaxWrapper +{ + private const string LocalFunctionStatementSyntaxTypeName = "Microsoft.CodeAnalysis.CSharp.Syntax.LocalFunctionStatementSyntax"; + private static readonly Type LocalFunctionStatementSyntaxType; + + private static readonly Func ModifiersAccessor; + private static readonly Func ReturnTypeAccessor; + private static readonly Func IdentifierAccessor; + private static readonly Func TypeParameterListAccessor; + private static readonly Func ParameterListAccessor; + private static readonly Func> ConstraintClausesAccessor; + private static readonly Func BodyAccessor; + private static readonly Func ExpressionBodyAccessor; + private static readonly Func SemicolonTokenAccessor; + private static readonly Func WithModifiersAccessor; + private static readonly Func WithReturnTypeAccessor; + private static readonly Func WithIdentifierAccessor; + private static readonly Func WithTypeParameterListAccessor; + private static readonly Func WithParameterListAccessor; + private static readonly Func, StatementSyntax> WithConstraintClausesAccessor; + private static readonly Func WithBodyAccessor; + private static readonly Func WithExpressionBodyAccessor; + private static readonly Func WithSemicolonTokenAccessor; + + private readonly StatementSyntax _node; + + static LocalFunctionStatementSyntaxWrapper() + { + LocalFunctionStatementSyntaxType = typeof(CSharpSyntaxNode).GetTypeInfo().Assembly.GetType(LocalFunctionStatementSyntaxTypeName); + ModifiersAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(Modifiers)); + ReturnTypeAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(ReturnType)); + IdentifierAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(Identifier)); + TypeParameterListAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(TypeParameterList)); + ParameterListAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(ParameterList)); + ConstraintClausesAccessor = LightupHelpers.CreateSyntaxPropertyAccessor>(LocalFunctionStatementSyntaxType, nameof(ConstraintClauses)); + BodyAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(Body)); + ExpressionBodyAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(ExpressionBody)); + SemicolonTokenAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(SemicolonToken)); + WithModifiersAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(Modifiers)); + WithReturnTypeAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(ReturnType)); + WithIdentifierAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(Identifier)); + WithTypeParameterListAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(TypeParameterList)); + WithParameterListAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(ParameterList)); + WithConstraintClausesAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor>(LocalFunctionStatementSyntaxType, nameof(ConstraintClauses)); + WithBodyAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(Body)); + WithExpressionBodyAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(ExpressionBody)); + WithSemicolonTokenAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(LocalFunctionStatementSyntaxType, nameof(SemicolonToken)); + } + private LocalFunctionStatementSyntaxWrapper(StatementSyntax node) => _node = node; + + public StatementSyntax SyntaxNode => _node; + + public SyntaxTokenList Modifiers => ModifiersAccessor(SyntaxNode); + + public TypeSyntax ReturnType => ReturnTypeAccessor(SyntaxNode); + + public SyntaxToken Identifier => IdentifierAccessor(SyntaxNode); + + public TypeParameterListSyntax TypeParameterList => TypeParameterListAccessor(SyntaxNode); + + public ParameterListSyntax ParameterList => ParameterListAccessor(SyntaxNode); + + public SyntaxList ConstraintClauses + { + get + { + return ConstraintClausesAccessor(SyntaxNode); + } + } + + public BlockSyntax Body => BodyAccessor(SyntaxNode); + + public ArrowExpressionClauseSyntax ExpressionBody => ExpressionBodyAccessor(SyntaxNode); + + public SyntaxToken SemicolonToken => SemicolonTokenAccessor(SyntaxNode); + + public static explicit operator LocalFunctionStatementSyntaxWrapper(SyntaxNode node) + { + if (node == null) + { + return default(LocalFunctionStatementSyntaxWrapper); + } + + if (!IsInstance(node)) + { + throw new InvalidCastException($"Cannot cast '{node.GetType().FullName}' to '{LocalFunctionStatementSyntaxTypeName}'"); + } + + return new LocalFunctionStatementSyntaxWrapper((StatementSyntax)node); + } + + public static implicit operator StatementSyntax(LocalFunctionStatementSyntaxWrapper wrapper) + { + return wrapper._node; + } + + public static bool IsInstance(SyntaxNode node) + { + return node != null && LightupHelpers.CanWrapNode(node, LocalFunctionStatementSyntaxType); + } + + public LocalFunctionStatementSyntaxWrapper WithModifiers(SyntaxTokenList modifiers) + { + return new LocalFunctionStatementSyntaxWrapper(WithModifiersAccessor(SyntaxNode, modifiers)); + } + + public LocalFunctionStatementSyntaxWrapper WithReturnType(TypeSyntax returnType) + { + return new LocalFunctionStatementSyntaxWrapper(WithReturnTypeAccessor(SyntaxNode, returnType)); + } + + public LocalFunctionStatementSyntaxWrapper WithIdentifier(SyntaxToken identifier) + { + return new LocalFunctionStatementSyntaxWrapper(WithIdentifierAccessor(SyntaxNode, identifier)); + } + + public LocalFunctionStatementSyntaxWrapper WithTypeParameterList(TypeParameterListSyntax typeParameterList) + { + return new LocalFunctionStatementSyntaxWrapper(WithTypeParameterListAccessor(SyntaxNode, typeParameterList)); + } + + public LocalFunctionStatementSyntaxWrapper WithParameterList(ParameterListSyntax parameterList) + { + return new LocalFunctionStatementSyntaxWrapper(WithParameterListAccessor(SyntaxNode, parameterList)); + } + + public LocalFunctionStatementSyntaxWrapper WithConstraintClauses(SyntaxList constraintClauses) + { + return new LocalFunctionStatementSyntaxWrapper(WithConstraintClausesAccessor(SyntaxNode, constraintClauses)); + } + + public LocalFunctionStatementSyntaxWrapper WithBody(BlockSyntax body) + { + return new LocalFunctionStatementSyntaxWrapper(WithBodyAccessor(SyntaxNode, body)); + } + + public LocalFunctionStatementSyntaxWrapper WithExpressionBody(ArrowExpressionClauseSyntax expressionBody) + { + return new LocalFunctionStatementSyntaxWrapper(WithExpressionBodyAccessor(SyntaxNode, expressionBody)); + } + + public LocalFunctionStatementSyntaxWrapper WithSemicolonToken(SyntaxToken semicolonToken) + { + return new LocalFunctionStatementSyntaxWrapper(WithSemicolonTokenAccessor(SyntaxNode, semicolonToken)); + } + + public LocalFunctionStatementSyntaxWrapper AddModifiers(params SyntaxToken[] items) + { + return WithModifiers(Modifiers.AddRange(items)); + } + + public LocalFunctionStatementSyntaxWrapper AddTypeParameterListParameters(params TypeParameterSyntax[] items) + { + var typeParameterList = TypeParameterList ?? SyntaxFactory.TypeParameterList(); + return WithTypeParameterList(typeParameterList.WithParameters(typeParameterList.Parameters.AddRange(items))); + } + + public LocalFunctionStatementSyntaxWrapper AddParameterListParameters(params ParameterSyntax[] items) + { + return WithParameterList(ParameterList.WithParameters(ParameterList.Parameters.AddRange(items))); + } + + public LocalFunctionStatementSyntaxWrapper AddConstraintClauses(params TypeParameterConstraintClauseSyntax[] items) + { + return WithConstraintClauses(ConstraintClauses.AddRange(items)); + } + + public LocalFunctionStatementSyntaxWrapper AddBodyStatements(params StatementSyntax[] items) + { + var body = Body ?? SyntaxFactory.Block(); + return WithBody(body.WithStatements(body.Statements.AddRange(items))); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/RenameHelper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/RenameHelper.cs new file mode 100644 index 0000000..cf70ae2 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/RenameHelper.cs @@ -0,0 +1,272 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal static partial class RenameHelper +{ + public static async Task RenameSymbolAsync(Document document, SyntaxNode root, SyntaxToken declarationToken, string newName, CancellationToken cancellationToken = default(CancellationToken)) + { + var annotatedRoot = root.ReplaceToken(declarationToken, declarationToken.WithAdditionalAnnotations(RenameAnnotation.Create())); + var annotatedSolution = document.Project.Solution.WithDocumentSyntaxRoot(document.Id, annotatedRoot); + var annotatedDocument = annotatedSolution.GetDocument(document.Id); + + annotatedRoot = await annotatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var annotatedToken = annotatedRoot.FindToken(declarationToken.SpanStart); + + var semanticModel = await annotatedDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var symbol = semanticModel.GetDeclaredSymbol(annotatedToken.Parent, cancellationToken); + + var newSolution = await Microsoft.CodeAnalysis.Rename.Renamer.RenameSymbolAsync(annotatedSolution, symbol, newName, null, cancellationToken).ConfigureAwait(false); + + // TODO: return annotatedSolution instead of newSolution if newSolution contains any new errors (for any project) + return newSolution; + } + + public static async Task IsValidNewMemberNameAsync(SemanticModel semanticModel, ISymbol symbol, string name, CancellationToken cancellationToken = default(CancellationToken)) + { + if (symbol.Kind == SymbolKind.NamedType) + { + var typeKind = ((INamedTypeSymbol)symbol).TypeKind; + + // If the symbol is a class or struct, the name can't be the same as any of its members. + if (typeKind == TypeKind.Class || typeKind == TypeKind.Struct) + { + var members = (symbol as INamedTypeSymbol)?.GetMembers(name); + + if (members.HasValue && !members.Value.IsDefaultOrEmpty) + { + return false; + } + } + } + + var containingSymbol = symbol.ContainingSymbol; + + if (symbol.Kind == SymbolKind.TypeParameter) + { + // If the symbol is a type parameter, the name can't be the same as any type parameters of the containing type. + var parentSymbol = containingSymbol?.ContainingSymbol as INamedTypeSymbol; + + if (parentSymbol != null + && parentSymbol.TypeParameters.Any(t => t.Name == name)) + { + return false; + } + + // Move up one level for the next validation step. + containingSymbol = containingSymbol?.ContainingSymbol; + } + + var containingNamespaceOrTypeSymbol = containingSymbol as INamespaceOrTypeSymbol; + + if (containingNamespaceOrTypeSymbol != null) + { + if (containingNamespaceOrTypeSymbol.Kind == SymbolKind.Namespace) + { + // Make sure to use the compilation namespace so interfaces in referenced assemblies are considered + containingNamespaceOrTypeSymbol = semanticModel.Compilation.GetCompilationNamespace((INamespaceSymbol)containingNamespaceOrTypeSymbol); + } + else if (containingNamespaceOrTypeSymbol.Kind == SymbolKind.NamedType) + { + var typeKind = ((INamedTypeSymbol)containingNamespaceOrTypeSymbol).TypeKind; + + // If the containing type is a class or struct, the name can't be the same as the name of the containing + // type. + if ((typeKind == TypeKind.Class || typeKind == TypeKind.Struct) + && containingNamespaceOrTypeSymbol.Name == name) + { + return false; + } + } + + // The name can't be the same as the name of an other member of the same type. At this point no special + // consideration is given to overloaded methods. + return containingNamespaceOrTypeSymbol.GetMembers(name).IsDefaultOrEmpty; + } + else if (containingSymbol.Kind == SymbolKind.Method) + { + var methodSymbol = (IMethodSymbol)containingSymbol; + + if (methodSymbol.Parameters.Any(i => i.Name == name) + || methodSymbol.TypeParameters.Any(i => i.Name == name)) + { + return false; + } + + var outermostMethod = methodSymbol; + + while (outermostMethod.ContainingSymbol.Kind == SymbolKind.Method) + { + outermostMethod = (IMethodSymbol)outermostMethod.ContainingSymbol; + + if (outermostMethod.Parameters.Any(i => i.Name == name) + || outermostMethod.TypeParameters.Any(i => i.Name == name)) + { + return false; + } + } + + foreach (var syntaxReference in outermostMethod.DeclaringSyntaxReferences) + { + var syntaxNode = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + var localNameFinder = new LocalNameFinder(name); + localNameFinder.Visit(syntaxNode); + if (localNameFinder.Found) return false; + } + + return true; + } + else + { + return true; + } + } + + //public static SyntaxNode GetParentDeclaration(SyntaxToken token) + //{ + // var parent = token.Parent; + + // while (parent != null) + // { + // switch ((SyntaxKind)parent.RawKind) + // { + // case SyntaxKind.VariableDeclarator: + // case SyntaxKind.Parameter: + // case SyntaxKind.TypeParameter: + // case SyntaxKind.CatchDeclaration: + // case SyntaxKind.ExternAliasDirective: + // case SyntaxKind.QueryContinuation: + // case SyntaxKind.FromClause: + // case SyntaxKind.LetClause: + // case SyntaxKind.JoinClause: + // case SyntaxKind.JoinIntoClause: + // case SyntaxKind.ForEachStatement: + // case SyntaxKind.UsingDirective: + // case SyntaxKind.LabeledStatement: + // case SyntaxKind.AnonymousObjectMemberDeclarator: + // case SyntaxKindEx.LocalFunctionStatement: + // case SyntaxKindEx.SingleVariableDesignation: + // return parent; + + // default: + // var declarationParent = parent as MemberDeclarationSyntax; + // if (declarationParent != null) + // { + // return declarationParent; + // } + + // break; + // } + + // parent = parent.Parent; + // } + + // return null; + //} + + private class LocalNameFinder : CSharpSyntaxWalker + { + private readonly string _name; + public LocalNameFinder(string name) => _name = name; + + public bool Found + { + get; + private set; + } + + public override void Visit(SyntaxNode node) + { + switch ((SyntaxKind)node.RawKind) + { + case SyntaxKindEx.LocalFunctionStatement: + Found |= ((LocalFunctionStatementSyntaxWrapper)node).Identifier.ValueText == _name; + break; + + case SyntaxKindEx.SingleVariableDesignation: + Found |= ((SingleVariableDesignationSyntaxWrapper)node).Identifier.ValueText == _name; + break; + + default: + break; + } + + base.Visit(node); + } + + public override void VisitVariableDeclarator(VariableDeclaratorSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitVariableDeclarator(node); + } + + public override void VisitParameter(ParameterSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitParameter(node); + } + + public override void VisitTypeParameter(TypeParameterSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitTypeParameter(node); + } + + public override void VisitCatchDeclaration(CatchDeclarationSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitCatchDeclaration(node); + } + + public override void VisitQueryContinuation(QueryContinuationSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitQueryContinuation(node); + } + + public override void VisitFromClause(FromClauseSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitFromClause(node); + } + + public override void VisitLetClause(LetClauseSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitLetClause(node); + } + + public override void VisitJoinClause(JoinClauseSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitJoinClause(node); + } + + public override void VisitJoinIntoClause(JoinIntoClauseSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitJoinIntoClause(node); + } + + public override void VisitForEachStatement(ForEachStatementSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitForEachStatement(node); + } + + public override void VisitLabeledStatement(LabeledStatementSyntax node) + { + Found |= node.Identifier.ValueText == _name; + base.VisitLabeledStatement(node); + } + + public override void VisitAnonymousObjectMemberDeclarator(AnonymousObjectMemberDeclaratorSyntax node) + { + Found |= node.NameEquals?.Name?.Identifier.ValueText == _name; + base.VisitAnonymousObjectMemberDeclarator(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SeparatedSyntaxListWrapper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SeparatedSyntaxListWrapper.cs new file mode 100644 index 0000000..8a19e88 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SeparatedSyntaxListWrapper.cs @@ -0,0 +1,381 @@ +using System.Collections; +using System.ComponentModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal abstract class SeparatedSyntaxListWrapper : IEquatable>, IReadOnlyList +{ + private static readonly SyntaxWrapper SyntaxWrapper = SyntaxWrapper.Default; + + public static SeparatedSyntaxListWrapper UnsupportedEmpty { get; } = + new UnsupportedSyntaxList(); + + public abstract int Count + { + get; + } + + public abstract TextSpan FullSpan + { + get; + } + + public abstract int SeparatorCount + { + get; + } + + public abstract TextSpan Span + { + get; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract object UnderlyingList + { + get; + } + + public abstract TNode this[int index] + { + get; + } + + public static bool operator ==(SeparatedSyntaxListWrapper left, SeparatedSyntaxListWrapper right) + { + throw new NotImplementedException(); + } + + public static bool operator !=(SeparatedSyntaxListWrapper left, SeparatedSyntaxListWrapper right) + { + throw new NotImplementedException(); + } + + // Summary: + // Creates a new list with the specified node added to the end. + // + // Parameters: + // node: + // The node to add. + public SeparatedSyntaxListWrapper Add(TNode node) + => Insert(Count, node); + + // Summary: + // Creates a new list with the specified nodes added to the end. + // + // Parameters: + // nodes: + // The nodes to add. + public SeparatedSyntaxListWrapper AddRange(IEnumerable nodes) + => InsertRange(Count, nodes); + + public abstract bool Any(); + + public abstract bool Contains(TNode node); + + public bool Equals(SeparatedSyntaxListWrapper other) + { + throw new NotImplementedException(); + } + + public override bool Equals(object obj) + { + throw new NotImplementedException(); + } + + public abstract TNode First(); + + public abstract TNode FirstOrDefault(); + + public Enumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + public override abstract int GetHashCode(); + + public abstract SyntaxToken GetSeparator(int index); + + public abstract IEnumerable GetSeparators(); + + public abstract SyntaxNodeOrTokenList GetWithSeparators(); + + public abstract int IndexOf(Func predicate); + + public abstract int IndexOf(TNode node); + + public abstract SeparatedSyntaxListWrapper Insert(int index, TNode node); + + public abstract SeparatedSyntaxListWrapper InsertRange(int index, IEnumerable nodes); + + public abstract TNode Last(); + + public abstract int LastIndexOf(Func predicate); + + public abstract int LastIndexOf(TNode node); + + public abstract TNode LastOrDefault(); + + public abstract SeparatedSyntaxListWrapper Remove(TNode node); + + public abstract SeparatedSyntaxListWrapper RemoveAt(int index); + + public abstract SeparatedSyntaxListWrapper Replace(TNode nodeInList, TNode newNode); + + public abstract SeparatedSyntaxListWrapper ReplaceRange(TNode nodeInList, IEnumerable newNodes); + + public abstract SeparatedSyntaxListWrapper ReplaceSeparator(SyntaxToken separatorToken, SyntaxToken newSeparator); + + public abstract string ToFullString(); + + public override abstract string ToString(); + + public struct Enumerator + { + + public TNode Current + { + get + { + throw new NotImplementedException(); + } + } + + public override bool Equals(object obj) + { + throw new NotImplementedException(); + } + + public override int GetHashCode() + { + throw new NotImplementedException(); + } + + public bool MoveNext() + { + throw new NotImplementedException(); + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + + internal sealed class AutoWrapSeparatedSyntaxList : SeparatedSyntaxListWrapper + where TSyntax : SyntaxNode + { + private readonly SeparatedSyntaxList _syntaxList; + + public AutoWrapSeparatedSyntaxList(SeparatedSyntaxList syntaxList) + { + _syntaxList = syntaxList; + } + + public override int Count + => _syntaxList.Count; + + public override TextSpan FullSpan + => _syntaxList.FullSpan; + + public override int SeparatorCount + => _syntaxList.SeparatorCount; + + public override TextSpan Span + => _syntaxList.Span; + + public override object UnderlyingList + => _syntaxList; + + public override TNode this[int index] + => SyntaxWrapper.Wrap(_syntaxList[index]); + + public override bool Any() + => _syntaxList.Any(); + + public override bool Contains(TNode node) + => _syntaxList.Contains(SyntaxWrapper.Unwrap(node)); + + public override TNode First() + => SyntaxWrapper.Wrap(_syntaxList.FirstOrDefault()); + + public override TNode FirstOrDefault() + => SyntaxWrapper.Wrap(_syntaxList.FirstOrDefault()); + + public override int GetHashCode() + => _syntaxList.GetHashCode(); + + public override SyntaxToken GetSeparator(int index) + => _syntaxList.GetSeparator(index); + + public override IEnumerable GetSeparators() + => _syntaxList.GetSeparators(); + + public override SyntaxNodeOrTokenList GetWithSeparators() + => _syntaxList.GetWithSeparators(); + + public override int IndexOf(TNode node) + => _syntaxList.IndexOf((TSyntax)SyntaxWrapper.Unwrap(node)); + + public override int IndexOf(Func predicate) + => _syntaxList.IndexOf(node => predicate(SyntaxWrapper.Wrap(node))); + + public override SeparatedSyntaxListWrapper Insert(int index, TNode node) + => new AutoWrapSeparatedSyntaxList(_syntaxList.Insert(index, (TSyntax)SyntaxWrapper.Unwrap(node))); + + public override SeparatedSyntaxListWrapper InsertRange(int index, IEnumerable nodes) + => new AutoWrapSeparatedSyntaxList(_syntaxList.InsertRange(index, nodes.Select(node => (TSyntax)SyntaxWrapper.Unwrap(node)))); + + public override TNode Last() + => SyntaxWrapper.Wrap(_syntaxList.Last()); + + public override int LastIndexOf(TNode node) + => _syntaxList.LastIndexOf((TSyntax)SyntaxWrapper.Unwrap(node)); + + public override int LastIndexOf(Func predicate) + => _syntaxList.LastIndexOf(node => predicate(SyntaxWrapper.Wrap(node))); + + public override TNode LastOrDefault() + => SyntaxWrapper.Wrap(_syntaxList.LastOrDefault()); + + public override SeparatedSyntaxListWrapper Remove(TNode node) + => new AutoWrapSeparatedSyntaxList(_syntaxList.Remove((TSyntax)SyntaxWrapper.Unwrap(node))); + + public override SeparatedSyntaxListWrapper RemoveAt(int index) + => new AutoWrapSeparatedSyntaxList(_syntaxList.RemoveAt(index)); + + public override SeparatedSyntaxListWrapper Replace(TNode nodeInList, TNode newNode) + => new AutoWrapSeparatedSyntaxList(_syntaxList.Replace((TSyntax)SyntaxWrapper.Unwrap(nodeInList), (TSyntax)SyntaxWrapper.Unwrap(newNode))); + + public override SeparatedSyntaxListWrapper ReplaceRange(TNode nodeInList, IEnumerable newNodes) + => new AutoWrapSeparatedSyntaxList(_syntaxList.ReplaceRange((TSyntax)SyntaxWrapper.Unwrap(nodeInList), newNodes.Select(node => (TSyntax)SyntaxWrapper.Unwrap(node)))); + + public override SeparatedSyntaxListWrapper ReplaceSeparator(SyntaxToken separatorToken, SyntaxToken newSeparator) + => new AutoWrapSeparatedSyntaxList(_syntaxList.ReplaceSeparator(separatorToken, newSeparator)); + + public override string ToFullString() + => _syntaxList.ToFullString(); + + public override string ToString() + => _syntaxList.ToString(); + } + + private sealed class UnsupportedSyntaxList : SeparatedSyntaxListWrapper + { + private static readonly SeparatedSyntaxList SyntaxList = default(SeparatedSyntaxList); + + public UnsupportedSyntaxList() + { + } + + public override int Count + => 0; + + public override TextSpan FullSpan + => SyntaxList.FullSpan; + + public override int SeparatorCount + => 0; + + public override TextSpan Span + => SyntaxList.Span; + + public override object UnderlyingList + => null; + + public override TNode this[int index] + => SyntaxWrapper.Wrap(SyntaxList[index]); + + public override bool Any() + => false; + + public override bool Contains(TNode node) + => false; + + public override TNode First() + => SyntaxWrapper.Wrap(SyntaxList.FirstOrDefault()); + + public override TNode FirstOrDefault() + => SyntaxWrapper.Wrap(default(SyntaxNode)); + + public override int GetHashCode() + => SyntaxList.GetHashCode(); + + public override SyntaxToken GetSeparator(int index) + => SyntaxList.GetSeparator(index); + + public override IEnumerable GetSeparators() + => SyntaxList.GetSeparators(); + + public override SyntaxNodeOrTokenList GetWithSeparators() + => SyntaxList.GetWithSeparators(); + + public override int IndexOf(TNode node) + => SyntaxList.IndexOf(SyntaxWrapper.Unwrap(node)); + + public override int IndexOf(Func predicate) + => SyntaxList.IndexOf(node => predicate(SyntaxWrapper.Wrap(node))); + + public override SeparatedSyntaxListWrapper Insert(int index, TNode node) + { + throw new NotSupportedException(); + } + + public override SeparatedSyntaxListWrapper InsertRange(int index, IEnumerable nodes) + { + throw new NotSupportedException(); + } + + public override TNode Last() + => SyntaxWrapper.Wrap(SyntaxList.Last()); + + public override int LastIndexOf(TNode node) + => SyntaxList.LastIndexOf(SyntaxWrapper.Unwrap(node)); + + public override int LastIndexOf(Func predicate) + => SyntaxList.LastIndexOf(node => predicate(SyntaxWrapper.Wrap(node))); + + public override TNode LastOrDefault() + => SyntaxWrapper.Wrap(default(SyntaxNode)); + + public override SeparatedSyntaxListWrapper Remove(TNode node) + { + throw new NotSupportedException(); + } + + public override SeparatedSyntaxListWrapper RemoveAt(int index) + { + throw new NotSupportedException(); + } + + public override SeparatedSyntaxListWrapper Replace(TNode nodeInList, TNode newNode) + { + throw new NotSupportedException(); + } + + public override SeparatedSyntaxListWrapper ReplaceRange(TNode nodeInList, IEnumerable newNodes) + { + throw new NotSupportedException(); + } + + public override SeparatedSyntaxListWrapper ReplaceSeparator(SyntaxToken separatorToken, SyntaxToken newSeparator) + { + throw new NotSupportedException(); + } + + public override string ToFullString() + => SyntaxList.ToFullString(); + + public override string ToString() + => SyntaxList.ToString(); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SingleVariableDesignationSyntaxWrapper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SingleVariableDesignationSyntaxWrapper.cs new file mode 100644 index 0000000..571d85c --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SingleVariableDesignationSyntaxWrapper.cs @@ -0,0 +1,70 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +using static RenameHelper; + +internal struct SingleVariableDesignationSyntaxWrapper : ISyntaxWrapper +{ + private const string SingleVariableDesignationSyntaxTypeName = "Microsoft.CodeAnalysis.CSharp.Syntax.SingleVariableDesignationSyntax"; + private static readonly Type SingleVariableDesignationSyntaxType; + + private static readonly Func IdentifierAccessor; + private static readonly Func WithIdentifierAccessor; + + private readonly CSharpSyntaxNode _node; + + static SingleVariableDesignationSyntaxWrapper() + { + SingleVariableDesignationSyntaxType = typeof(CSharpSyntaxNode).GetTypeInfo().Assembly.GetType(SingleVariableDesignationSyntaxTypeName); + IdentifierAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(SingleVariableDesignationSyntaxType, nameof(Identifier)); + WithIdentifierAccessor = LightupHelpers.CreateSyntaxWithPropertyAccessor(SingleVariableDesignationSyntaxType, nameof(Identifier)); + } + private SingleVariableDesignationSyntaxWrapper(CSharpSyntaxNode node) => _node = node; + + public CSharpSyntaxNode SyntaxNode => _node; + + public SyntaxToken Identifier => IdentifierAccessor(SyntaxNode); + + public static explicit operator SingleVariableDesignationSyntaxWrapper(VariableDesignationSyntaxWrapper node) + { + return (SingleVariableDesignationSyntaxWrapper)node.SyntaxNode; + } + + public static explicit operator SingleVariableDesignationSyntaxWrapper(SyntaxNode node) + { + if (node == null) + { + return default(SingleVariableDesignationSyntaxWrapper); + } + + if (!IsInstance(node)) + { + throw new InvalidCastException($"Cannot cast '{node.GetType().FullName}' to '{SingleVariableDesignationSyntaxTypeName}'"); + } + + return new SingleVariableDesignationSyntaxWrapper((CSharpSyntaxNode)node); + } + + public static implicit operator VariableDesignationSyntaxWrapper(SingleVariableDesignationSyntaxWrapper wrapper) + { + return VariableDesignationSyntaxWrapper.FromUpcast(wrapper._node); + } + + public static implicit operator CSharpSyntaxNode(SingleVariableDesignationSyntaxWrapper wrapper) + { + return wrapper._node; + } + + public static bool IsInstance(SyntaxNode node) + { + return node != null && LightupHelpers.CanWrapNode(node, SingleVariableDesignationSyntaxType); + } + + public SingleVariableDesignationSyntaxWrapper WithIdentifier(SyntaxToken identifier) + { + return new SingleVariableDesignationSyntaxWrapper(WithIdentifierAccessor(SyntaxNode, identifier)); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SyntaxKindEx.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SyntaxKindEx.cs new file mode 100644 index 0000000..4cc808e --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SyntaxKindEx.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis.CSharp; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal static class SyntaxKindEx +{ + public const SyntaxKind UnderscoreToken = (SyntaxKind)8491; + public const SyntaxKind IsPatternExpression = (SyntaxKind)8657; + public const SyntaxKind DefaultLiteralExpression = (SyntaxKind)8755; + public const SyntaxKind LocalFunctionStatement = (SyntaxKind)8830; + public const SyntaxKind TupleType = (SyntaxKind)8924; + public const SyntaxKind TupleElement = (SyntaxKind)8925; + public const SyntaxKind TupleExpression = (SyntaxKind)8926; + public const SyntaxKind SingleVariableDesignation = (SyntaxKind)8927; + public const SyntaxKind ParenthesizedVariableDesignation = (SyntaxKind)8928; + public const SyntaxKind ForEachVariableStatement = (SyntaxKind)8929; + public const SyntaxKind DeclarationPattern = (SyntaxKind)9000; + public const SyntaxKind ConstantPattern = (SyntaxKind)9002; + public const SyntaxKind CasePatternSwitchLabel = (SyntaxKind)9009; + public const SyntaxKind WhenClause = (SyntaxKind)9013; + public const SyntaxKind DiscardDesignation = (SyntaxKind)9014; + public const SyntaxKind DeclarationExpression = (SyntaxKind)9040; + public const SyntaxKind RefExpression = (SyntaxKind)9050; + public const SyntaxKind RefType = (SyntaxKind)9051; + public const SyntaxKind ThrowExpression = (SyntaxKind)9052; +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SyntaxWrapper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SyntaxWrapper.cs new file mode 100644 index 0000000..c81c56b --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/SyntaxWrapper.cs @@ -0,0 +1,58 @@ +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal abstract class SyntaxWrapper +{ + public static SyntaxWrapper Default { get; } = FindDefaultSyntaxWrapper(); + + public abstract TNode Wrap(SyntaxNode node); + + public abstract SyntaxNode Unwrap(TNode node); + + private static SyntaxWrapper FindDefaultSyntaxWrapper() + { + if (typeof(SyntaxNode).GetTypeInfo().IsAssignableFrom(typeof(TNode).GetTypeInfo())) + { + return new DirectCastSyntaxWrapper(); + } + + return new ConversionSyntaxWrapper(); + } + + private sealed class DirectCastSyntaxWrapper : SyntaxWrapper + { + public override SyntaxNode Unwrap(TNode node) => (SyntaxNode)(object)node; + + public override TNode Wrap(SyntaxNode node) => (TNode)(object)node; + } + + private sealed class ConversionSyntaxWrapper : SyntaxWrapper + { + private readonly Func _unwrapAccessor; + private readonly Func _wrapAccessor; + + public ConversionSyntaxWrapper() + { + _unwrapAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(typeof(TNode), nameof(ISyntaxWrapper.SyntaxNode)); + + var explicitOperator = typeof(TNode).GetTypeInfo().GetDeclaredMethods("op_Explicit") + .Single(m => m.ReturnType == typeof(TNode) && m.GetParameters()[0].ParameterType == typeof(SyntaxNode)); + + var syntaxParameter = Expression.Parameter(typeof(SyntaxNode), "syntax"); + + var wrapAccessorExpression = + Expression.Lambda>( + Expression.Call(explicitOperator, syntaxParameter), + syntaxParameter); + + _wrapAccessor = wrapAccessorExpression.Compile(); + } + + public override SyntaxNode Unwrap(TNode node) => _unwrapAccessor(node); + + public override TNode Wrap(SyntaxNode node) => _wrapAccessor(node); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/VariableDesignationSyntaxWrapper.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/VariableDesignationSyntaxWrapper.cs new file mode 100644 index 0000000..ae2dd25 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Lib/VariableDesignationSyntaxWrapper.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper.Lib; + +internal static partial class RenameHelper +{ + internal struct VariableDesignationSyntaxWrapper : ISyntaxWrapper + { + private const string VariableDesignationSyntaxTypeName = "Microsoft.CodeAnalysis.CSharp.Syntax.VariableDesignationSyntax"; + private static readonly Type VariableDesignationSyntaxType; + + private readonly CSharpSyntaxNode _node; + + static VariableDesignationSyntaxWrapper() + { + VariableDesignationSyntaxType = typeof(CSharpSyntaxNode).GetTypeInfo().Assembly.GetType(VariableDesignationSyntaxTypeName); + } + private VariableDesignationSyntaxWrapper(CSharpSyntaxNode node) => _node = node; + + public CSharpSyntaxNode SyntaxNode => _node; + + public static explicit operator VariableDesignationSyntaxWrapper(SyntaxNode node) + { + if (node == null) + { + return default(VariableDesignationSyntaxWrapper); + } + + if (!IsInstance(node)) + { + throw new InvalidCastException($"Cannot cast '{node.GetType().FullName}' to '{VariableDesignationSyntaxTypeName}'"); + } + + return new VariableDesignationSyntaxWrapper((CSharpSyntaxNode)node); + } + + public static implicit operator CSharpSyntaxNode(VariableDesignationSyntaxWrapper wrapper) + { + return wrapper._node; + } + + public static bool IsInstance(SyntaxNode node) + { + return node != null && LightupHelpers.CanWrapNode(node, VariableDesignationSyntaxType); + } + + internal static VariableDesignationSyntaxWrapper FromUpcast(CSharpSyntaxNode node) + { + return new VariableDesignationSyntaxWrapper(node); + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/ParameterRenamer.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/ParameterRenamer.cs new file mode 100644 index 0000000..105b600 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/ParameterRenamer.cs @@ -0,0 +1,20 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +internal class ParameterRenamer : VariableRenamer +{ + public ParameterRenamer(Document document) : base(document) + { + } + + protected override IEnumerable GetItemsToRename(SyntaxNode currentNode) + { + return + (currentNode as MethodDeclarationSyntax) + .ParameterList + .Parameters + .Select(x => x.Identifier); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Renamer.RenameOutput.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Renamer.RenameOutput.cs new file mode 100644 index 0000000..cb628db --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Renamer.RenameOutput.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +internal abstract partial class Renamer +{ + public class RenameResult + { + public SyntaxNode Node { get; set; } + public Solution Solution { get; set; } + public Document Document { get; set; } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Renamer.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Renamer.cs new file mode 100644 index 0000000..14a5dd9 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/Renamer.cs @@ -0,0 +1,279 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +internal abstract partial class Renamer +{ + private SemanticModel _semanticModel; + private Document WorkingDocument { get; set; } + + public Renamer(Document document) + { + WorkingDocument = document; + + Task.Run(async delegate + { + _semanticModel = await WorkingDocument.GetSemanticModelAsync(); + }).GetAwaiter().GetResult(); + } + + public async Task RenameDeclarationsAsync(SyntaxNode containerNode) + { + SyntaxNode currentNode; + var newNode = containerNode; + RenameResult renamingResult = null; + RenameResult workingRenamingResult = null; + + do + { + currentNode = newNode; + workingRenamingResult = await RenameDeclarationNodeOfContainerNodeAsync(currentNode); + + if (workingRenamingResult != null) + { + renamingResult = workingRenamingResult; + + if (workingRenamingResult.Node != null) + { + newNode = renamingResult.Node; + WorkingDocument = renamingResult.Document; + _semanticModel = await renamingResult.Document.GetSemanticModelAsync(); + } + } + } + while (workingRenamingResult != null && newNode != currentNode); + + return renamingResult; + } + + private IList _visitedTokens = new List(); + + private async Task RenameDeclarationNodeOfContainerNodeAsync(SyntaxNode containerNode) + { + var filteredItemsToRenmae = GetItemsToRename(containerNode).Where(i => _visitedTokens.Contains(i.ValueText) == false); + + foreach (var identifierToRename in filteredItemsToRenmae) + { + var currentName = identifierToRename.ValueText; + var newNames = GetNewName(currentName); + + if (newNames == null) continue; + + var selectedName = currentName; + RenameResult result = null; + + foreach (var newName in newNames) + { + var renameResult = await RenameIdentifierOfContainerNodeAsync(containerNode, identifierToRename, newName); + + if (renameResult == null) continue; + result = renameResult.Value.Key; + selectedName = renameResult.Value.Value; + } + + _visitedTokens.Add(selectedName); + + if (result != null) return result; + } + + return null; + } + + private async Task?> RenameIdentifierOfContainerNodeAsync(SyntaxNode containerNode, SyntaxToken identifierToRename, string newVarName) + { + RenameResult result = null; + + var currentName = identifierToRename.ValueText; + var selectedName = currentName; + + if (string.Compare(newVarName, currentName, false) == 0) return null; + if (ValidateNewName(newVarName) == false) return null; + + var identifierDeclarationNode = identifierToRename.Parent; + + var identifierSymbol = _semanticModel.GetDeclaredSymbol(identifierDeclarationNode); + + var validateNameResult = await Lib.RenameHelper.IsValidNewMemberNameAsync(_semanticModel, identifierSymbol, newVarName); + + if (validateNameResult == false) return null; + + result = await RenameSymbolAsync(WorkingDocument, await WorkingDocument.GetSyntaxRootAsync(), containerNode, identifierDeclarationNode, newVarName); + + if (result != null) selectedName = newVarName; + + return new KeyValuePair(result, selectedName); + } + + private const string Keyword = "Keyword"; + + private Lazy _keywords = + new( + () => + Enum.GetNames(typeof(SyntaxKind)) + .Where(k => k.EndsWith(Keyword)) + .Select(k => k.Remove(k.Length - Keyword.Length).ToLower()) + .ToArray() + ); + + protected virtual bool ValidateNewName(string newVarName) + { + var isValid = true; + isValid &= SyntaxFacts.IsValidIdentifier(newVarName); + isValid &= !_keywords.Value.Contains(newVarName); + return isValid; + } + + protected abstract IEnumerable GetItemsToRename(SyntaxNode currentNode); + + protected abstract string[] GetNewName(string currentName); + + protected static string GetCamelCased(string variableName) + { + var noneLetterCount = variableName.TakeWhile(x => char.IsLetter(x) == false).Count(); + + if (noneLetterCount >= variableName.Length) return variableName; + + if (char.IsUpper(variableName[noneLetterCount]) == false) return variableName; + + var newVarName = + variableName.Substring(0, noneLetterCount) + + variableName.Substring(noneLetterCount, 1).ToLower() + + variableName.Substring(noneLetterCount + 1); + + return newVarName; + } + + protected static string GetPascalCased(string variableName) + { + var noneLetterCount = variableName.TakeWhile(x => char.IsLetter(x) == false).Count(); + + if (noneLetterCount >= variableName.Length) return variableName; + + if (char.IsUpper(variableName[noneLetterCount]) == false) return variableName; + + var newVarName = + variableName.Substring(0, noneLetterCount) + + variableName.Substring(noneLetterCount, 1).ToUpper() + + variableName.Substring(noneLetterCount + 1); + + return newVarName; + } + + protected static bool IsPrivate(FieldDeclarationSyntax x) + { + return + x.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)) || + x.Modifiers + .Any( + m => + m.IsKind(SyntaxKind.PublicKeyword) || + m.IsKind(SyntaxKind.ProtectedKeyword) || + m.IsKind(SyntaxKind.InternalKeyword) + ) == false; + } + + private const string SelectedMethodAnnotation = "SELECTED_METHOD_ANNOTATION"; + + private static async Task RenameSymbolAsync(Document document, SyntaxNode root, SyntaxNode startNode, ParameterSyntax declarationNode, string newName) + { + var identifierToken = declarationNode.Identifier; + + var methodAnnotation = new SyntaxAnnotation(SelectedMethodAnnotation); + var changeDic = new Dictionary(); + + if (startNode != null) + { + changeDic.Add(startNode, startNode.WithAdditionalAnnotations(methodAnnotation)); + } + + changeDic.Add(declarationNode, declarationNode.WithIdentifier(identifierToken.WithAdditionalAnnotations(RenameAnnotation.Create()))); + + var annotatedRoot = root.ReplaceNodes(changeDic.Keys, (x, y) => changeDic[x]); + + var newSolution = await RenameSymbolAsync(document, annotatedRoot, identifierToken, methodAnnotation, newName); + + return await GetNewStartNodeAsync(newSolution, document, methodAnnotation, startNode); + } + + private static async Task RenameSymbolAsync(Document document, SyntaxNode root, SyntaxNode startNode, VariableDeclaratorSyntax declarationNode, string newName) + { + var identifierToken = declarationNode.Identifier; + + var methodAnnotation = new SyntaxAnnotation(SelectedMethodAnnotation); + var changeDic = new Dictionary(); + + if (startNode != null) + { + changeDic.Add(startNode, startNode.WithAdditionalAnnotations(methodAnnotation)); + } + + changeDic.Add(declarationNode, declarationNode.WithIdentifier(identifierToken.WithAdditionalAnnotations(RenameAnnotation.Create()))); + + var annotatedRoot = root.ReplaceNodes(changeDic.Keys, (x, y) => changeDic[x]); + + var newSolution = await RenameSymbolAsync(document, annotatedRoot, identifierToken, methodAnnotation, newName); + + return await GetNewStartNodeAsync(newSolution, document, methodAnnotation, startNode); + } + + private static async Task GetNewStartNodeAsync(Solution newSolution, Document document, SyntaxAnnotation methodAnnotation, SyntaxNode startNode) + { + var newDocument = + newSolution.Projects.FirstOrDefault(x => x.Name == document.Project.Name) + .Documents.FirstOrDefault(x => x.FilePath == document.FilePath); + + var newRoot = await + newSolution.Projects.FirstOrDefault(x => x.Name == document.Project.Name) + .Documents.FirstOrDefault(x => x.FilePath == document.FilePath).GetSyntaxRootAsync(); + + return new RenameResult + { + Node = newRoot.GetAnnotatedNodes(methodAnnotation).FirstOrDefault(), + Solution = newSolution, + Document = newDocument + }; + } + + public static Task RenameSymbolAsync(Document document, SyntaxNode root, SyntaxNode startNode, SyntaxToken identifierToken, string newName) + { + return RenameSymbolAsync(document, root, startNode, identifierToken.Parent, newName); + } + + public static async Task RenameSymbolAsync(Document document, SyntaxNode root, SyntaxNode startNode, SyntaxNode declarationNode, string newName) + { + if (declarationNode is VariableDeclaratorSyntax variableNode) + { + return await RenameSymbolAsync(document, root, startNode, variableNode, newName); + } + + if (declarationNode is ParameterSyntax parameterNode) + { + return await RenameSymbolAsync(document, root, startNode, parameterNode, newName); + } + + return null; + } + + private static async Task RenameSymbolAsync(Document document, SyntaxNode annotatedRoot, SyntaxToken identifierNode, SyntaxAnnotation methodAnnotation, string newName, CancellationToken cancellationToken = default(CancellationToken)) + { + var annotatedSolution = document.Project.Solution.WithDocumentSyntaxRoot(document.Id, annotatedRoot); + + var annotatedDocument = annotatedSolution.GetDocument(document.Id); + + annotatedRoot = await annotatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var annotatedToken = annotatedRoot.FindToken(identifierNode.SpanStart); + + var semanticModel = await annotatedDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var symbol = semanticModel.GetDeclaredSymbol(annotatedToken.Parent, cancellationToken); + + // await Microsoft.CodeAnalysis.Rename.Renamer.RenameSymbolAsync(annotatedSolution, symbol, newName, annotatedSolution.Workspace.Options, cancellationToken); + var renamingresult = await Microsoft.CodeAnalysis.Rename.Renamer.RenameSymbolAsync(annotatedSolution, symbol, newName, annotatedSolution.Workspace.Options); + + return renamingresult; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/VariableRenamer.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/VariableRenamer.cs new file mode 100644 index 0000000..47e52b9 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/VariableRenamer.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +internal class VariableRenamer : Renamer +{ + public VariableRenamer(Document document) : base(document) + { + } + + protected override IEnumerable GetItemsToRename(SyntaxNode currentNode) + { + return + currentNode + .DescendantNodes() + .OfType() + .Where(v => (v?.Parent?.Parent as LocalDeclarationStatementSyntax)?.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ConstKeyword)) == false) + .Select(x => x.Identifier); + } + + protected override string[] GetNewName(string currentName) + { + if (currentName.StartsWith("_")) + { + currentName = currentName.TrimStart('_'); + if (currentName.Length == 0) return null; + + if (char.IsLetter(currentName[0])) + { + return new[] { GetCamelCased(currentName) }; + } + } + else if (char.IsLetter(currentName[0]) && char.IsUpper(currentName[0])) + { + return new[] { GetCamelCased(currentName) }; + } + + return null; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/VariableRenamingBase.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/VariableRenamingBase.cs new file mode 100644 index 0000000..364c591 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/RenameHelper/VariableRenamingBase.cs @@ -0,0 +1,62 @@ +using Microsoft.CodeAnalysis; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils.RenameHelper; + +public abstract class VariableRenamingBase : CodeCleanerCommandRunnerBase +{ + private const string SelectedMethodAnnotation = "SELECTED_METHOD_ANNOTATION"; + + private Document _workingDocument, _orginalDocument; + + public override async Task CleanUpAsync(SyntaxNode initialSourceNode) + { + var annotationForSelectedNode = new SyntaxAnnotation(SelectedMethodAnnotation); + _orginalDocument = ProjectItemDetails.Document; + _workingDocument = ProjectItemDetails.Document; + + if (_orginalDocument == null) return initialSourceNode; + + SyntaxNode workingNode; + var annotatedRoot = initialSourceNode; + + do + { + workingNode = GetWorkingNode(annotatedRoot, annotationForSelectedNode); + + if (workingNode == null) continue; + + var annotatedNode = workingNode.WithAdditionalAnnotations(annotationForSelectedNode); + annotatedRoot = annotatedRoot?.ReplaceNode(workingNode, annotatedNode); + _workingDocument = _workingDocument.WithSyntaxRoot(annotatedRoot); + annotatedRoot = await _workingDocument.GetSyntaxRootAsync(); + annotatedNode = annotatedRoot?.GetAnnotatedNodes(annotationForSelectedNode).FirstOrDefault(); + + var rewriter = GetRewriter(_workingDocument); + + rewriter.Visit(annotatedNode); + CollectMessages(rewriter.GetReport()); + _workingDocument = rewriter.WorkingDocument; + } while (workingNode != null); + + return null; + } + + protected override async Task SaveResultAsync(SyntaxNode initialSourceNode) + { + await base.SaveResultAsync((await _workingDocument.GetSyntaxTreeAsync()).GetRoot()); + } + + protected abstract SyntaxNode GetWorkingNode(SyntaxNode initialSourceNode, SyntaxAnnotation annotationForSelectedNodes); + + protected abstract VariableRenamingBaseRewriter GetRewriter(Document workingDocument); + + protected abstract class VariableRenamingBaseRewriter : CleanupCSharpSyntaxRewriter + { + public Document WorkingDocument { get; protected set; } + protected VariableRenamingBaseRewriter(Document workingDocument, bool isReportOnlyMode, ICleanupOption options) : base(isReportOnlyMode, options) + { + WorkingDocument = workingDocument; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/SyntaxTokenExtensions.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/SyntaxTokenExtensions.cs new file mode 100644 index 0000000..2e61d2d --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/SyntaxTokenExtensions.cs @@ -0,0 +1,209 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils; + +public static class SyntaxTokenExtensions +{ + public static SyntaxNode RemovePrivateTokens(this SyntaxNode root, List tokens) + { + if (!tokens.Any()) return root; + + return root.ReplaceTokens(tokens, MakeReplacementToken(tokens)); + } + + private static Func MakeReplacementToken(List tokens) + { + // replace with the LeadingTrivia so that the comments (if any) will not be lost also the private keyword is replaced at the same time + return (oldToken, newToken) => SyntaxFactory.ParseToken(oldToken.LeadingTrivia.ToFullString()); + } + + public static FileLinePositionSpan GetFileLinePosSpan(this SyntaxToken node) + { + return node.SyntaxTree.GetLineSpan(new TextSpan(node.Span.Start, node.Span.Length)); + } + + private static object _lockFileWrite = new(); + public static void WriteSourceTo(this SyntaxNode sourceCode, string filePath) + { + lock (_lockFileWrite) + { + var encoding = DetectFileEncoding(filePath); + + var source = sourceCode.ToFullString().Trim(new[] { '\r', '\n' }); + var fileText = File.ReadAllText(filePath).Trim(new[] { '\r', '\n' }); + + var bEqual = string.Compare(source, fileText, StringComparison.Ordinal) == 0; + + if (!bEqual) + { + using (var write = new StreamWriter(filePath, false, encoding)) + write.Write(sourceCode.ToFullString()); + } + } + } + + private static Encoding DetectFileEncoding(string filePath) + { + Encoding encoding = null; + + using (var reader = new StreamReader(filePath)) + encoding = reader.CurrentEncoding; + + using (var reader = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + var bom = new byte[4]; + reader.Read(bom, 0, 4); + + if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) + { + encoding = new UTF8Encoding(true); + } + else if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) + { + encoding = new UTF7Encoding(true); + } + else if (bom[0] == 0xff && bom[1] == 0xfe) + { + encoding = new UnicodeEncoding(false, true); + } + else if (bom[0] == 0xfe && bom[1] == 0xff) + { + encoding = new UTF8Encoding(false); + // encoding = new BigEndianUnicode(true); + } + else if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) + { + encoding = new UTF32Encoding(false, true); + } + else + { + encoding = new UTF8Encoding(false); + } + } + + return encoding; + } + + public static SyntaxNode ToSyntaxNode(this Document item) + { + lock (_lockFileWrite) + { + return CSharpSyntaxTree.ParseText(File.ReadAllText(item.ToFullPathPropertyValue())).GetRoot(); + } + } + + public static string ToFullPathPropertyValue(this Document item) + { + if (item == null) return null; + return item.FilePath; + } + + public static SyntaxToken WithoutTrivia(this SyntaxToken token) + { + return token.WithLeadingTrivia().WithTrailingTrivia(); + } + + public static SyntaxTriviaList WithoutWhiteSpaceTrivia(this SyntaxTriviaList triviaList) + { + return new SyntaxTriviaList().AddRange(triviaList.Where(t => !t.IsWhiteSpaceTrivia())); + } + + public static SyntaxToken WithoutWhiteSpaceTrivia(this SyntaxToken token) + { + return + token + .WithLeadingTrivia(token.LeadingTrivia.Where(t => !t.IsWhiteSpaceTrivia())) + .WithTrailingTrivia(token.TrailingTrivia.Where(t => !t.IsWhiteSpaceTrivia())); + } + + public static T WithoutWhiteSpaceTrivia(this T token) + where T : SyntaxNode + { + return + token + .WithLeadingTrivia(token.GetLeadingTrivia().Where(t => !t.IsWhiteSpaceTrivia())) + .WithTrailingTrivia(token.GetTrailingTrivia().Where(t => !t.IsWhiteSpaceTrivia())); + } + + public static bool HasNoneWhiteSpaceTrivia(this IEnumerable triviaList, SyntaxKind[] exceptionList = null) + { + if (exceptionList == null) + return triviaList.Any(t => !t.IsWhiteSpaceTrivia()); + + return triviaList.Any(t => !t.IsWhiteSpaceTrivia() && exceptionList.Any(e => t.IsKind(e)) == false); + } + + public static bool IsWhiteSpaceTrivia(this SyntaxTrivia trivia) + { + return trivia.IsKind(SyntaxKind.EndOfLineTrivia) || trivia.IsKind(SyntaxKind.WhitespaceTrivia); + } + + public static bool HasNoneWhiteSpaceTrivia(this SyntaxNode node, SyntaxKind[] exceptionList = null) + { + return (node.ContainsDirectives || node.HasStructuredTrivia || node.DescendantTrivia(descendIntoTrivia: true).HasNoneWhiteSpaceTrivia(exceptionList)); + } + + public static bool HasNoneWhiteSpaceTrivia(this SyntaxToken token, SyntaxKind[] exceptionList = null) + { + return (token.ContainsDirectives || token.HasStructuredTrivia || token.GetAllTrivia().HasNoneWhiteSpaceTrivia()); + } + + public static bool IsPrivate(this FieldDeclarationSyntax field) => IsPrivate(field.Modifiers); + + public static bool IsPrivate(this PropertyDeclarationSyntax field) + { + return IsPrivate(field.Modifiers); + } + + public static bool IsPublic(this FieldDeclarationSyntax field) + { + return field.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)); + } + + public static bool IsPublic(this PropertyDeclarationSyntax field) + { + return field.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)); + } + + public static bool IsProtected(this FieldDeclarationSyntax field) + { + return field.Modifiers.Any(m => m.IsKind(SyntaxKind.ProtectedKeyword)); + } + + public static bool IsProtected(this PropertyDeclarationSyntax field) + { + return field.Modifiers.Any(m => m.IsKind(SyntaxKind.ProtectedKeyword)); + } + + public static bool IsInternal(this FieldDeclarationSyntax field) + { + return field.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword)); + } + + public static bool IsInternal(this PropertyDeclarationSyntax field) + { + return field.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword)); + } + + public static bool IsPrivate(this LocalDeclarationStatementSyntax local) + { + return IsPrivate(local.Modifiers); + } + + private static bool IsPrivate(SyntaxTokenList modifiers) + { + return + modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)) || + modifiers + .Any( + m => + m.IsKind(SyntaxKind.PublicKeyword) || + m.IsKind(SyntaxKind.ProtectedKeyword) || + m.IsKind(SyntaxKind.InternalKeyword) + ) == false; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Menus/Cleanup/Utils/TypesMapItem.cs b/TidyCSharp.Cli/Menus/Cleanup/Utils/TypesMapItem.cs new file mode 100644 index 0000000..036bf85 --- /dev/null +++ b/TidyCSharp.Cli/Menus/Cleanup/Utils/TypesMapItem.cs @@ -0,0 +1,122 @@ +using System.CodeDom; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CSharp; + +namespace TidyCSharp.Cli.Menus.Cleanup.Utils; + +public class TypesMapItem +{ + public string Name { get; set; } + public string FullName { get; set; } + public string BuiltInName { get; set; } + + private Type _builtInType; + public Type BuiltInType + { + get + { + if (_builtInType != null) return _builtInType; + if (string.IsNullOrEmpty(BuiltInName)) return _builtInType; + + _builtInType = Type.GetType(FullName); + + GetTypeDefaultValue(); + + return _builtInType; + } + } + + private object _defaultValue; + public object DefaultValue => GetTypeDefaultValue(); + + private object GetTypeDefaultValue() + { + if (BuiltInType == null) return null; + if (_defaultValue != null) return _defaultValue; + + if (BuiltInType.IsValueType) + { + _defaultValue = Activator.CreateInstance(BuiltInType); + } + + return _defaultValue; + } + + public TypeSyntax NewNode { get; set; } + + internal static TypesMapItem GetBuiltInTypes(Type type, TypeSyntax node, CSharpCodeProvider provider) + { + return new TypesMapItem + { + Name = type.Name, + FullName = type.FullName, + BuiltInName = provider.GetTypeOutput(new CodeTypeReference(type)), + NewNode = node + }; + } + + internal static TypeSyntax GetPredefineType(SyntaxKind keyword) + { + return SyntaxFactory.PredefinedType(SyntaxFactory.Token(keyword)); + } + + private static Dictionary _builtInTypesDic, _predefinedTypesDic; + public static Dictionary GetBuiltInTypesDic() + { + if (_builtInTypesDic != null) return _builtInTypesDic; + + var output = new Dictionary(); + + using (var provider = new Microsoft.CSharp.CSharpCodeProvider()) + { + var typesList = new TypesMapItem[] + { + GetBuiltInTypes(Type.GetType("System.Boolean"), GetPredefineType(SyntaxKind.BoolKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Byte"), GetPredefineType(SyntaxKind.ByteKeyword), provider), + GetBuiltInTypes(Type.GetType("System.SByte"), GetPredefineType(SyntaxKind.SByteKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Char"), GetPredefineType(SyntaxKind.CharKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Decimal"), GetPredefineType(SyntaxKind.DecimalKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Double"), GetPredefineType(SyntaxKind.DoubleKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Single"), GetPredefineType(SyntaxKind.FloatKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Int32"), GetPredefineType(SyntaxKind.IntKeyword), provider), + GetBuiltInTypes(Type.GetType("System.UInt32"), GetPredefineType(SyntaxKind.UIntKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Int64"), GetPredefineType(SyntaxKind.LongKeyword), provider), + GetBuiltInTypes(Type.GetType("System.UInt64"), GetPredefineType(SyntaxKind.ULongKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Object"), GetPredefineType(SyntaxKind.ObjectKeyword), provider), + GetBuiltInTypes(Type.GetType("System.Int16"), GetPredefineType(SyntaxKind.ShortKeyword), provider), + GetBuiltInTypes(Type.GetType("System.UInt16"), GetPredefineType(SyntaxKind.UShortKeyword), provider), + GetBuiltInTypes(Type.GetType("System.String"), GetPredefineType(SyntaxKind.StringKeyword), provider), + }; + + foreach (var item in typesList) + { + output.Add(item.Name, item); + output.Add(item.FullName, item); + } + + return _builtInTypesDic = output; + } + } + + internal static Dictionary GetAllPredefinedTypesDic() + { + if (_predefinedTypesDic != null) return _predefinedTypesDic; + + var output = GetBuiltInTypesDic(); + + using (var provider = new CSharpCodeProvider()) + { + var oldValues = output.Values.GroupBy(x => x.BuiltInName).ToList(); + + foreach (var item0 in oldValues) + { + var item = item0.FirstOrDefault(); + + output.Add(item.BuiltInName, new TypesMapItem { BuiltInName = item.BuiltInName, Name = item.BuiltInName, FullName = item.FullName }); + } + + return _predefinedTypesDic = output; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Program.cs b/TidyCSharp.Cli/Program.cs new file mode 100644 index 0000000..63d08a8 --- /dev/null +++ b/TidyCSharp.Cli/Program.cs @@ -0,0 +1,46 @@ +using Microsoft.Build.Locator; +using TidyCSharp.Cli.Menus.Cleanup.CommandsHandlers; +using CommandLine; +using TidyCSharp.Cli.Menus.Cleanup.CommandRunners._Infra; + +namespace TidyCSharp.Cli; + +internal class Program +{ + private static async Task Main(string[] args) + { +#if DEBUG + if (args.Length == 0) + args = new string[] { "-p", "D:\\Projects\\Repositories\\hub\\MS.Hub.sln", "-o", "C:\\tmp\\TidyDiag.txt", "-m", "r" }; +#endif + + await Parser.Default.ParseArguments(args) + .WithParsedAsync(async o => + { + await Start(o); + }); + } + + private static async Task Start(CliOptions o) + { + MSBuildLocator.RegisterDefaults(); + + await new TidyCSharpPackage() + .InitializeAsync(o.SolutionPath); + + if (o.Mode == "w") + { + var cleanUpRunner = new ActionSafeRulesCodeCleanup(); + await cleanUpRunner.RunSafeRulesCleanUpAsync(); + } + else if (o.Mode == "r") + { + var cleanUpRunner = new ActionReadOnlyCodeCleanup(); + cleanUpRunner.RunReadOnlyCleanUp(); + + CodeCleanerHost.GenerateMessages(o.OutputPath); + + } + + } +} diff --git a/TidyCSharp.Cli/ProgressBarProjectLoadStatus.cs b/TidyCSharp.Cli/ProgressBarProjectLoadStatus.cs new file mode 100644 index 0000000..fac19dd --- /dev/null +++ b/TidyCSharp.Cli/ProgressBarProjectLoadStatus.cs @@ -0,0 +1,11 @@ +using Microsoft.CodeAnalysis.MSBuild; + +namespace TidyCSharp.Cli; + +public class ProgressBarProjectLoadStatus : IProgress +{ + public void Report(ProjectLoadProgress value) + { + Console.Out.WriteLine($"{value.Operation} {value.FilePath}"); + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Properties/Resources.Designer.cs b/TidyCSharp.Cli/Properties/Resources.Designer.cs new file mode 100644 index 0000000..87a41e5 --- /dev/null +++ b/TidyCSharp.Cli/Properties/Resources.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TidyCSharp.Cli.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TidyCSharp.Cli.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to PrivateModifierRemover: The file path is either null or the file does not exist. + /// + internal static string PrivateModifierCleanUpFailed { + get { + return ResourceManager.GetString("PrivateModifierCleanUpFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Warning - C# Code Cleanup. + /// + internal static string WarningCaptionCleanup { + get { + return ResourceManager.GetString("WarningCaptionCleanup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Warning - Organize using directives. + /// + internal static string WarningCaptionOrganizeUsing { + get { + return ResourceManager.GetString("WarningCaptionOrganizeUsing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to It is strongly recommended that you close all open windows before starting this process.. + /// + internal static string WarnOnCodeCleanUp { + get { + return ResourceManager.GetString("WarnOnCodeCleanUp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to It is strongly recommended that you close all open windows before starting this process.. + /// + internal static string WarnOnOrganizeUsings { + get { + return ResourceManager.GetString("WarnOnOrganizeUsings", resourceCulture); + } + } + } +} diff --git a/TidyCSharp.Cli/Properties/Resources.resx b/TidyCSharp.Cli/Properties/Resources.resx new file mode 100644 index 0000000..197e284 --- /dev/null +++ b/TidyCSharp.Cli/Properties/Resources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + PrivateModifierRemover: The file path is either null or the file does not exist + + + Warning - C# Code Cleanup + + + Warning - Organize using directives + + + It is strongly recommended that you close all open windows before starting this process. + + + It is strongly recommended that you close all open windows before starting this process. + + \ No newline at end of file diff --git a/TidyCSharp.Cli/TidyCSharp.Cli.csproj b/TidyCSharp.Cli/TidyCSharp.Cli.csproj new file mode 100644 index 0000000..22c9d6b --- /dev/null +++ b/TidyCSharp.Cli/TidyCSharp.Cli.csproj @@ -0,0 +1,35 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/TidyCSharp.Cli/TidyCSharpPackage.cs b/TidyCSharp.Cli/TidyCSharpPackage.cs new file mode 100644 index 0000000..9c3997e --- /dev/null +++ b/TidyCSharp.Cli/TidyCSharpPackage.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; +using TidyCSharp.Cli.Menus.Cleanup.Utils; + +namespace TidyCSharp.Cli; + +public sealed class TidyCSharpPackage +{ + public static TidyCSharpPackage Instance { get; private set; } + + public MSBuildWorkspace VsWorkspace { get; set; } + + public Solution Solution + { + get; private set; + } + + public async Task InitializeAsync(string solutionPath) + { + // MSBuild should copy Microsoft.CodeAnalysis.CSharp.Workspaces.dll + var _ = typeof(Microsoft.CodeAnalysis.CSharp.Formatting.CSharpFormattingOptions); + + VsWorkspace = MSBuildWorkspace.Create(); + + VsWorkspace.SkipUnrecognizedProjects = true; + VsWorkspace.WorkspaceFailed += (sender, e) => + { + if (e.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure) + { + Console.Error.WriteLine(e.Diagnostic.Message); + } + }; + + Solution = await VsWorkspace.OpenSolutionAsync(solutionPath, new ProgressBarProjectLoadStatus()); + + Instance = this; + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Utility/ProcessActions.cs b/TidyCSharp.Cli/Utility/ProcessActions.cs new file mode 100644 index 0000000..95d619b --- /dev/null +++ b/TidyCSharp.Cli/Utility/ProcessActions.cs @@ -0,0 +1,17 @@ +namespace TidyCSharp.Cli.Utility; + +public class ProcessActions +{ + public static void GeeksProductivityToolsProcess() + { + var visualStudioProcesses = System.Diagnostics.Process.GetProcessesByName("devenv"); + + foreach (var process in visualStudioProcesses) + { + if (!process.MainWindowTitle.Contains("GeeksProductivityTools")) continue; + + process.Kill(); + break; + } + } +} \ No newline at end of file diff --git a/TidyCSharp.Cli/Utility/SolutionActions.cs b/TidyCSharp.Cli/Utility/SolutionActions.cs new file mode 100644 index 0000000..964e0ae --- /dev/null +++ b/TidyCSharp.Cli/Utility/SolutionActions.cs @@ -0,0 +1,11 @@ +using Microsoft.CodeAnalysis; + +namespace TidyCSharp.Cli.Utility; + +public class SolutionActions +{ + public static List FindProjects() + { + return TidyCSharpPackage.Instance.Solution.Projects.ToList(); + } +} \ No newline at end of file diff --git a/TidyCSharp.sln b/TidyCSharp.sln index f8d4aa6..79890ba 100644 --- a/TidyCSharp.sln +++ b/TidyCSharp.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31129.286 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TidyCSharp", "TidyCSharp\TidyCSharp.csproj", "{7B708065-1213-49BD-94B6-A67A786FAC5E}" EndProject @@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution GCop.json = GCop.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TidyCSharp.Cli", "TidyCSharp.Cli\TidyCSharp.Cli.csproj", "{CDA0BACB-37AF-4457-9ED3-1834575D65B2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,6 +24,12 @@ Global {7B708065-1213-49BD-94B6-A67A786FAC5E}.Debug-ReadonlyMode|Any CPU.Build.0 = Debug-ReadonlyMode|Any CPU {7B708065-1213-49BD-94B6-A67A786FAC5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B708065-1213-49BD-94B6-A67A786FAC5E}.Release|Any CPU.Build.0 = Release|Any CPU + {CDA0BACB-37AF-4457-9ED3-1834575D65B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDA0BACB-37AF-4457-9ED3-1834575D65B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDA0BACB-37AF-4457-9ED3-1834575D65B2}.Debug-ReadonlyMode|Any CPU.ActiveCfg = Debug|Any CPU + {CDA0BACB-37AF-4457-9ED3-1834575D65B2}.Debug-ReadonlyMode|Any CPU.Build.0 = Debug|Any CPU + {CDA0BACB-37AF-4457-9ED3-1834575D65B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDA0BACB-37AF-4457-9ED3-1834575D65B2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE