Skip to content

Commit

Permalink
Merge branch 'dansiegel:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
vmachacek committed Oct 24, 2023
2 parents ac3c31f + 35cbdf3 commit acc757e
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 7 deletions.
18 changes: 13 additions & 5 deletions src/CodeGenHelpers/ClassBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public sealed class ClassBuilder : BuilderBase<ClassBuilder>
private readonly GenericCollection _generics = new GenericCollection();
private readonly bool _isPartial;
private DocumentationComment? _xmlDoc;
private Func<PropertyBuilder, string>? _propertiesOrderBy = null;

internal ClassBuilder(string className, CodeBuilder codeBuilder, bool partial = true)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
49 changes: 49 additions & 0 deletions src/CodeGenHelpers/Extensions/RoslynExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Method to get the <see cref="ISymbol"/> from a <see cref="BaseTypeDeclarationSyntax"/>
/// </summary>
/// <typeparam name="TSymbol">The symbol that you want that derives from <see cref="ISymbol"/></typeparam>
/// <param name="compilation">The <see cref="Compilation"/></param>
/// <param name="declarationSyntax">The <see cref="BaseTypeDeclarationSyntax"/>that you want to convert to symbol.</param>
/// <returns>The desired Symbol.</returns>
public static TSymbol? GetSymbol<TSymbol>(this Compilation compilation, BaseTypeDeclarationSyntax declarationSyntax)
where TSymbol : ISymbol
{
var model = compilation.GetSemanticModel(declarationSyntax.SyntaxTree);
return (TSymbol?)model.GetDeclaredSymbol(declarationSyntax);
}

/// <summary>
/// Returns the value of a named attribute as a <see cref="TypedConstant"/>. If no attribute with the given name is found, this method returns <c>null</c>.
/// </summary>
/// <param name="attribute">The attribute whose value is to be returned.</param>
/// <param name="name">The name of the attribute to be returned.</param>
/// <returns>The value of the named attribute, or <c>null</c> if no such attribute is found.</returns>
public static TypedConstant GetAttributeValueByName(this AttributeData attribute, string name)
{
return attribute.NamedArguments.SingleOrDefault(arg => arg.Key == name).Value;
}


/// <summary>
/// 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.
/// </summary>
/// <param name="attribute">The attribute to inspect.</param>
/// <param name="name">The name of the attribute value to retrieve.</param>
/// <param name="placeholder">The default value to return if the attribute value is null.</param>
/// <returns>The attribute value as a string, or the specified default value if the attribute is not found or its value is null.</returns>
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();
}
}
2 changes: 1 addition & 1 deletion src/CodeGenHelpers/Internals/AccessibilityExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;

namespace CodeGenHelpers.SampleCode
{
partial class SampleClassWithPropertiesNotSorted
{
public string PropZ;

public string PropC;

public string PropB;

public string PropA;

public string PropD;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;

namespace CodeGenHelpers.SampleCode
{
partial class SampleClassWithPropertiesSorted
{
public string PropA;

public string PropB;

public string PropC;
}
}
33 changes: 32 additions & 1 deletion tests/CodeGenHelpers.Tests/Tests/ClassTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Xunit;
using Microsoft.CodeAnalysis;
using Xunit;
using Xunit.Abstractions;

namespace CodeGenHelpers.Tests
Expand Down Expand Up @@ -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<string>();
builder.AddProperty("PropB", Accessibility.Public).SetType<string>();
builder.AddProperty("PropA", Accessibility.Public).SetType<string>();

MakeAssertion(builder);
}

[Fact]
public void SampleClassWithPropertiesNotSorted()
{
var builder = CodeBuilder.Create(Namespace)
.AddClass("SampleClassWithPropertiesNotSorted");

builder.AddProperty("PropZ", Accessibility.Public).SetType<string>();
builder.AddProperty("PropC", Accessibility.Public).SetType<string>();
builder.AddProperty("PropB", Accessibility.Public).SetType<string>();
builder.AddProperty("PropA", Accessibility.Public).SetType<string>();
builder.AddProperty("PropD", Accessibility.Public).SetType<string>();

builder.DontSortPropertiesByName();

MakeAssertion(builder);
}
}
}
182 changes: 182 additions & 0 deletions tests/CodeGenHelpers.Tests/Tests/RoslynExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -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<ClassDeclarationSyntax>().Single();
var expected = compilation.GetTypeByMetadataName("MyNamespace.MyClass");

// Act
var actual = compilation.GetSymbol<INamedTypeSymbol>(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<ClassDeclarationSyntax>().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<ClassDeclarationSyntax>()
.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<ClassDeclarationSyntax>().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);
}
}

0 comments on commit acc757e

Please # to comment.