diff --git a/test2/Reflector.Test/Reflector.Test.csproj b/test2/Reflector.Test/Reflector.Test.csproj new file mode 100644 index 0000000..41d9948 --- /dev/null +++ b/test2/Reflector.Test/Reflector.Test.csproj @@ -0,0 +1,38 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test2/Reflector.Test/ReflectorTests.cs b/test2/Reflector.Test/ReflectorTests.cs new file mode 100644 index 0000000..5bcff5d --- /dev/null +++ b/test2/Reflector.Test/ReflectorTests.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector.Test; + +public class SampleClass +{ + public int Number; + private string Text; + + public void SayHello() { } + public int GetNumber() => Number; +} + +public class SampleClass2 +{ + public int Number; + public string Text; + + public void SayHello() { } + public int GetNumber() => Number; + public void ExtraMethod() { } +} + +[TestFixture] +public class ReflectorOutput +{ + [Test] + public void ShowPrintStructure() + { + Reflector.PrintStructure(typeof(SampleClass)); + } + + [Test] + public void ShowDiffClasses() + { + Reflector.DiffClasses(typeof(SampleClass), typeof(SampleClass2)); + } +} \ No newline at end of file diff --git a/test2/Reflector.Test/stylecop.json b/test2/Reflector.Test/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/test2/Reflector.Test/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/test2/Reflector/Accessibility.cs b/test2/Reflector/Accessibility.cs new file mode 100644 index 0000000..56d0dc4 --- /dev/null +++ b/test2/Reflector/Accessibility.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +/// +/// Defines accessibility levels for a class, method, or field. +/// +public enum Accessibility +{ + /// + /// Public accessibility. + /// + Public, + + /// + /// Internal accessibility. + /// + Internal, + + /// + /// Protected accessibility. + /// + Protected, + + /// + /// Private accessibility. + /// + Private, +} diff --git a/test2/Reflector/ClassModel.cs b/test2/Reflector/ClassModel.cs new file mode 100644 index 0000000..44db38b --- /dev/null +++ b/test2/Reflector/ClassModel.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +/// +/// Represents a structural model of class. +/// +public class ClassModel +{ + /// + /// Initializes a new instance of the class. + /// + /// Class name. + /// Namespace of the class. + public ClassModel(string name, string namespaceName) + { + this.Name = name ?? throw new ArgumentNullException(nameof(name)); + this.Namespace = namespaceName; + this.GenericParameters = new List(); + this.ImplementedInterfaces = new List(); + this.Fields = new List(); + this.Methods = new List(); + this.NestedClasses = new List(); + } + + /// + /// Gets the class name. + /// + public string Name { get; } + + /// + /// Gets the namespace of the class. + /// + public string Namespace { get; } + + /// + /// Gets or sets the accessibility level of the class. + /// + public Accessibility Accessibility { get; set; } + + /// + /// Gets or sets a value indicating whether the class is static. + /// + public bool IsStatic { get; set; } + + /// + /// Gets or sets a value indicating whether the class is abstract. + /// + public bool IsAbstract { get; set; } + + /// + /// Gets or sets a value indicating whether the class is sealed. + /// + public bool IsSealed { get; set; } + + /// + /// Gets or sets the base class name. + /// + public string? BaseClass { get; set; } + + /// + /// Gets the generic parameters of the class. + /// + public IList GenericParameters { get; } + + /// + /// Gets the interfaces implemented by the class. + /// + public IList ImplementedInterfaces { get; } + + /// + /// Gets the fields declared in the class. + /// + public IList Fields { get; } + + /// + /// Gets the methods declared in the class. + /// + public IList Methods { get; } + + /// + /// Gets the nested classes declared in the class. + /// + public IList NestedClasses { get; } +} diff --git a/test2/Reflector/ClassModelBuilder.cs b/test2/Reflector/ClassModelBuilder.cs new file mode 100644 index 0000000..b760c3c --- /dev/null +++ b/test2/Reflector/ClassModelBuilder.cs @@ -0,0 +1,179 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +using System; +using System.Reflection; + +/// +/// Builds a ClassModel using reflection. +/// +public static class ClassModelBuilder +{ + /// + /// Builds a from a given . + /// + /// The type to analyze. + /// A representing the type. + public static ClassModel Build(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + var classModel = new ClassModel(type.Name, type.Namespace ?? string.Empty) + { + Accessibility = GetAccessibility(type), + IsAbstract = type.IsAbstract && !type.IsInterface, + IsSealed = type.IsSealed, + IsStatic = type.IsAbstract && type.IsSealed, + BaseClass = type.BaseType != null && type.BaseType != typeof(object) ? type.BaseType.Name : null, + }; + + foreach (var generic in type.GetGenericArguments()) + { + classModel.GenericParameters.Add(generic.Name); + } + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) + { + classModel.Fields.Add(new FieldModel(field.Name, field.FieldType.Name) + { + Accessibility = GetAccessibility(field), + IsStatic = field.IsStatic, + IsReadonly = field.IsInitOnly, + IsConst = field.IsLiteral, + }); + } + + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) + { + if (method.IsSpecialName) + { + continue; + } + + var methodModel = new MethodModel(method.Name, method.ReturnType.Name) + { + Accessibility = GetAccessibility(method), + IsStatic = method.IsStatic, + IsAbstract = method.IsAbstract, + IsVirtual = method.IsVirtual && !method.IsAbstract, + IsOverride = method.GetBaseDefinition().DeclaringType != method.DeclaringType, + }; + + foreach (var gen in method.GetGenericArguments()) + { + methodModel.GenericParameters.Add(gen.Name); + } + + foreach (var param in method.GetParameters()) + { + var modifier = string.Empty; + if (param.IsOut) + { + modifier = "out"; + } + else if (param.ParameterType.IsByRef) + { + modifier = "ref"; + } + + methodModel.Parameters.Add(new ParameterModel(param.Name, param.ParameterType.Name, modifier)); + } + + classModel.Methods.Add(methodModel); + } + + foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + { + classModel.NestedClasses.Add(Build(nested)); + } + + return classModel; + } + + /// + /// Return the accessibility of a type. + /// + private static Accessibility GetAccessibility(Type type) + { + if (type.IsPublic || type.IsNestedPublic) + { + return Accessibility.Public; + } + + if (type.IsNestedFamily) + { + return Accessibility.Protected; + } + + if (type.IsNestedAssembly) + { + return Accessibility.Internal; + } + + if (type.IsNestedPrivate) + { + return Accessibility.Private; + } + + return Accessibility.Public; + } + + /// + /// Return the accessibility of a field. + /// + private static Accessibility GetAccessibility(FieldInfo field) + { + if (field.IsPublic) + { + return Accessibility.Public; + } + + if (field.IsFamily) + { + return Accessibility.Protected; + } + + if (field.IsAssembly) + { + return Accessibility.Internal; + } + + if (field.IsPrivate) + { + return Accessibility.Private; + } + + return Accessibility.Public; + } + + /// + /// Return the accessibility of a method. + /// + private static Accessibility GetAccessibility(MethodInfo method) + { + if (method.IsPublic) + { + return Accessibility.Public; + } + + if (method.IsFamily) + { + return Accessibility.Protected; + } + + if (method.IsAssembly) + { + return Accessibility.Internal; + } + + if (method.IsPrivate) + { + return Accessibility.Private; + } + + return Accessibility.Public; + } +} diff --git a/test2/Reflector/FieldModel.cs b/test2/Reflector/FieldModel.cs new file mode 100644 index 0000000..7d28bc8 --- /dev/null +++ b/test2/Reflector/FieldModel.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +/// +/// Represents a field declared in a class. +/// +public class FieldModel +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the field. + /// The type of the field (e.g., int, string). + public FieldModel(string name, string typeName) + { + this.Name = name; + this.TypeName = typeName; + } + + /// + /// Gets the name of the field. + /// + public string Name { get; } + + /// + /// Gets the type of the field as a string. + /// + public string TypeName { get; } + + /// + /// Gets or sets the accessibility of the field (public, private, etc.). + /// + public Accessibility Accessibility { get; set; } + + /// + /// Gets or sets a value indicating whether the field is static. + /// + public bool IsStatic { get; set; } + + /// + /// Gets or sets a value indicating whether the field is readonly. + /// + public bool IsReadonly { get; set; } + + /// + /// Gets or sets a value indicating whether the field is const. + /// + public bool IsConst { get; set; } +} diff --git a/test2/Reflector/MethodModel.cs b/test2/Reflector/MethodModel.cs new file mode 100644 index 0000000..62cbd98 --- /dev/null +++ b/test2/Reflector/MethodModel.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +/// +/// Represents a method declared in a class. +/// +public class MethodModel +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the method. + /// The return type of the method (e.g., void, int). + public MethodModel(string name, string returnTypeName) + { + this.Name = name; + this.ReturnTypeName = returnTypeName; + this.Parameters = new List(); + this.GenericParameters = new List(); + } + + /// + /// Gets the name of the method. + /// + public string Name { get; } + + /// + /// Gets the return type of the method as a string. + /// + public string ReturnTypeName { get; } + + /// + /// Gets or sets the accessibility of the method (public, private, etc.). + /// + public Accessibility Accessibility { get; set; } + + /// + /// Gets or sets a value indicating whether the method is static. + /// + public bool IsStatic { get; set; } + + /// + /// Gets or sets a value indicating whether the method is abstract. + /// + public bool IsAbstract { get; set; } + + /// + /// Gets or sets a value indicating whether the method is virtual. + /// + public bool IsVirtual { get; set; } + + /// + /// Gets or sets a value indicating whether the method overrides a base method. + /// + public bool IsOverride { get; set; } + + /// + /// Gets the generic type parameters of the method. + /// + public List GenericParameters { get; } + + /// + /// Gets the parameters of the method. + /// + public List Parameters { get; } +} diff --git a/test2/Reflector/ParameterModel.cs b/test2/Reflector/ParameterModel.cs new file mode 100644 index 0000000..e2fb804 --- /dev/null +++ b/test2/Reflector/ParameterModel.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +/// +/// Represents a parameter of a method. +/// +public class ParameterModel +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the parameter. + /// The type of the parameter (e.g., int, string). + /// The modifier of the parameter (ref, out, in) or empty string if none. + public ParameterModel(string name, string typeName, string modifier = "") + { + this.Name = name; + this.TypeName = typeName; + this.Modifier = modifier; + } + + /// + /// Gets the name of the parameter. + /// + public string Name { get; } + + /// + /// Gets the type of the parameter as a string. + /// + public string TypeName { get; } + + /// + /// Gets or sets the modifier of the parameter. + /// + public string Modifier { get; set; } +} diff --git a/test2/Reflector/Reflector.cs b/test2/Reflector/Reflector.cs new file mode 100644 index 0000000..b2640df --- /dev/null +++ b/test2/Reflector/Reflector.cs @@ -0,0 +1,301 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace Reflector; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +/// +/// Provides simple methods for reflecting, generating, and comparing C# class structures. +/// +public static class Reflector +{ + /// + /// Generates a C# file describing the given class type. + /// + /// The type to generate the structure from. + public static void PrintStructure(Type someClass) + { + ArgumentNullException.ThrowIfNull(someClass); + + var model = ClassModelBuilder.Build(someClass); + var fileName = model.Name + ".cs"; + var text = string.Empty; + + if (!string.IsNullOrEmpty(model.Namespace)) + { + text += "namespace " + model.Namespace + ";\n\n"; + } + + text += GetClassCode(model, 0); + + File.WriteAllText(fileName, text); + } + + /// + /// Compares two types and prints the differences between their fields and methods. + /// + /// The first type. + /// The second type. + public static void DiffClasses(Type a, Type b) + { + ArgumentNullException.ThrowIfNull(a); + ArgumentNullException.ThrowIfNull(b); + + var classA = ClassModelBuilder.Build(a); + var classB = ClassModelBuilder.Build(b); + + Console.WriteLine("=== Comparing " + classA.Name + " and " + classB.Name + " ===\n"); + + Console.WriteLine("--- Fields ---"); + foreach (var f in classA.Fields) + { + if (classB.Fields.All(fb => fb.Name != f.Name)) + { + Console.WriteLine("Only in first: " + f.TypeName + " " + f.Name); + } + } + + foreach (var f in classB.Fields) + { + if (classA.Fields.All(fa => fa.Name != f.Name)) + { + Console.WriteLine("Only in second: " + f.TypeName + " " + f.Name); + } + } + + foreach (var f in classA.Fields) + { + var fb = classB.Fields.FirstOrDefault(x => x.Name == f.Name); + if (fb != null && !AreFieldsEqual(f, fb)) + { + Console.WriteLine("Different field: " + f.Name + " first(" + f.TypeName + ") second(" + fb.TypeName + ")"); + } + } + + Console.WriteLine("\n--- Methods ---"); + foreach (var m in classA.Methods) + { + if (!classB.Methods.Any(mb => mb.Name == m.Name && ParametersMatch(m.Parameters, mb.Parameters))) + { + Console.WriteLine("Only in first: " + m.Name + "()"); + } + } + + foreach (var m in classB.Methods) + { + if (!classA.Methods.Any(ma => ma.Name == m.Name && ParametersMatch(ma.Parameters, m.Parameters))) + { + Console.WriteLine("Only in second: " + m.Name + "()"); + } + } + + foreach (var m in classA.Methods) + { + var mb = classB.Methods.FirstOrDefault(x => x.Name == m.Name); + if (mb != null && !AreMethodsEqual(m, mb)) + { + Console.WriteLine("Different method: " + m.Name + "() first(" + m.ReturnTypeName + ") second(" + mb.ReturnTypeName + ")"); + } + } + + Console.WriteLine("=== End of comparison ===\n"); + } + + private static string GetClassCode(ClassModel model, int indentLevel) + { + string indent = new(' ', indentLevel * 4); + var code = string.Empty; + + var modifiers = GetAccessibilityString(model.Accessibility); + if (model.IsStatic) + { + modifiers += " static"; + } + else if (model.IsAbstract) + { + modifiers += " abstract"; + } + else if (model.IsSealed) + { + modifiers += " sealed"; + } + + var generics = model.GenericParameters.Count > 0 ? "<" + string.Join(", ", model.GenericParameters) + ">" : string.Empty; + var baseClass = !string.IsNullOrEmpty(model.BaseClass) ? " : " + model.BaseClass : string.Empty; + + code += indent + modifiers + " class " + model.Name + generics + baseClass + "\n"; + code += indent + "{\n"; + + foreach (var field in model.Fields) + { + var fieldMod = GetAccessibilityString(field.Accessibility); + if (field.IsStatic) + { + fieldMod += " static"; + } + + if (field.IsReadonly) + { + fieldMod += " readonly"; + } + + if (field.IsConst) + { + fieldMod += " const"; + } + + code += indent + " " + fieldMod + " " + field.TypeName + " " + field.Name + ";\n"; + } + + foreach (var method in model.Methods) + { + var methodMod = GetAccessibilityString(method.Accessibility); + if (method.IsStatic) + { + methodMod += " static"; + } + + if (method.IsAbstract) + { + methodMod += " abstract"; + } + + if (method.IsVirtual) + { + methodMod += " virtual"; + } + + if (method.IsOverride) + { + methodMod += " override"; + } + + var methodGenerics = method.GenericParameters.Count > 0 ? "<" + string.Join(", ", method.GenericParameters) + ">" : string.Empty; + var parameters = string.Empty; + for (var i = 0; i < method.Parameters.Count; i++) + { + var p = method.Parameters[i]; + if (!string.IsNullOrEmpty(p.Modifier)) + { + parameters += p.Modifier + " "; + } + + parameters += p.TypeName + " " + p.Name; + if (i < method.Parameters.Count - 1) + { + parameters += ", "; + } + } + + code += indent + " " + methodMod + " " + method.ReturnTypeName + " " + method.Name + methodGenerics + "(" + parameters + ")\n"; + code += indent + " {\n"; + + if (method.ReturnTypeName != "void") + { + code += indent + " return " + GetDefaultValue(method.ReturnTypeName) + ";\n"; + } + + code += indent + " }\n"; + } + + foreach (var nested in model.NestedClasses) + { + code += GetClassCode(nested, indentLevel + 1); + } + + code += indent + "}\n\n"; + + return code; + } + + private static string GetAccessibilityString(Accessibility access) + { + return access switch + { + Accessibility.Public => "public", + Accessibility.Private => "private", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + _ => "public", + }; + } + + private static string GetDefaultValue(string typeName) + { + return typeName switch + { + "int" or "short" or "long" or "byte" or "sbyte" or "ushort" or "uint" or "ulong" or "float" or "double" or "decimal" => "0", + "bool" => "false", + "char" => "'\\0'", + "string" => "string.Empty", + _ => "null", + }; + } + + private static bool AreFieldsEqual(FieldModel a, FieldModel b) + { + return a.TypeName == b.TypeName && + a.Accessibility == b.Accessibility && + a.IsStatic == b.IsStatic && + a.IsReadonly == b.IsReadonly && + a.IsConst == b.IsConst; + } + + private static bool AreMethodsEqual(MethodModel a, MethodModel b) + { + if (a.ReturnTypeName != b.ReturnTypeName || + a.Accessibility != b.Accessibility || + a.IsStatic != b.IsStatic || + a.IsAbstract != b.IsAbstract || + a.IsVirtual != b.IsVirtual || + a.IsOverride != b.IsOverride || + a.GenericParameters.Count != b.GenericParameters.Count || + a.Parameters.Count != b.Parameters.Count) + { + return false; + } + + for (var i = 0; i < a.Parameters.Count; i++) + { + if (a.Parameters[i].TypeName != b.Parameters[i].TypeName || + a.Parameters[i].Name != b.Parameters[i].Name || + a.Parameters[i].Modifier != b.Parameters[i].Modifier) + { + return false; + } + } + + for (var i = 0; i < a.GenericParameters.Count; i++) + { + if (a.GenericParameters[i] != b.GenericParameters[i]) + { + return false; + } + } + + return true; + } + + private static bool ParametersMatch(List a, List b) + { + if (a.Count != b.Count) + { + return false; + } + + for (var i = 0; i < a.Count; i++) + { + if (a[i].TypeName != b[i].TypeName) + { + return false; + } + } + + return true; + } +} diff --git a/test2/Reflector/Reflector.csproj b/test2/Reflector/Reflector.csproj new file mode 100644 index 0000000..50249ce --- /dev/null +++ b/test2/Reflector/Reflector.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/test2/Reflector/stylecop.json b/test2/Reflector/stylecop.json new file mode 100644 index 0000000..9bb4698 --- /dev/null +++ b/test2/Reflector/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/test2/test2.sln b/test2/test2.sln new file mode 100644 index 0000000..e46fcd8 --- /dev/null +++ b/test2/test2.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reflector", "Reflector\Reflector.csproj", "{06A2A92F-D0A6-4B1D-9CDE-5CE7F82AA1D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reflector.Test", "Reflector.Test\Reflector.Test.csproj", "{876DF0B0-FD06-4466-9DF0-2EF67C524974}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06A2A92F-D0A6-4B1D-9CDE-5CE7F82AA1D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06A2A92F-D0A6-4B1D-9CDE-5CE7F82AA1D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06A2A92F-D0A6-4B1D-9CDE-5CE7F82AA1D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06A2A92F-D0A6-4B1D-9CDE-5CE7F82AA1D9}.Release|Any CPU.Build.0 = Release|Any CPU + {876DF0B0-FD06-4466-9DF0-2EF67C524974}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {876DF0B0-FD06-4466-9DF0-2EF67C524974}.Debug|Any CPU.Build.0 = Debug|Any CPU + {876DF0B0-FD06-4466-9DF0-2EF67C524974}.Release|Any CPU.ActiveCfg = Release|Any CPU + {876DF0B0-FD06-4466-9DF0-2EF67C524974}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal