Skip to content

Commit

Permalink
adds a public property for each primary contructor parameter (#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianoc committed Jun 9, 2024
1 parent d0268f8 commit 1368788
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 10 deletions.
47 changes: 45 additions & 2 deletions Cecilifier.Core.Tests/Tests/Unit/TypeTests.Records.cs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -37,4 +38,46 @@ public void RecordType_Implements_IEquatable(string classOrStruct)
\s+assembly.MainModule.Types.Add\(\k<recVar>\);
"""));
}

[Test]
public void PrimaryConstructorParameters_AreMappedToPublicProperties()
{
var result = RunCecilifier("public record TheRecord(int Value, TheRecord Parent, char Ch = '?');");

var cecilifiedCode = result.GeneratedCode.ReadToEnd();
Span<Range> 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_var>prop_{Char.ToLower(propertyName[0])}{propertyName.Slice(1)}_\d+) = new PropertyDefinition\("{propertyName}", PropertyAttributes.None, {propertyType}\);
\s+rec_theRecord_\d+.Properties.Add\(\k<prop_var>\);
"""));

// 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\);
"""));
}
}
}
12 changes: 6 additions & 6 deletions Cecilifier.Core/AST/TypeDeclarationVisitor.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
Expand Down
101 changes: 101 additions & 0 deletions Cecilifier.Core/CodeGeneration/PrimaryConstructor.Generator.cs
Original file line number Diff line number Diff line change
@@ -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<ISymbol, IParameterSymbol>();
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<string, string>
{
["get"] = publicPropertyMethodAttributes,
["set"] = publicPropertyMethodAttributes
},
false,
context.TypeResolver.Resolve(propertyType),
propertyType.ToDisplayString(),
Array.Empty<ParameterSpec>(),
"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);
}
}
}
}
4 changes: 2 additions & 2 deletions Cecilifier.Core/CodeGeneration/Property.Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
12 changes: 12 additions & 0 deletions Cecilifier.Core/CodeGeneration/Record.Generator.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 1368788

Please sign in to comment.