From 1368788ce566dadc78abaed1304a282d7b525997 Mon Sep 17 00:00:00 2001 From: Adriano Carlos Verona Date: Mon, 29 Apr 2024 05:12:27 -0400 Subject: [PATCH] adds a public property for each primary contructor parameter (#273) --- .../Tests/Unit/TypeTests.Records.cs | 47 +++++++- Cecilifier.Core/AST/TypeDeclarationVisitor.cs | 12 +-- .../PrimaryConstructor.Generator.cs | 101 ++++++++++++++++++ .../CodeGeneration/Property.Generator.cs | 4 +- .../CodeGeneration/Record.Generator.cs | 12 +++ 5 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 Cecilifier.Core/CodeGeneration/PrimaryConstructor.Generator.cs create mode 100644 Cecilifier.Core/CodeGeneration/Record.Generator.cs diff --git a/Cecilifier.Core.Tests/Tests/Unit/TypeTests.Records.cs b/Cecilifier.Core.Tests/Tests/Unit/TypeTests.Records.cs index 47270ab3..b51bef76 100644 --- a/Cecilifier.Core.Tests/Tests/Unit/TypeTests.Records.cs +++ b/Cecilifier.Core.Tests/Tests/Unit/TypeTests.Records.cs @@ -1,10 +1,11 @@ +using System; using Cecilifier.Core.Extensions; using NUnit.Framework; namespace Cecilifier.Core.Tests.Tests.Unit; -[TestFixture] -public partial class TypeTests +[TestFixture(Category = "TypeTests")] +public class RecordTests : CecilifierUnitTestBase { [TestCase("struct")] [TestCase("class")] @@ -37,4 +38,46 @@ public void RecordType_Implements_IEquatable(string classOrStruct) \s+assembly.MainModule.Types.Add\(\k\); """)); } + + [Test] + public void PrimaryConstructorParameters_AreMappedToPublicProperties() + { + var result = RunCecilifier("public record TheRecord(int Value, TheRecord Parent, char Ch = '?');"); + + var cecilifiedCode = result.GeneratedCode.ReadToEnd(); + Span ranges = stackalloc Range[2]; + + foreach (var pair in new[] { "Value:assembly.MainModule.TypeSystem.Int32", @"Parent:rec_theRecord_\d+", "Ch:assembly.MainModule.TypeSystem.Char" }) + { + var expected = pair.AsSpan(); + var splitted = expected.Split(ranges, ':', StringSplitOptions.TrimEntries); + Assert.That(splitted, Is.EqualTo(2)); + + var propertyName = expected[ranges[0]]; + var propertyType = expected[ranges[1]]; + Assert.That( + cecilifiedCode, + Does.Match($""" + //Property: {propertyName} \(primary constructor\) + \s+var (?prop_{Char.ToLower(propertyName[0])}{propertyName.Slice(1)}_\d+) = new PropertyDefinition\("{propertyName}", PropertyAttributes.None, {propertyType}\); + \s+rec_theRecord_\d+.Properties.Add\(\k\); + """)); + + // ensure a method was generated for the `get` accessor + Assert.That(cecilifiedCode, Does.Match($""" + //{propertyName} getter + \s+var (m_get{propertyName}_\d+) = new MethodDefinition\("get_{propertyName}", (MethodAttributes\.)Public \| \2HideBySig \| \2SpecialName, {propertyType}\); + \s+rec_theRecord_\d+.Methods.Add\(\1\); + """)); + + // ensure a method was generated for the `init` accessor + Assert.That(cecilifiedCode, Does.Match($""" + //{propertyName} init + \s+var (m_set{propertyName}_\d+) = new MethodDefinition\("set_{propertyName}", (MethodAttributes\.)Public \| \2HideBySig \| \2SpecialName, new RequiredModifierType\(.+ImportReference\(typeof\(.+IsExternalInit\)\), .+Void\)\); + \s+var (p_value_\d+) = new ParameterDefinition\("value", ParameterAttributes.None, {propertyType}\); + \s+\1.Parameters.Add\(\3\); + \s+rec_theRecord_\d+.Methods.Add\(\1\); + """)); + } + } } diff --git a/Cecilifier.Core/AST/TypeDeclarationVisitor.cs b/Cecilifier.Core/AST/TypeDeclarationVisitor.cs index 8d4ceca3..08bbf2f8 100644 --- a/Cecilifier.Core/AST/TypeDeclarationVisitor.cs +++ b/Cecilifier.Core/AST/TypeDeclarationVisitor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Cecilifier.Core.CodeGeneration; using Cecilifier.Core.Extensions; using Cecilifier.Core.Mappings; using Cecilifier.Core.Misc; @@ -62,12 +63,11 @@ public override void VisitRecordDeclaration(RecordDeclarationSyntax node) using var _ = LineInformationTracker.Track(Context, node); var definitionVar = Context.Naming.Type(node); var recordSymbol = Context.SemanticModel.GetDeclaredSymbol(node).EnsureNotNull(); - using (Context.DefinitionVariables.WithCurrent(recordSymbol.ContainingSymbol.FullyQualifiedName(false), node.Identifier.ValueText, VariableMemberKind.Type, definitionVar)) - { - HandleTypeDeclaration(node, definitionVar); - base.VisitRecordDeclaration(node); - EnsureCurrentTypeHasADefaultCtor(node, definitionVar); - } + using var variable = Context.DefinitionVariables.WithCurrent(recordSymbol.ContainingSymbol.FullyQualifiedName(false), node.Identifier.ValueText, VariableMemberKind.Type, definitionVar); + HandleTypeDeclaration(node, definitionVar); + base.VisitRecordDeclaration(node); + + RecordGenerator.AddSyntheticMembers(Context, definitionVar, node); } public override void VisitEnumDeclaration(EnumDeclarationSyntax node) diff --git a/Cecilifier.Core/CodeGeneration/PrimaryConstructor.Generator.cs b/Cecilifier.Core/CodeGeneration/PrimaryConstructor.Generator.cs new file mode 100644 index 00000000..9c6f1918 --- /dev/null +++ b/Cecilifier.Core/CodeGeneration/PrimaryConstructor.Generator.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Cecilifier.Core.AST; +using Cecilifier.Core.Extensions; +using Cecilifier.Core.Mappings; +using Cecilifier.Core.Misc; +using Cecilifier.Core.Naming; +using Cecilifier.Core.Variables; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mono.Cecil.Cil; + +namespace Cecilifier.Core.CodeGeneration; + +public class PrimaryConstructorGenerator +{ + internal static void AddPropertiesFrom(IVisitorContext context, string typeDefinitionVariable, TypeDeclarationSyntax type) + { + if (type.ParameterList is null) + return; + + foreach (var parameter in type.ParameterList.Parameters) + { + AddPropertyFor(context, parameter, typeDefinitionVariable); + context.WriteNewLine(); + } + } + + private static void AddPropertyFor(IVisitorContext context, ParameterSyntax parameter, string typeDefinitionVariable) + { + using var _ = LineInformationTracker.Track(context, parameter); + + context.WriteComment($"Property: {parameter.Identifier.Text} (primary constructor)"); + var propDefVar = context.Naming.SyntheticVariable(parameter.Identifier.Text, ElementKind.Property); + var paramSymbol = context.SemanticModel.GetDeclaredSymbol(parameter).EnsureNotNull(); + var exps = CecilDefinitionsFactory.PropertyDefinition(propDefVar, parameter.Identifier.Text, context.TypeResolver.Resolve(paramSymbol.Type)); + + context.WriteCecilExpressions(exps); + context.WriteCecilExpression($"{typeDefinitionVariable}.Properties.Add({propDefVar});"); + context.WriteNewLine(); + context.WriteNewLine(); + + var declaringTypeVariable = context.DefinitionVariables.GetLastOf(VariableMemberKind.Type); + if (!declaringTypeVariable.IsValid) + throw new InvalidOperationException(); + + var publicPropertyMethodAttributes = "MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName"; + var propertyType = context.SemanticModel.GetDeclaredSymbol(parameter).GetMemberType(); + var propertyData = new PropertyGenerationData( + declaringTypeVariable.MemberName, + declaringTypeVariable.VariableName, + false, // TODO + propDefVar, + parameter.Identifier.Text, + new Dictionary + { + ["get"] = publicPropertyMethodAttributes, + ["set"] = publicPropertyMethodAttributes + }, + false, + context.TypeResolver.Resolve(propertyType), + propertyType.ToDisplayString(), + Array.Empty(), + "FieldAttributes.Private", //TODO: Constant + OpCodes.Stfld, + OpCodes.Ldfld); + + PropertyGenerator propertyGenerator = new (context); + + AddGetter(); + context.WriteNewLine(); + AddInit(); + + void AddGetter() + { + context.WriteComment($"{propertyData.Name} getter"); + var getMethodVar = context.Naming.SyntheticVariable($"get{propertyData.Name}", ElementKind.Method); + // properties for primary ctor parameters cannot override base properties, so hasCovariantReturn = false and overridenMethod = null (none) + using (propertyGenerator.AddGetterMethodDeclaration(in propertyData, getMethodVar, false, $"get_{propertyData.Name}", null)) + { + var ilVar = context.Naming.ILProcessor($"get{propertyData.Name}"); + context.WriteCecilExpressions([$"var {ilVar} = {getMethodVar}.Body.GetILProcessor();"]); + + propertyGenerator.AddAutoGetterMethodImplementation(in propertyData, ilVar); + } + } + + void AddInit() + { + context.WriteComment($"{propertyData.Name} init"); + var setMethodVar = context.Naming.SyntheticVariable($"set{propertyData.Name}", ElementKind.Method); + using (propertyGenerator.AddSetterMethodDeclaration(in propertyData, setMethodVar, true, $"set_{propertyData.Name}", null)) + { + var ilVar = context.Naming.ILProcessor($"set{propertyData.Name}"); + context.WriteCecilExpressions([$"var {ilVar} = {setMethodVar}.Body.GetILProcessor();"]); + + propertyGenerator.AddAutoSetterMethodImplementation(in propertyData, ilVar); + } + } + } +} diff --git a/Cecilifier.Core/CodeGeneration/Property.Generator.cs b/Cecilifier.Core/CodeGeneration/Property.Generator.cs index b30be21e..1e601826 100644 --- a/Cecilifier.Core/CodeGeneration/Property.Generator.cs +++ b/Cecilifier.Core/CodeGeneration/Property.Generator.cs @@ -117,8 +117,8 @@ internal ScopedDefinitionVariable AddGetterMethodDeclaration(ref readonly Proper return scopedVariable; } - - internal void AddAutoGetterMethodImplementation(PropertyGenerationData propertyGenerationData, string ilVar) + + internal void AddAutoGetterMethodImplementation(ref readonly PropertyGenerationData propertyGenerationData, string ilVar) { AddBackingFieldIfNeeded(in propertyGenerationData); diff --git a/Cecilifier.Core/CodeGeneration/Record.Generator.cs b/Cecilifier.Core/CodeGeneration/Record.Generator.cs new file mode 100644 index 00000000..b76c84a6 --- /dev/null +++ b/Cecilifier.Core/CodeGeneration/Record.Generator.cs @@ -0,0 +1,12 @@ +using Cecilifier.Core.AST; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Cecilifier.Core.CodeGeneration; + +public class RecordGenerator +{ + internal static void AddSyntheticMembers(IVisitorContext context, string recordTypeDefinitionVariable, TypeDeclarationSyntax record) + { + PrimaryConstructorGenerator.AddPropertiesFrom(context, recordTypeDefinitionVariable, record); + } +}