AArnott / CodeGeneration.Roslyn

Assists in performing Roslyn-based code generation during a build.
Microsoft Public License
408 stars 59 forks source link

Hot to add a property to a class? #185

Closed ZeeOcho closed 4 years ago

ZeeOcho commented 4 years ago

Hi,

first of all: thank you for this project and the good work! I really like it.

I would like some help on how to add a property to a class, because I am not sure where I am going wrong. I followed the tutorial and got the DuplicateWithSuffixGenerator to work successfully in my project (it is a netstandard project).

What I want to do is the following:

    [GeneratePropertyForType(typeof(StringBuilder))]
    public class GenTest1
    {
        public GenTest1()
        {
            //If the code generation works, it should generate a property with type and name StringBuilder
            //this.StringBuilder = new StringBuilder();
        }
    }

This is the Attribute an IRichCodeGenerator definition I am using:

    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
    [CodeGenerationAttribute(typeof(PropertyGenerator))]
    public class GeneratePropertyForTypeAttribute : Attribute
    {
        public Type PropertyType { get; }

        public GeneratePropertyForTypeAttribute(Type propertyType)
        {
            PropertyType = propertyType;
        }
    }
    public class PropertyGenerator : IRichCodeGenerator
    {
        private AttributeData AttributeData { get; }
        public string TypeOfGeneratedProperty { get; }

        public PropertyGenerator(AttributeData attributeData)
        {
            AttributeData = attributeData;
            TypeOfGeneratedProperty = AttributeData.ConstructorArguments[0].Value.ToString();
        }

        public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
        {
            //not called
            return Task.FromResult(new SyntaxList<MemberDeclarationSyntax>());
        }

        public Task<RichGenerationResult> GenerateRichAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
        {
            try
            {
                var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
                var property = SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(TypeOfGeneratedProperty), TypeOfGeneratedProperty.Split('.').Last())
                    .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
                    .AddAccessorListAccessors(SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)))
                    .AddAccessorListAccessors(SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)))
                    .NormalizeWhitespace();
                var result = new RichGenerationResult
                {
                    Members = new SyntaxList<MemberDeclarationSyntax>().Add(applyToClass.AddMembers(property))
                };
                return Task.FromResult(result);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
    }

I know the generator is called because throwing an Exception in GenerateRichAsync shows up in build log, and the GenTest1...generated.cs file looks good:

// ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------

using CodeGenerator; using System; using System.Collections.Generic; using System.Text;

[GeneratePropertyForType(typeof(StringBuilder))] public class GenTest1 { public GenTest1() { //this.StringBuilder = new StringBuilder(); }

public System.Text.StringBuilder StringBuilder
{
    get;
    set;
}

}

However, if I try to uncomment the access to the StringBuilder-Property, I get an error saying it does not exist (in VS). What am I missing?

Thank you for your time and input!

Edit: Sorted out my Syntax game, code generation itself looks fine now.

hypervtechnics commented 4 years ago

Did you declare the class as partial?

ZeeOcho commented 4 years ago

Did you declare the class as partial?

No I didn't. Thinking about your reply I think I didn't correctly understand how the CG process works. I thought I could "alter" the source code, but I have to think more in the lines of adding source files to the compilation unit, correct?

I'll try partial declaration. How would I go about multiple instances of my attribute on the same class, i.e. wanting to have multiple properties generated this way: would I have to generate a partial class for each property, or is there a way to "bundle" them?

Edit: I altered everything to partial stuff. Generated file looks good, but VS still "can't see" the generated property. Pasting code:

    [GeneratePropertyForType(typeof(StringBuilder))]
    public partial class GenTest1
    {
        public GenTest1()
        {
            //this.StringBuilder = new StringBuilder();
        }
    }
                var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
                if (!ClassIsPartial(applyToClass, progress))
                    return EmptyResult;
                var property = SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(TypeOfGeneratedProperty), TypeOfGeneratedProperty.Split('.').Last())
                    .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
                    .AddAccessorListAccessors(SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)))
                    .AddAccessorListAccessors(SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)));
                var partialClass = applyToClass.WithMembers(new SyntaxList<MemberDeclarationSyntax>().Add(property)).NormalizeWhitespace();
                var result = new RichGenerationResult
                {
                    Members = new SyntaxList<MemberDeclarationSyntax>().Add(partialClass)
                };
                return Task.FromResult(result);

// ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------

using CodeGenerator; using System; using System.Collections.Generic; using System.Text;

[GeneratePropertyForType(typeof(StringBuilder))] public partial class GenTest1 { public System.Text.StringBuilder StringBuilder { get; set; } }

ZeeOcho commented 4 years ago

Digging further, I am at least able to identify my problem. In my generated file (see above), there is no namespace, which most probably is the reason that the generated partial class is not found.

The generated file from the example contains the corresponding namespace.

The only difference is that I am using the IRichCodeGenerator interface. What do I have to do to make it add the namespace?

amis92 commented 4 years ago

IRichCodeGenerator docs

Well for your case it seems you really don't need any of the Rich additions, so I'd advise you to just use ICodeGenerator instead.

Rich variant allows you to add using declarations, change namespaces etc. - but it's Result.Members is essentially a list of root compilation unit members. So, for your specific instance, you'll need to either create the namespace syntax needed and add your class as a member of it, or reuse the Namespace syntax in which your context.ProcessingNode is.

ZeeOcho commented 4 years ago

Thank you for the explanation. I read the documentation, but did not realize the effect of the differences. I switched back to ICodeGenerator , and everything is working now. The only reason for me to try the IRichCodeGenerator in the first place was because I missed making the class partial (result from incomplete understandig of how the code generation works), so I the rich generator was necessary when enhancing existing types. My proof of concept works now, thank you for your help! I intend to use it at work to simplify some IoC-Injection-Boilerplate-Code.

For the sake of completeness, here is my working code:


    [CodeGenerationAttribute(typeof(PropertyGenerator))]
    public class GeneratePropertyForTypeAttribute : Attribute
    {
        public Type PropertyType { get; }

        public GeneratePropertyForTypeAttribute(Type propertyType)
        {
            PropertyType = propertyType;
        }
    }
    public class PropertyGenerator : ICodeGenerator
    {
        private string TypeOfGeneratedProperty { get; }

        public PropertyGenerator(AttributeData attributeData)
        {
            TypeOfGeneratedProperty = attributeData.ConstructorArguments[0].Value.ToString();
        }

        public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
        {
            try
            {
                var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
                if (!ClassIsPartial(applyToClass, progress))
                    return Task.FromResult(new SyntaxList<MemberDeclarationSyntax>());

                var property = SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(TypeOfGeneratedProperty), TypeOfGeneratedProperty.Split('.').Last())
                    .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
                    .AddAccessorListAccessors(SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)))
                    .AddAccessorListAccessors(SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)));

                var partialClass = applyToClass
                                    .WithMembers(new SyntaxList<MemberDeclarationSyntax>().Add(property))
                                    .WithAttributeLists(new SyntaxList<AttributeListSyntax>())
                                    .WithModifiers(new SyntaxTokenList().Add(SyntaxFactory.Token(SyntaxKind.PartialKeyword)))
                                    .NormalizeWhitespace();
                var result = new SyntaxList<MemberDeclarationSyntax>().Add(partialClass);
                return Task.FromResult(result);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        private bool ClassIsPartial(ClassDeclarationSyntax applyToClass, IProgress<Diagnostic> progress)
        {
            if (!applyToClass.Modifiers.Any(SyntaxKind.PartialKeyword))
            {
                progress.Report(
                    Diagnostic.Create(
                        new DiagnosticDescriptor("CA1001", "Cannot apply attribute to non-partial class", "Make class partial", "Microsoft.Design", DiagnosticSeverity.Error, true)
                        , Location.Create(applyToClass.SyntaxTree, applyToClass.Span)));
                return false;
            }
            return true;
        }
    }`

(I think closing this should be ok, since it is resolved).