Skip to content

Commit

Permalink
Support for multi parameter 'With' methods (#11)
Browse files Browse the repository at this point in the history
Added support for multi-parameter With methods
  • Loading branch information
aalmada authored and mikhailshilkov committed Apr 3, 2017
1 parent c729238 commit d597579
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 33 deletions.
3 changes: 3 additions & 0 deletions AssemblyToProcess/PrimitiveValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ public PrimitiveValues(int value1, string value2, long value3)
public long Value3 { get; }

public PrimitiveValues With<T>(T value) => this;
public PrimitiveValues With(int value1, string value2) => this;
public PrimitiveValues With(int value1, long value3) => this;
public PrimitiveValues WithSecondAndThird(string value2, long value3) => this;
}
}
4 changes: 4 additions & 0 deletions AssemblyToProcess/PropertiesOfSameType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ public PropertiesOfSameType(int? value1, int? value2, int? value3)
public PropertiesOfSameType WithValue2(int? value) => this;

public PropertiesOfSameType WithValue3(int? value) => this;

public PropertiesOfSameType WithValue1Value2(int? value1, int? value2) => this;

public PropertiesOfSameType WithSecondAndThird(int? value2, int? value3) => this;
}
}
25 changes: 25 additions & 0 deletions Tests/WeaverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ public void PrimitiveValues_ShortWithIsInjected()
Assert.AreEqual(instance.Value1, result3.Value1);
Assert.AreEqual(instance.Value2, result3.Value2);
Assert.AreEqual(31231, result3.Value3);

var result4 = instance.With(123, "World");
Assert.AreEqual(123, result1.Value1);
Assert.AreEqual("World", result2.Value2);
Assert.AreEqual(31231, result3.Value3);

var result5 = instance.With(123, (long)31231);
Assert.AreEqual(123, result1.Value1);
Assert.AreEqual(instance.Value2, result3.Value2);
Assert.AreEqual(31231, result3.Value3);

var result6 = instance.WithSecondAndThird("World", (long)31231);
Assert.AreEqual(instance.Value1, result2.Value1);
Assert.AreEqual("World", result2.Value2);
Assert.AreEqual(31231, result3.Value3);
}

[Test]
Expand All @@ -182,6 +197,16 @@ public void PropertiesOfSameType_LongNamedWithIsInjected()
Assert.AreEqual(instance.Value1, result3.Value1);
Assert.AreEqual(instance.Value2, result3.Value2);
Assert.AreEqual(333, result3.Value3);

var result4 = instance.WithValue1Value2(111, 222);
Assert.AreEqual(111, result1.Value1);
Assert.AreEqual(222, result2.Value2);
Assert.AreEqual(instance.Value3, result1.Value3);

var result5 = instance.WithSecondAndThird(222, 333);
Assert.AreEqual(instance.Value1, result3.Value1);
Assert.AreEqual(222, result2.Value2);
Assert.AreEqual(333, result3.Value3);
}

[Test]
Expand Down
141 changes: 108 additions & 33 deletions With.Fody/ModuleWeaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,52 +69,113 @@ private void RemoveGenericWith(TypeDefinition type)

private void AddWith(TypeDefinition type, MethodDefinition ctor)
{
foreach (var property in ctor.Parameters)
foreach (var withMethod in type.Methods.Where(m => m.IsPublic && m.Name.StartsWith("With")).ToArray())
{
var parameterName = property.Name;
var getter = GetPropertyGetter(type, parameterName);

var propertyName = ToPropertyName(property.Name);
MethodDefinition method;
var explicitName = $"With{propertyName}";
if (type.Methods.Any(m => m.Name == explicitName)
|| ctor.Parameters.Except(new[] { property }).Any(p => p.ParameterType.FullName == property.ParameterType.FullName))
if (withMethod.HasGenericParameters)
{
method = type.Methods.FirstOrDefault(m => m.Name == explicitName);
if (method == null)
continue;
}
else
{
method = new MethodDefinition("With", MethodAttributes.Public, type);
method.Parameters.Add(new ParameterDefinition(parameterName, ParameterAttributes.None, getter.ReturnType));
type.Methods.Add(method);
}
if (withMethod.Parameters.Count == 1)
{
// create one 'with' method for each contructor parameter
foreach (var parameter in ctor.Parameters)
{
// step over if type contains explicit 'with' method for this parameter
var propertyName = ToPropertyName(parameter.Name);
var explicitName = $"With{propertyName}";
if (!type.Methods.Any(m => m.Name == explicitName))
{
var methodName = "With";
// append property name if another parameter with same type
if(ctor.Parameters
.Except(new[] { parameter })
.Any(p => p.ParameterType.FullName == parameter.ParameterType.FullName))
{
methodName += propertyName;
}

var processor = method.Body.GetILProcessor();
foreach (var i in processor.Body.Instructions.ToArray())
{
processor.Remove(i);
var method = new MethodDefinition(methodName, MethodAttributes.Public, type);
method.Parameters.Add(new ParameterDefinition(parameter.Name, ParameterAttributes.None, parameter.ParameterType));
type.Methods.Add(method);
AddWith(type, ctor, method);

this.ReplaceCalls(type, method, parameter.ParameterType);
}
}
}
else
{
// do nothing
// any use case for a generic multi-parameter 'with' method?
}
}
foreach (var parameter in ctor.Parameters)
else
{
if (parameter.Name == parameterName)
var parameterName = (string)null;
if (withMethod.Parameters.Count == 1 && IsExplicitName(type, withMethod.Name, out parameterName))
{
processor.Emit(OpCodes.Ldarg_1);
AddWith(type, ctor, withMethod, parameterName);
}
else
{
var getterParameter = GetPropertyGetter(type, parameter.Name);
processor.Emit(OpCodes.Ldarg_0);
processor.Emit(OpCodes.Call, getterParameter);
AddWith(type, ctor, withMethod);
}
}
processor.Emit(OpCodes.Newobj, ctor);
processor.Emit(OpCodes.Ret);
method.Body.OptimizeMacros();
}
}

this.ReplaceCalls(type, method, getter.ReturnType);
public void AddWith(TypeDefinition type, MethodDefinition ctor, MethodDefinition withMethod, string parameterName)
{
var processor = withMethod.Body.GetILProcessor();
foreach (var i in processor.Body.Instructions.ToArray())
{
processor.Remove(i);
}
foreach (var ctorParameter in ctor.Parameters)
{
if (String.Compare(parameterName, ctorParameter.Name, StringComparison.InvariantCultureIgnoreCase) == 0)
{
processor.Emit(OpCodes.Ldarg_1);
}
else
{
var getter = GetPropertyGetter(type, ctorParameter.Name);
processor.Emit(OpCodes.Ldarg_0);
processor.Emit(OpCodes.Call, getter);
}
}
processor.Emit(OpCodes.Newobj, ctor);
processor.Emit(OpCodes.Ret);
withMethod.Body.OptimizeMacros();
}

public void AddWith(TypeDefinition type, MethodDefinition ctor, MethodDefinition withMethod)
{
var processor = withMethod.Body.GetILProcessor();
foreach (var i in processor.Body.Instructions.ToArray())
{
processor.Remove(i);
}
foreach (var ctorParameter in ctor.Parameters)
{
var withParameter = withMethod.Parameters
.Select((par, index) => new { Parameter = par, Index = index })
.FirstOrDefault(item =>
item.Parameter.ParameterType.FullName == ctorParameter.ParameterType.FullName &&
String.Compare(item.Parameter.Name, ctorParameter.Name, StringComparison.InvariantCultureIgnoreCase) == 0);

if (withParameter != null)
{
processor.Emit(OpCodes.Ldarg, withParameter.Index + 1);
}
else
{
var getter = GetPropertyGetter(type, ctorParameter.Name);
processor.Emit(OpCodes.Ldarg_0);
processor.Emit(OpCodes.Call, getter);
}
}
processor.Emit(OpCodes.Newobj, ctor);
processor.Emit(OpCodes.Ret);
withMethod.Body.OptimizeMacros();
}

private void ReplaceCalls(TypeDefinition withType, MethodDefinition newMethod, TypeReference argumentType)
Expand Down Expand Up @@ -152,6 +213,20 @@ private static string ToPropertyName(string fieldName)
return Char.ToUpperInvariant(fieldName[0]) + fieldName.Substring(1);
}

private bool IsExplicitName(TypeDefinition type, string methodName, out string parameterName)
{
if (!(methodName.Length > 4 && methodName.StartsWith("With")))
{
parameterName = null;
return false;
}

parameterName = methodName.Substring(4);
var name = parameterName; // required for lambda
return GetAllProperties(type)
.Any(pro => String.Compare(pro.Name, name, StringComparison.InvariantCultureIgnoreCase) == 0);
}

private IEnumerable<PropertyDefinition> GetAllProperties(TypeDefinition type)
{
// get recursively through the hierachy all the properties with a public getter
Expand Down

0 comments on commit d597579

Please sign in to comment.