using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace File_Folder_Helper.ADO2025.PI5; internal static partial class Helper20250320 { private record Match(string Name, string Parameters, string Result, string Scope, string Static, string Value, string Async, string Partial); private record Search(string Constructor, string Delegate, string Name, string Not, string Wrap); private record Method(int? EndLine, ReadOnlyDictionary Parameters, string FirstLine, int I, string Line, Match Match, ReadOnlyCollection ReferenceToLineNumbers, int? ScopeEnum, Search Search, int StartLine) { public override string ToString() { string result = JsonSerializer.Serialize(this, MethodCollectionCommonSourceGenerationContext.Default.Method); return result; } } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Method[]))] private partial class MethodCollectionCommonSourceGenerationContext : JsonSerializerContext { } private record MethodWith(int? EndLine, ReadOnlyDictionary Parameters, string FirstLine, string Line, Match Match, ReadOnlyCollection References, ReadOnlyCollection ReferenceToLineNumbers, int? ScopeEnum, Search Search, int StartLine) { public override string ToString() { string result = JsonSerializer.Serialize(this, MethodCollectionCommonSourceGenerationContext.Default.Method); return result; } } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(MethodWith[]))] private partial class MethodWithCollectionCommonSourceGenerationContext : JsonSerializerContext { } private const string _Name = "name"; private const string _Async = "async"; private const string _Scope = "scope"; private const string _Result = "result"; private const string _Static = "static"; private const string _Partial = "partial"; private const string _Parameters = "parameters"; [GeneratedRegex(@"[[\]<,>?a-zA-Z0-9_()\s]*?\s[a-z_]{1}[a-zA-Z0-9_]*?,")] private static partial Regex CSharpParameter(); [GeneratedRegex(@"(?public|private|internal|protected|\sI[a-zA-Z0-9_]*\.)\s?\b(?static)?\s?\b(?partial)?\s?\b(?async)?\s?\b(?[\[\]\.\?<,>a-zA-Z0-9_()\s]*?)\s?\b(?[A-Z_]{1}[a-zA-Z0-9_]*)+\((?.*)\)")] private static partial Regex CSharpMethodLine(); internal static void SortCodeMethods(ILogger logger, List args, CancellationToken cancellationToken) { bool check; string[] lines; List changed = []; bool usePathCombine = true; long ticks = DateTime.Now.Ticks; bool logOnly = bool.Parse(args[2]); int scopeSpaces = int.Parse(args[3]); logger.LogInformation("{ticks}", ticks); string repositoryDirectory = Path.GetFullPath(args[0]); string[] cSharpFiles = Directory.GetFiles(repositoryDirectory, "*.cs", SearchOption.AllDirectories); ReadOnlyCollection gitOthersModifiedAndDeletedExcludingStandardFiles = logOnly ? new(cSharpFiles) : Helpers.HelperGit.GetOthersModifiedAndDeletedExcludingStandardFiles(repositoryDirectory, usePathCombine, cancellationToken); foreach (string cSharpFile in cSharpFiles) { if (!gitOthersModifiedAndDeletedExcludingStandardFiles.Contains(cSharpFile)) continue; for (int i = 0; i < 10; i++) { lines = File.ReadAllLines(cSharpFile); check = SortFile(logger, logOnly, scopeSpaces, cSharpFile, lines); if (check) { Thread.Sleep(500); changed.Add($"{i + 1:00}) {cSharpFile}"); } if (logOnly || !check) break; } } if (changed.Count == 0) logger.LogInformation("No changes :)"); else { changed.Reverse(); foreach (string c in changed) logger.LogInformation(c); } } private static bool SortFile(ILogger logger, bool logOnly, int scopeSpaces, string cSharpFile, string[] lines) { bool result; ReadOnlyCollection methods = GetMethods(logger, scopeSpaces, cSharpFile, lines); if (methods.Count == 0) result = false; else if (methods.Any(l => l.EndLine is null)) result = false; else if (logOnly) { foreach (Method method in methods) logger.LogInformation("{cSharpFile} - {Name} has {lines} line(s)", cSharpFile, method.Match.Name, (method.EndLine is null ? 999999 : method.EndLine.Value - method.StartLine).ToString("000000")); result = false; } else { ReadOnlyCollection sortedMethods = GetSortedMethods(methods); if (Debugger.IsAttached) File.WriteAllText(Path.Combine(".vscode", "helper", ".txt"), string.Join(Environment.NewLine, sortedMethods.Select(l => $"{l.Match.Name} => {l.Parameters.Count}"))); ReadOnlyCollection collection = GetCollection(logger, lines, sortedMethods); result = WriteAllLines(cSharpFile, lines, collection); } return result; } private static ReadOnlyCollection GetMethods(ILogger logger, int scopeSpaces, string cSharpFile, string[] lines) { List results = []; int check; int blocks; bool isLinq; Match match; string line; int? endLine; int startLine; Method method; Search search; int? scopeEnum; string firstLine; string innerLine; string lineSegmentFirst; List referenceToLineNumbers; Regex parameterRegex = CSharpParameter(); ReadOnlyDictionary parameters; System.Text.RegularExpressions.Match regularExpressionsMatch; for (int i = 0; i < lines.Length; i++) { check = GetNumberOfStartSpaces(lines, i); if (check != scopeSpaces) continue; line = lines[i].Trim(); if (string.IsNullOrEmpty(line)) continue; if (line.Length < 5) continue; if (line.EndsWith(',')) continue; regularExpressionsMatch = CSharpMethodLine().Match(line); if (!regularExpressionsMatch.Success) continue; match = new(Async: regularExpressionsMatch.Groups[_Async].Value, Name: regularExpressionsMatch.Groups[_Name].Value, Parameters: regularExpressionsMatch.Groups[_Parameters].Value, Partial: regularExpressionsMatch.Groups[_Partial].Value, Result: regularExpressionsMatch.Groups[_Result].Value, Scope: regularExpressionsMatch.Groups[_Scope].Value, Static: regularExpressionsMatch.Groups[_Static].Value, Value: regularExpressionsMatch.Value); scopeEnum = GetScopeEnum(match); parameters = GetParameters(parameterRegex, match); search = new(Constructor: $"{match.Name.ToLower()} = new(", Delegate: $" += {match.Name};", Name: $" {match.Name}(", Not: $"!{match.Name}(", Wrap: $"({match.Name}("); logger.LogInformation("{line} {a} // {results}", line.Split(" =>")[0], "{ }", results.Count); if (string.IsNullOrEmpty(match.Name)) continue; blocks = 0; startLine = GetStartLine(lines, i); if (!lines[startLine].StartsWith("#pragma") && !lines[startLine].StartsWith("#nullable")) firstLine = lines[startLine].Trim(); else firstLine = lines[startLine + 1].Trim(); isLinq = !lines[i + 1].StartsWith("#pragma") && !lines[i + 1].StartsWith("#nullable") && lines[i + 1].Trim() != "{"; if (isLinq) blocks++; endLine = null; for (int j = i + 1; j < lines.Length; j++) { innerLine = lines[j].Trim(); if (innerLine.StartsWith("#pragma") || innerLine.StartsWith("#nullable")) continue; if (isLinq && string.IsNullOrEmpty(innerLine)) { if (line.EndsWith(';')) blocks--; } blocks += GetLineBlockCount(innerLine, isLinq); if (blocks != 0) continue; endLine = j; if (lines.Length > j + 1 && string.IsNullOrEmpty(lines[j + 1].Trim())) endLine++; if (j > lines.Length - 2) throw new Exception(); break; } referenceToLineNumbers = GetReferenceToLineNumbers(lines: lines, start: 0, end: lines.Length, i: i, search: search, parameters: parameters); if (referenceToLineNumbers.Count == 0) { lineSegmentFirst = line.Split(match.Name)[0]; if (!lines[i - 1].Trim().StartsWith("[Obsolete")) { if (lineSegmentFirst.StartsWith("private")) logger.LogWarning("// <{cSharpFileName}> {name} with {parameters} parameter(s) <{line}>", Path.GetFileName(cSharpFile), match.Name, parameters, lineSegmentFirst); else logger.LogInformation("// <{cSharpFileName}> {name} with {parameters} parameter(s) <{line}>", Path.GetFileName(cSharpFile), match.Name, parameters, lineSegmentFirst); } } if (referenceToLineNumbers.Count == 0) referenceToLineNumbers.Add(-1); logger.LogInformation("{line} {a} // {results} ~~~ {startLine} => {firstUsedLine}", line.Split(" =>")[0], "{ }", results.Count, startLine, referenceToLineNumbers.First()); method = new(EndLine: endLine, FirstLine: firstLine, I: i, Line: line, Match: match, Parameters: parameters, ReferenceToLineNumbers: referenceToLineNumbers.AsReadOnly(), Search: search, ScopeEnum: scopeEnum, StartLine: startLine); results.Add(method); } return results.AsReadOnly(); } private static int GetNumberOfStartSpaces(string[] lines, int i) { int result = 0; foreach (char @char in lines[i]) { if (@char != ' ') break; result += 1; } return result; } private static int GetScopeEnum(Match match) { int result; int value = match.Scope switch { "public" => 8000, "internal" => 7000, "protected" => 6000, "private" => 5000, _ => match.Scope.Length > 2 && match.Scope[..2] == " I" && match.Scope[^1] == '.' ? 9000 : throw new NotImplementedException() }; result = value + (string.IsNullOrEmpty(match.Result) ? 100 : 0) + (string.IsNullOrEmpty(match.Static) ? 0 : 10) + (string.IsNullOrEmpty(match.Async) ? 0 : 1); return result; } private static ReadOnlyDictionary GetParameters(Regex parameterRegex, Match match) { Dictionary results = []; string value; string[] segments; System.Text.RegularExpressions.Match[] matches = parameterRegex.Matches($"{match.Parameters},").ToArray(); foreach (System.Text.RegularExpressions.Match m in matches) { if (!m.Success) continue; value = m.Value.Trim()[..^1]; segments = value.Split(' '); results.Add(segments[^1], value); } return new(results); } private static int GetStartLine(string[] lines, int i) { int result = i; string line; for (int j = i - 1; j > -1; j--) { line = lines[j].Trim(); if (!line.StartsWith('[') && !line.StartsWith('#') && !line.StartsWith("/// ")) break; result--; } return result; } private static int GetLineBlockCount(string line, bool isLinq) { int result = 0; bool ignore = false; for (int i = 0; i < line.Length; i++) { if (line[i] == '\'') i++; else if (!isLinq && !ignore && line[i] == '{') result++; else if (!isLinq && !ignore && line[i] == '}') result--; else if (isLinq && !ignore && line[i] == ';') result--; else if (i > 0 && line[i] == '"' && line[i - 1] != '\\') ignore = !ignore; } return result; } private static List GetReferenceToLineNumbers(string[] lines, int start, int end, int i, Search search, ReadOnlyDictionary parameters) { List results = []; string[] segments; string[] afterSegments; string lastSegmentBeforeDot; for (int j = start; j < end; j++) { if (j == i) continue; segments = lines[j].Split(search.Name); if (segments.Length == 1) { segments = lines[j].Split(search.Not); if (segments.Length == 1) { segments = lines[j].Split(search.Wrap); if (segments.Length == 1) { if (!lines[j].EndsWith(search.Delegate)) { segments = lines[j].Split(search.Constructor); if (segments.Length == 1) continue; } } } } if (lines[j].EndsWith(search.Delegate)) results.Add(j); else { lastSegmentBeforeDot = segments[^1].Split(").")[0]; if (parameters.Count == 0) { if (lastSegmentBeforeDot.Contains(',')) continue; } else { afterSegments = lastSegmentBeforeDot.Split(','); if (afterSegments.Length != parameters.Count) continue; } results.Add(j); } } return results; } private static ReadOnlyCollection GetSortedMethods(ReadOnlyCollection methods) => (from l in methods orderby l.ScopeEnum descending, l.ReferenceToLineNumbers.Count descending, l.Line.Length, l.Match.Name.Length, l.Match.Name select l).ToArray().AsReadOnly(); private static ReadOnlyCollection GetCollection(ILogger logger, string[] lines, ReadOnlyCollection sortedMethods) { List results = []; List check = sortedMethods.ToList(); foreach (Method method in sortedMethods) { logger.LogInformation($"{method.Match.Name} => {method.Parameters.Count}"); if (method.EndLine is null) continue; if (!check.Remove(method)) continue; MethodWith methodWith = GetMethodWith(lines, sortedMethods, check, method, method.EndLine.Value); results.Add(methodWith); } return results.AsReadOnly(); } private static MethodWith GetMethodWith(string[] lines, ReadOnlyCollection methods, List check, Method method, int methodEndLineValue) { MethodWith methodWith; List referenceToLineNumbers; MethodWith[] sortedReferences; Dictionary references = []; foreach (Method m in methods) { if (m.EndLine is null) continue; if (m == method) continue; referenceToLineNumbers = GetReferenceToLineNumbers(lines: lines, start: method.StartLine, end: methodEndLineValue, i: -1, search: m.Search, parameters: m.Parameters); if (referenceToLineNumbers.Count > 0) { if (!check.Remove(m)) continue; foreach (int i in referenceToLineNumbers) { if (references.ContainsKey(i)) continue; methodWith = GetMethodWith(lines, methods, check, m, m.EndLine.Value); references.Add(i, methodWith); break; } } } if (references.Count < 2) sortedReferences = (from l in references select l.Value).ToArray(); else sortedReferences = (from l in references orderby l.Key select l.Value).ToArray(); methodWith = new(EndLine: method.EndLine, FirstLine: method.FirstLine, Line: method.Line, Match: method.Match, Parameters: method.Parameters, References: new(sortedReferences), ReferenceToLineNumbers: method.ReferenceToLineNumbers, ScopeEnum: method.ScopeEnum, Search: method.Search, StartLine: method.StartLine); return methodWith; } private static bool WriteAllLines(string cSharpFile, string[] lines, ReadOnlyCollection collection) { bool result; if (Debugger.IsAttached) WriteDebug(collection); List results = []; ReadOnlyCollection methodLines = GetMethodLines(collection); int maxMethodLines = methodLines.Max(); for (int i = 0; i < maxMethodLines; i++) { if (methodLines.Contains(i)) continue; results.Add(lines[i]); } List nests = [true]; foreach (MethodWith methodWith in collection) { if (methodWith.EndLine is null) continue; AppendLines(results, nests, lines, methodWith, methodWith.EndLine.Value); } for (int i = maxMethodLines + 1; i < lines.Length; i++) results.Add(lines[i]); string text = File.ReadAllText(cSharpFile); string join = string.Join(Environment.NewLine, results); if (join == text) result = false; else { result = true; File.WriteAllText(cSharpFile, join); } return result; } private static void WriteDebug(ReadOnlyCollection collection) { List results = []; List nests = [true]; foreach (MethodWith methodWith in collection) AppendLines(results, nests, methodWith); File.WriteAllText(Path.Combine(".vscode", "helper", ".md"), string.Join(Environment.NewLine, results)); } private static void AppendLines(List results, List nests, MethodWith methodWith) { nests.Add(true); results.Add($" - {new string('#', nests.Count)} {methodWith.Match.Name} => {methodWith.Parameters.Count}"); foreach (MethodWith m in methodWith.References) AppendLines(results, nests, m); nests.RemoveAt(nests.Count - 1); } private static ReadOnlyCollection GetMethodLines(ReadOnlyCollection collection) { List results = []; List nests = [true]; foreach (MethodWith methodWith in collection) { if (methodWith.EndLine is null) continue; AppendLineNumbers(results, nests, methodWith, methodWith.EndLine.Value); } int[] distinct = results.Distinct().ToArray(); if (distinct.Length != results.Count) throw new Exception(); return new(results); } private static void AppendLineNumbers(List results, List nests, MethodWith methodWith, int methodWithEndLineValue) { nests.Add(true); for (int i = methodWith.StartLine; i < methodWithEndLineValue + 1; i++) results.Add(i); foreach (MethodWith m in methodWith.References) { if (m.EndLine is null) continue; AppendLineNumbers(results, nests, m, m.EndLine.Value); } nests.RemoveAt(nests.Count - 1); } private static void AppendLines(List results, List nests, string[] lines, MethodWith methodWith, int methodWithEndLineValue) { nests.Add(true); for (int i = methodWith.StartLine; i < methodWithEndLineValue + 1; i++) results.Add(lines[i]); foreach (MethodWith m in methodWith.References) { if (m.EndLine is null) continue; AppendLines(results, nests, lines, m, m.EndLine.Value); } nests.RemoveAt(nests.Count - 1); } }