diff --git a/Documentation/Synergy.Documentation.Tests/Architecture/Debt/Todos.Technical.Debt.verified.md b/Documentation/Synergy.Documentation.Tests/Architecture/Debt/Todos.Technical.Debt.verified.md new file mode 100644 index 0000000..8f11160 --- /dev/null +++ b/Documentation/Synergy.Documentation.Tests/Architecture/Debt/Todos.Technical.Debt.verified.md @@ -0,0 +1,18 @@ +# Technical Debt for Synergy.Contracts + +Total: 5 + +## [ApiDescription.cs](../../../Synergy.Documentation/Api/ApiDescription.cs) +- TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Duplicated class + +## [CodeFile.cs](../../../Synergy.Documentation/Code/CodeFile.cs) +- TODO: Marcin Celej [from: Marcin Celej on: 14-05-2023]: rename to SourceFile + +## [CodeFolder.cs](../../../Synergy.Documentation/Code/CodeFolder.cs) +- TODO: Marcin Celej [from: Marcin Celej on: 14-05-2023]: Rename to SourceFolder + +## [TodoPattern.cs](../../../Synergy.Documentation/Todos/Patterns/TodoPattern.cs) +- TODO: Marcin Celej [from: Marcin Celej on: 14-04-2023]: Add way to exclude some files from the scan - by path + +## [TodoExplorer.cs](../../../Synergy.Documentation/Todos/TodoExplorer.cs) +- TODO: Marcin Celej [from: Marcin Celej on: 30-04-2023]: Publish this library via nuget diff --git a/Documentation/Synergy.Documentation.Tests/Todos/Todos.cs b/Documentation/Synergy.Documentation.Tests/Architecture/Debt/Todos.cs similarity index 81% rename from Documentation/Synergy.Documentation.Tests/Todos/Todos.cs rename to Documentation/Synergy.Documentation.Tests/Architecture/Debt/Todos.cs index 0de81e2..2bd1bb5 100644 --- a/Documentation/Synergy.Documentation.Tests/Todos/Todos.cs +++ b/Documentation/Synergy.Documentation.Tests/Architecture/Debt/Todos.cs @@ -1,7 +1,7 @@ using Synergy.Documentation.Code; using Synergy.Documentation.Todos; -namespace Synergy.Documentation.Tests.Todos; +namespace Synergy.Documentation.Tests.Architecture; [UsesVerify] public class Todos @@ -10,7 +10,7 @@ public class Todos public async Task Generate() { var rootFolder = CodeFolder.Current() - .Up(2); + .Up(3); var technicalDebt = TodoExplorer.DebtFor("Synergy.Contracts", rootFolder); await Verifier diff --git a/Documentation/Synergy.Documentation.Tests/Architecture/Public/Api.cs b/Documentation/Synergy.Documentation.Tests/Architecture/Public/Api.cs new file mode 100644 index 0000000..52d5b08 --- /dev/null +++ b/Documentation/Synergy.Documentation.Tests/Architecture/Public/Api.cs @@ -0,0 +1,22 @@ +using Synergy.Documentation.Api; + +namespace Synergy.Documentation.Tests.Architecture.Public; + +[UsesVerify] +public class Api +{ + [Fact] + public async Task Generate() + { + // ARRANGE + var assembly = typeof(ApiDescription).Assembly; + + // ACT + var publicApi = ApiDescription.GenerateFor(assembly); + + // ASSERT + await Verifier.Verify(publicApi, "md") + .UseMethodName("of." + assembly.GetName() + .Name); + } +} \ No newline at end of file diff --git a/Documentation/Synergy.Documentation.Tests/Architecture/Public/Api.of.Synergy.Documentation.verified.md b/Documentation/Synergy.Documentation.Tests/Architecture/Public/Api.of.Synergy.Documentation.verified.md new file mode 100644 index 0000000..3e7d76c --- /dev/null +++ b/Documentation/Synergy.Documentation.Tests/Architecture/Public/Api.of.Synergy.Documentation.verified.md @@ -0,0 +1,109 @@ +# Synergy.Documentation + +## Todos.TodoExplorer (abstract class) + - TodoExplorer.DebtFor( + name: string, + from: CodeFolder, + currentPath: string [CallerFilePath, Optional], + patterns: params TodoPattern[] [ParamArray] + ) : string + +## Todos.Patterns.CsharpTodoPattern (record) : TodoPattern, IEquatable, IEquatable + - FileExtension: string { get; set; } + - Regex: Regex { get; set; } + - TodoExtractor: Func { get; set; } + - ctor() + +## Todos.Patterns.GherkinTodoPattern (record) : TodoPattern, IEquatable, IEquatable + - FileExtension: string { get; set; } + - Regex: Regex { get; set; } + - TodoExtractor: Func { get; set; } + - ctor() + +## Todos.Patterns.TextTodoPattern (record) : TodoPattern, IEquatable, IEquatable + - FileExtension: string { get; set; } + - Regex: Regex { get; set; } + - TodoExtractor: Func { get; set; } + - ctor() + +## Todos.Patterns.TodoPattern (abstract class) : IEquatable + - FileExtension: string { get; set; } + - Regex: Regex { get; set; } + - TodoExtractor: Func { get; set; } + +## Code.ClassReader (class) + - ctor() + - ClassReader.ReadMethodBody( + methodName: string, + sourceFilePath: string [CallerFilePath, Optional] + ) : string? [NullableContext] + +## Code.CodeFile (class) + - Extension: string { get; } + - FileName: string { get; } + - FileNameWithoutExtension: string { get; } + - FilePath: string { get; } + - Folder: CodeFolder { get; } + - ctor( + filePath: string + ) + - CodeFile.Current( + path: string [CallerFilePath, Optional] + ) : CodeFolder + - RelativeTo( + folder: CodeFolder + ) : CodeFile + - ToString() : string + +## Code.CodeFolder (class) + - Path: string { get; } + - ctor( + path: string + ) + - CodeFolder.Current( + path: string [CallerFilePath, Optional] + ) : CodeFolder + - File( + fileName: string + ) : CodeFile + - ToString() : string + - Up( + jumps: int [Optional] + ) : CodeFolder + +## Api.ApiDescription (abstract class) + - ApiDescription.For( + method: MethodInfo, + withAttributes: bool [Optional] + ) : string + - ApiDescription.GenerateFor( + assembly: Assembly + ) : string + - ApiDescription.GenerateFor( + types: IEnumerable, + description: StringBuilder? [Nullable, Optional], + assemblyName: string? [Nullable, Optional] + ) : string + - ApiDescription.GetTypeName( + method: MethodInfo + ) : string + - ApiDescription.GetTypeName( + parameter: ParameterInfo + ) : string + - ApiDescription.GetTypeName( + type: Type + ) : string + +## Api.ClassDocumentation (class) : Markdown+Document, IEnumerable, IEnumerable + - ctor( + type: Type + ) + - Append( + element: Markdown+IElement + ) : Markdown+Document + - Append( + newElements: IEnumerable + ) : Markdown+Document + - GetEnumerator() : IEnumerator + - ToString() : string + diff --git a/Documentation/Synergy.Documentation.Tests/Synergy.Documentation.Tests.csproj b/Documentation/Synergy.Documentation.Tests/Synergy.Documentation.Tests.csproj index 70634cb..dcddf03 100644 --- a/Documentation/Synergy.Documentation.Tests/Synergy.Documentation.Tests.csproj +++ b/Documentation/Synergy.Documentation.Tests/Synergy.Documentation.Tests.csproj @@ -12,6 +12,10 @@ Todos Todos.cs + + Api + Api.cs + diff --git a/Documentation/Synergy.Documentation.Tests/Todos/Todos.Technical.Debt.verified.md b/Documentation/Synergy.Documentation.Tests/Todos/Todos.Technical.Debt.verified.md deleted file mode 100644 index a934e73..0000000 --- a/Documentation/Synergy.Documentation.Tests/Todos/Todos.Technical.Debt.verified.md +++ /dev/null @@ -1,13 +0,0 @@ -# Technical Debt for Synergy.Contracts - -Total: 4 - -## [ApiDescription.cs](../../Synergy.Documentation/Api/ApiDescription.cs) -- TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Duplicated class -- TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Update to newest version - -## [TodoPattern.cs](../../Synergy.Documentation/Todos/Patterns/TodoPattern.cs) -- TODO: Marcin Celej [from: Marcin Celej on: 14-04-2023]: Add way to exclude some files from the scan - by path - -## [TodoExplorer.cs](../../Synergy.Documentation/Todos/TodoExplorer.cs) -- TODO: Marcin Celej [from: Marcin Celej on: 30-04-2023]: Publish this library via nuget diff --git a/Documentation/Synergy.Documentation/Api/ApiDescription.cs b/Documentation/Synergy.Documentation/Api/ApiDescription.cs index 75f2d90..451fd8f 100644 --- a/Documentation/Synergy.Documentation/Api/ApiDescription.cs +++ b/Documentation/Synergy.Documentation/Api/ApiDescription.cs @@ -1,77 +1,146 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using JetBrains.Annotations; +using Synergy.Catalogue; +using Synergy.Catalogue.Reflection; -namespace Synergy.Convention.Testing +namespace Synergy.Documentation.Api { - // TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Duplicated class public static class ApiDescription { - // TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Update to newest version - + static readonly BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public; + public static string GenerateFor(Assembly assembly) { - StringBuilder description = new StringBuilder(); + var description = new StringBuilder(); - string assemblyName = assembly.GetName().Name; + var assemblyName = assembly.GetName().Name; description.AppendLine($"# {assemblyName}"); description.AppendLine(); - foreach (Type type in assembly.GetTypes()) + return GenerateFor(assembly.GetTypes(), description, assemblyName); + } + + public static string GenerateFor(IEnumerable types, StringBuilder? description = null, string? assemblyName = null) + { + description ??= new StringBuilder(); + + foreach (var type in types) { if (type.IsPublic == false && type.IsNestedPublic == false) continue; - var gType = type.IsEnum ? " (enum)" : (type.IsValueType ? " (struct)" : ""); - var baseType = ApiDescription.GetBaseTypeName(type); - description.AppendLine($"## {type.FullName.Replace(assemblyName + ".", "")}{gType}{baseType}"); - foreach (var property in type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)) + var shortNamespace = GetShortenNamespace(type, assemblyName); + var typeName = GetShortTypeName(type); + var baseType = GetParents(type); + + description.AppendLine($"## {shortNamespace}{GetTypeName(type)}{typeName}{baseType}"); + + foreach (var property in type.GetProperties(bindingFlags).OrderBy(p => p.Name)) { + if (property.DeclaringType == typeof(Exception) || property.DeclaringType == typeof(Attribute)) + continue; + description.AppendLine($" - {GetPropertyName(property)}: {GetTypeName(property)}{GetAttributes(property)} {GetAccessors(property)}"); } - foreach (var field in type.GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)) + foreach (var field in type.GetFields(bindingFlags)) { if (field.Name == "value__" && type.IsEnum) continue; - - description.AppendLine($" - {ApiDescription.GetFieldName(field)}: {GetTypeName(field)}{GetAttributes(field)} (field)"); + + if (type.IsEnum) + { + description.AppendLine($" - {field.Name} = {(int)Enum.Parse(type, field.Name)}"); + } + else + { + description.AppendLine($" - {GetFieldName(field)}: {GetTypeName(field)}{GetAttributes(field)} (field)"); + } } if (type.IsEnum == false) { - foreach (var method in type.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)) + foreach (var constructor in type.GetConstructors(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)) { - if (method.IsSpecialName || method.DeclaringType == typeof(object) || method.DeclaringType == typeof(Exception)) + description.AppendLine($" - ctor({GetParametersOf(constructor)})"); + } + + foreach (var method in type.GetMethods(bindingFlags).OrderBy(m => m.Name)) + { + if (method.IsSpecialName || method.DeclaringType.In(typeof(object), typeof(Exception), typeof(Attribute))) continue; - + if (type.IsValueType && method.Name.In(nameof(Equals), nameof(GetHashCode), nameof(ToString))) continue; - var generics = method.GetGenericArguments(); - var gD = generics.Length == 0 ? "" : "<" + String.Join(", ", generics.Select(g => g.Name)) + ">"; - description.AppendLine($" - {GetMethodName(method)}{gD}({GetParametersOf(method)}) : {GetTypeName(method)}{GetAttributes(method)}"); + if (method.GetCustomAttribute() != null) + continue; + + if (type.IsRecord() && method.Name.In(nameof(ToString), nameof(GetHashCode), nameof(Equals), "$", "Deconstruct")) + continue; + + var methodDescription = For(method); + description.AppendLine($" - {methodDescription}"); } } description.AppendLine(); } - + return description.ToString(); } + public static string For(MethodInfo method, bool withAttributes = true) + { + var generics = method.GetGenericArguments(); + var gD = generics.Length == 0 ? "" : "<" + String.Join(", ", generics.Select(g => GetTypeName(g))) + ">"; + var attributes = withAttributes ? GetAttributes(method) : ""; + var methodDescription = $"{GetMethodName(method)}{gD}({GetParametersOf(method, withAttributes)}) : {GetTypeName(method)}{attributes}"; + return methodDescription; + } + + private static string GetShortenNamespace(Type type, string? assemblyName) + { + if (assemblyName == null) + return type.Namespace + "."; + + var shortNamespace = type.Namespace.Replace(assemblyName, "").TrimStart('.'); + if (shortNamespace.IsNullOrEmpty() == false) + shortNamespace += "."; + return shortNamespace; + } + + private static string GetShortTypeName(Type type) + { + var typeName = type.IsEnum ? " (enum)" : (type.IsValueType ? " (struct)" : " (class)"); + if (type.IsRecord()) + typeName = " (record)"; + if (typeof(Exception).IsAssignableFrom(type)) + typeName = " (exception)"; + if (typeof(Attribute).IsAssignableFrom(type)) + typeName = " (attribute)"; + if (type.IsAbstract) + typeName = " (abstract class)"; + if (type.IsInterface) + typeName = " (interface)"; + return typeName; + } + private static string GetAccessors(PropertyInfo property) { var canRead = property.GetGetMethod(false)?.IsPublic ?? false; var canWrite = property.GetSetMethod(false)?.IsPublic ?? false; - + if (canRead && canWrite) return "{ get; set; }"; - + if (canRead) return "{ get; }"; - + if (canWrite) return "{ set; }"; @@ -90,42 +159,66 @@ private static string GetPropertyName(PropertyInfo property) { if (property.IsStatic()) return $"{property.DeclaringType.Name}.{property.Name}"; - + return property.Name; } - public static bool IsStatic(this PropertyInfo source, bool nonPublic = false) + private static bool IsStatic(this PropertyInfo source, bool nonPublic = false) => source.GetAccessors(nonPublic).Any(x => x.IsStatic); - - [Pure] public static bool In(this T value, params T[] values) + + [Pure] + private static bool In(this T value, params T[] values) => values.Contains(value); - - [Pure] public static bool NotIn(this T value, params T[] values) + + [Pure] + private static bool NotIn(this T value, params T[] values) => value.In(values) == false; - - private static string GetMethodName(MethodInfo method) + + internal static string GetMethodName(MethodInfo method) { if (method.IsStatic) return $"{method.DeclaringType.Name}.{method.Name}"; - + return method.Name; } - private static string GetBaseTypeName(Type type) + private static string GetParents(Type type) { - if (type.BaseType == null) - return ""; - - if (type.BaseType == typeof(object)) + List parents = new List(); + + if (type.BaseType != null && type.BaseType != typeof(object) && type.IsValueType == false) + parents.Add(GetTypeName(type.BaseType)); + + parents.AddRange(type.GetInterfaces().Select(i => GetTypeName(i))); + + if (parents.Count == 0) return ""; - - if (type.IsValueType) + + return " : " + String.Join(", ", parents); + } + + private static string GetParametersOf(ConstructorInfo constructor) + { + var parameters = constructor.GetParameters(); + if (parameters.Any() == false) return ""; - - return " : " + type.BaseType.Name; + + var last = parameters.Last(); + StringBuilder description = new StringBuilder(); + foreach (ParameterInfo parameter in parameters) + { + description.AppendLine(); + description.Append($" {parameter.Name}: {GetTypeName(parameter)}{GetAttributes(parameter)}"); + if (parameter != last) + description.Append(","); + } + + description.AppendLine(); + description.Append(" "); + return description.ToString(); } - private static string GetParametersOf(MethodInfo method) + private static string GetParametersOf(MethodInfo method, bool withAttributes = true) { var parameters = method.GetParameters(); if (parameters.Any() == false) @@ -136,7 +229,8 @@ private static string GetParametersOf(MethodInfo method) foreach (ParameterInfo parameter in parameters) { description.AppendLine(); - description.Append($" {parameter.Name}: {GetTypeName(parameter)}{GetAttributes(parameter)}"); + var attributes = withAttributes ? GetAttributes(parameter) : ""; + description.Append($" {parameter.Name}: {GetTypeName(parameter)}{attributes}"); if (parameter != last) description.Append(","); } @@ -149,63 +243,79 @@ private static string GetParametersOf(MethodInfo method) private static string GetTypeName(PropertyInfo property) { var type = GetTypeName(property.PropertyType); - var nullable = property.GetCustomAttributes() - .Any(a => a.GetType() - .FullName == "System.Runtime.CompilerServices.NullableAttribute"); - + var nullable = IsMarkedAsNullable(property); if (nullable) - return type +"?"; - + return type + "?"; + return type; } + private static bool IsMarkedAsNullable(PropertyInfo p) + => new NullabilityInfoContext().Create(p).WriteState is NullabilityState.Nullable; + private static string GetTypeName(FieldInfo field) { var type = GetTypeName(field.FieldType); + var nullable = IsMarkedAsNullable(field); + if (nullable) + return type + "?"; + return type; } - private static string GetTypeName(MethodInfo method) + private static bool IsMarkedAsNullable(FieldInfo p) + => new NullabilityInfoContext().Create(p).WriteState is NullabilityState.Nullable; + + public static string GetTypeName(MethodInfo method) { var type = GetTypeName(method.ReturnType); - var nullable = method.GetCustomAttributes() - .Any(a => a.GetType() - .FullName == "System.Runtime.CompilerServices.NullableContextAttribute"); - + var nullable = IsMarkedAsNullable(method.ReturnParameter); if (nullable) - return type +"?"; - + return type + "?"; + return type; } - - private static string GetTypeName(ParameterInfo parameter) + + private static bool IsMarkedAsNullable(ParameterInfo p) + => new NullabilityInfoContext().Create(p).WriteState is NullabilityState.Nullable; + + public static string GetTypeName(ParameterInfo parameter) { var type = GetTypeName(parameter.ParameterType); - var nullable = parameter.GetCustomAttributes() - .Any(a => a.GetType() - .FullName == "System.Runtime.CompilerServices.NullableAttribute"); - + var nullable = IsMarkedAsNullable(parameter); var paramsArray = parameter.GetCustomAttribute(); - + if (paramsArray != null) type = "params " + type; - + var outAttribute = parameter.GetCustomAttribute(); - + if (outAttribute != null) type = "out " + type; - + if (nullable) - return type +"?"; - + return type.TrimEnd('?') + "?"; + return type; } - - private static string GetTypeName(Type type) + + public static string GetTypeName(Type type) { + if (Nullable.GetUnderlyingType(type) != null) + { + return $"{GetTypeName(Nullable.GetUnderlyingType(type))}?"; + } + + if (type.IsGenericType) + { + var arguments = type.GetGenericArguments(); + return + $"{type.Name.Substring(0, type.Name.IndexOf("`", StringComparison.Ordinal))}<{String.Join(", ", arguments.Select(a => GetTypeName(a)))}>"; + } + if (type == typeof(object)) return "object"; - + if (type == typeof(string)) return "string"; @@ -214,11 +324,20 @@ private static string GetTypeName(Type type) if (type == typeof(long)) return "long"; - + if (type == typeof(bool)) return "bool"; - - return type.Name; + + if (type == typeof(void)) + return "void"; + + if (type == typeof(decimal)) + return "decimal"; + + if (type.FullName == null) + return type.Name; + + return type.FullName.Substring(type.FullName.LastIndexOf('.') + 1); } private static string GetAttributes(MemberInfo member) @@ -236,8 +355,10 @@ private static string GetAttributes(ParameterInfo parameter) private static string GetAttributes(IEnumerable enumerable) { var attributes = enumerable.Where(a => a.GetType().Name.StartsWith("__") == false) - .Select(a => a.GetType().Name.Replace("Attribute", "")) - .ToList(); + .Where(a => a is not DebuggerStepThroughAttribute) + .Select(a => a.GetType().Name.Replace("Attribute", "")) + .ToList(); + if (attributes.Any() == false) return ""; diff --git a/Documentation/Synergy.Documentation/Synergy.Documentation.csproj b/Documentation/Synergy.Documentation/Synergy.Documentation.csproj index 78d8e40..7c09b59 100644 --- a/Documentation/Synergy.Documentation/Synergy.Documentation.csproj +++ b/Documentation/Synergy.Documentation/Synergy.Documentation.csproj @@ -26,8 +26,14 @@ - - Api\ReflectionExtensions.cs + + Extensions\StringExtensions.cs + + + Extensions\ReflectionExtensions.cs + + + Extensions\EnumerableExtensions.cs