diff --git a/src/CodeGenHelpers/ClassBuilder.cs b/src/CodeGenHelpers/ClassBuilder.cs index 9daec90..67cdad5 100644 --- a/src/CodeGenHelpers/ClassBuilder.cs +++ b/src/CodeGenHelpers/ClassBuilder.cs @@ -26,6 +26,7 @@ public sealed class ClassBuilder : BuilderBase private readonly GenericCollection _generics = new GenericCollection(); private readonly bool _isPartial; private DocumentationComment? _xmlDoc; + private Func? _propertiesOrderBy = null; internal ClassBuilder(string className, CodeBuilder codeBuilder, bool partial = true) { @@ -296,6 +297,12 @@ public DelegateBuilder AddNestedDelegate(string name, Accessibility? accessModif public string Build() => Builder.Build(); + public ClassBuilder DontSortPropertiesByName() + { + _propertiesOrderBy = _ => ""; + return this; + } + internal override void Write(in CodeWriter writer) { _xmlDoc?.Write(writer); @@ -341,34 +348,35 @@ internal override void Write(in CodeWriter writer) var hadOutput = false; hadOutput = InvokeBuilderWrite(_nestedDelegates, ref hadOutput, in writer); hadOutput = InvokeBuilderWrite(_events, ref hadOutput, writer); + var orderBy = _propertiesOrderBy ?? (x => x.Name); hadOutput = InvokeBuilderWrite( _properties.Where(x => x.FieldTypeValue == PropertyBuilder.FieldType.Const && x.IsStatic == false) - .OrderBy(x => x.Name), + .OrderBy(orderBy), ref hadOutput, writer, true); hadOutput = InvokeBuilderWrite( _properties.Where(x => x.FieldTypeValue == PropertyBuilder.FieldType.Const && x.IsStatic == true) - .OrderBy(x => x.Name), + .OrderBy(orderBy), ref hadOutput, writer, true); hadOutput = InvokeBuilderWrite( _properties.Where(x => x.FieldTypeValue == PropertyBuilder.FieldType.ReadOnly) - .OrderBy(x => x.Name), + .OrderBy(orderBy), ref hadOutput, writer, true); hadOutput = InvokeBuilderWrite( _properties.Where(x => x.FieldTypeValue == PropertyBuilder.FieldType.Default) - .OrderBy(x => x.Name), + .OrderBy(orderBy), ref hadOutput, writer, true); hadOutput = InvokeBuilderWrite(_constructors.OrderBy(x => x.Parameters.Count), ref hadOutput, writer); hadOutput = InvokeBuilderWrite( _properties.Where(x => x.FieldTypeValue == PropertyBuilder.FieldType.Property) - .OrderBy(x => x.Name), + .OrderBy(orderBy), ref hadOutput, writer); hadOutput = InvokeBuilderWrite( diff --git a/src/CodeGenHelpers/Extensions/RoslynExtensions.cs b/src/CodeGenHelpers/Extensions/RoslynExtensions.cs new file mode 100644 index 0000000..2b531ad --- /dev/null +++ b/src/CodeGenHelpers/Extensions/RoslynExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AvantiPoint.CodeGenHelpers.Extensions; + +public static class RoslynExtensions +{ + /// + /// Method to get the from a + /// + /// The symbol that you want that derives from + /// The + /// The that you want to convert to symbol. + /// The desired Symbol. + public static TSymbol? GetSymbol(this Compilation compilation, BaseTypeDeclarationSyntax declarationSyntax) + where TSymbol : ISymbol + { + var model = compilation.GetSemanticModel(declarationSyntax.SyntaxTree); + return (TSymbol?)model.GetDeclaredSymbol(declarationSyntax); + } + + /// + /// Returns the value of a named attribute as a . If no attribute with the given name is found, this method returns null. + /// + /// The attribute whose value is to be returned. + /// The name of the attribute to be returned. + /// The value of the named attribute, or null if no such attribute is found. + public static TypedConstant GetAttributeValueByName(this AttributeData attribute, string name) + { + return attribute.NamedArguments.SingleOrDefault(arg => arg.Key == name).Value; + } + + + /// + /// Returns the value of an attribute with the given name as a string. If no attribute with the specified name is found, the default value of "null" is returned. + /// + /// The attribute to inspect. + /// The name of the attribute value to retrieve. + /// The default value to return if the attribute value is null. + /// The attribute value as a string, or the specified default value if the attribute is not found or its value is null. + public static string GetAttributeValueByNameAsString(this AttributeData attribute, string name, string placeholder = "null") + { + var data = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value; + + return data.Value is null ? placeholder : data.Value.ToString(); + } +} diff --git a/src/CodeGenHelpers/Internals/AccessibilityExtensions.cs b/src/CodeGenHelpers/Internals/AccessibilityExtensions.cs index 9e7451f..15d9d4c 100644 --- a/src/CodeGenHelpers/Internals/AccessibilityExtensions.cs +++ b/src/CodeGenHelpers/Internals/AccessibilityExtensions.cs @@ -8,7 +8,7 @@ internal static class AccessibilityExtensions public static string? Code(this Accessibility accessModifier) => accessModifier switch { - Accessibility.ProtectedAndInternal => "protected internal", + Accessibility.ProtectedAndInternal => "private protected", Accessibility.ProtectedOrInternal => "protected internal", Accessibility.NotApplicable => null, _ => accessModifier.ToString().ToLower() diff --git a/tests/CodeGenHelpers.Tests/SampleCode/SampleClassWithPropertiesNotSorted.cs b/tests/CodeGenHelpers.Tests/SampleCode/SampleClassWithPropertiesNotSorted.cs new file mode 100644 index 0000000..e3adc6c --- /dev/null +++ b/tests/CodeGenHelpers.Tests/SampleCode/SampleClassWithPropertiesNotSorted.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; + +namespace CodeGenHelpers.SampleCode +{ + partial class SampleClassWithPropertiesNotSorted + { + public string PropZ; + + public string PropC; + + public string PropB; + + public string PropA; + + public string PropD; + } +} diff --git a/tests/CodeGenHelpers.Tests/SampleCode/SampleClassWithPropertiesSorted.cs b/tests/CodeGenHelpers.Tests/SampleCode/SampleClassWithPropertiesSorted.cs new file mode 100644 index 0000000..18f63fe --- /dev/null +++ b/tests/CodeGenHelpers.Tests/SampleCode/SampleClassWithPropertiesSorted.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; + +namespace CodeGenHelpers.SampleCode +{ + partial class SampleClassWithPropertiesSorted + { + public string PropA; + + public string PropB; + + public string PropC; + } +} diff --git a/tests/CodeGenHelpers.Tests/Tests/ClassTests.cs b/tests/CodeGenHelpers.Tests/Tests/ClassTests.cs index a77e49e..722ac6a 100644 --- a/tests/CodeGenHelpers.Tests/Tests/ClassTests.cs +++ b/tests/CodeGenHelpers.Tests/Tests/ClassTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using Microsoft.CodeAnalysis; +using Xunit; using Xunit.Abstractions; namespace CodeGenHelpers.Tests @@ -83,5 +84,35 @@ public void GeneratesClassInGlobalNamespace() MakeAssertion(builder); } + + [Fact] + public void SampleClassWithPropertiesSorted() + { + var builder = CodeBuilder.Create(Namespace) + .AddClass("SampleClassWithPropertiesSorted"); + + builder.AddProperty("PropC", Accessibility.Public).SetType(); + builder.AddProperty("PropB", Accessibility.Public).SetType(); + builder.AddProperty("PropA", Accessibility.Public).SetType(); + + MakeAssertion(builder); + } + + [Fact] + public void SampleClassWithPropertiesNotSorted() + { + var builder = CodeBuilder.Create(Namespace) + .AddClass("SampleClassWithPropertiesNotSorted"); + + builder.AddProperty("PropZ", Accessibility.Public).SetType(); + builder.AddProperty("PropC", Accessibility.Public).SetType(); + builder.AddProperty("PropB", Accessibility.Public).SetType(); + builder.AddProperty("PropA", Accessibility.Public).SetType(); + builder.AddProperty("PropD", Accessibility.Public).SetType(); + + builder.DontSortPropertiesByName(); + + MakeAssertion(builder); + } } } diff --git a/tests/CodeGenHelpers.Tests/Tests/RoslynExtensionTests.cs b/tests/CodeGenHelpers.Tests/Tests/RoslynExtensionTests.cs new file mode 100644 index 0000000..de7af4c --- /dev/null +++ b/tests/CodeGenHelpers.Tests/Tests/RoslynExtensionTests.cs @@ -0,0 +1,182 @@ +using System.Linq; +using AvantiPoint.CodeGenHelpers.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Xunit; + +namespace CodeGenHelpers.Tests; + +public class RoslynExtensionTests +{ + readonly string _placeholder = "null"; + readonly string _notFoundName = "NotFoundAttribute"; + + [Fact] + public void GetSymbol_ReturnsSymbol_ForGivenDeclarationSyntax() + { + // Arrange + var text = @" + namespace MyNamespace + { + public class MyClass {} + }"; + var tree = CSharpSyntaxTree.ParseText(text); + var compilation = CSharpCompilation.Create("MyCompilation", + references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + syntaxTrees: new[] { tree } + ); + + var root = tree.GetCompilationUnitRoot(); + var classDeclaration = root.DescendantNodes().OfType().Single(); + var expected = compilation.GetTypeByMetadataName("MyNamespace.MyClass"); + + // Act + var actual = compilation.GetSymbol(classDeclaration); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void GetAttributeValueByName_ReturnsTypedConstant() + { + // arrange + var syntaxTree = SyntaxFactory.ParseSyntaxTree(@" + using System; + using System.Collections.Generic; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + sealed class MyAttribute : Attribute + { + public MyAttribute(string arg1, string arg2) + { + } + }"); + + var compilation = CSharpCompilation.Create("MyCompilation", + references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + syntaxTrees: new[] { syntaxTree } + ); + var root = syntaxTree.GetCompilationUnitRoot(); + var @class = root.Members[0] as ClassDeclarationSyntax; + var symbol = compilation.GetSemanticModel(syntaxTree).GetDeclaredSymbol(@class!)!; + var attribute = symbol.GetAttributes().First(); + + var expected = typeof(TypedConstant); + + // act + var actual = RoslynExtensions.GetAttributeValueByName(attribute, "Inherited"); + + //// assert + Assert.IsType(expected, actual); + Assert.NotNull(actual.Value); + } + + [Fact] + public void GetAttributeValueByName_ReturnsValueNullIfNotFound() + { + // arrange + var syntaxTree = SyntaxFactory.ParseSyntaxTree(@" + using System; + using System.Collections.Generic; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + sealed class MyAttribute : Attribute + { + public MyAttribute(string arg1, string arg2) + { + } + }"); + + var compilation = CSharpCompilation.Create("MyCompilation", + references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + syntaxTrees: new[] { syntaxTree } + ); + + var root = syntaxTree.GetCompilationUnitRoot(); + var @class = root.DescendantNodes()?.OfType().FirstOrDefault(); + var symbol = compilation.GetSemanticModel(syntaxTree).GetDeclaredSymbol(@class); + var attribute = symbol.GetAttributes().First(); + + // act + var actual = RoslynExtensions.GetAttributeValueByName(attribute, _notFoundName); + + // assert + Assert.Null(actual.Value); + } + + [Fact] + public void GetAttributeValueByNameAsString_ReturnsString() + { + // arrange + var syntaxTree = SyntaxFactory.ParseSyntaxTree(@" + using System; + using System.Collections.Generic; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + sealed class MyAttribute : Attribute + { + public MyAttribute(Type type, string name) + { + Name = name; + Type = type; + } + + public Type Type { get; } + + public string Name { get; } + }"); + + var compilation = CSharpCompilation.Create("MyCompilation", + references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + syntaxTrees: new[] { syntaxTree } + ); + var root = syntaxTree.GetCompilationUnitRoot(); + var @class = root.DescendantNodes()?.OfType() + .Single(x => x.AttributeLists.Count > 0); + + var symbol = compilation.GetSemanticModel(syntaxTree).GetDeclaredSymbol(@class); + var attribute = symbol.GetAttributes().First(); + + var expected = "True"; + + // act + var actual = RoslynExtensions.GetAttributeValueByNameAsString(attribute, "AllowMultiple", "SomeDefaultValue"); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void GetAttributeValueByNameAsString_ReturnsNullIfNotFound() + { + // arrange + var syntaxTree = SyntaxFactory.ParseSyntaxTree(@" + using System; + using System.Collections.Generic; + + [ExcludeFromCodeCoverage] + [AttributeUsage(AttributeTargets.All)] + sealed class MyAttribute : Attribute + { + public MyAttribute() + { + } + }"); + + var compilation = CSharpCompilation.Create("MyCompilation", + references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + syntaxTrees: new[] { syntaxTree }); + var root = syntaxTree.GetCompilationUnitRoot(); + var @class = root.DescendantNodes()?.OfType().FirstOrDefault(); + var symbol = compilation.GetSemanticModel(syntaxTree).GetDeclaredSymbol(@class); + var attribute = symbol.GetAttributes().First(); + + //// act + var actual = RoslynExtensions.GetAttributeValueByNameAsString(attribute, _notFoundName); + + //// assert + Assert.Equal(_placeholder, actual); + } +}