Skip to content

Commit

Permalink
Add new matchBaseParameterOnName parameter on AutoConstructorAttribute
Browse files Browse the repository at this point in the history
  • Loading branch information
k94ll13nn3 committed Jun 24, 2024
1 parent d30c221 commit 2b8d9b5
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 16 deletions.
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,96 @@ partial class Test
}
```

### Base constructor parameter matching

When inheriting a class, a call to the base constructor will be generated. By default, any parameter with the same name and the same type in both the parent and the
child class will be matched together.

```csharp
// This (same name and type)
public class ParentClass
{
private readonly int value;

public ParentClass(int value)
{
this.value = value;
}
}

[AutoConstructor]
public partial class Test : ParentClass
{
private readonly int value;
}

// Generates
partial class Test
{
public Test(int service) : base(service)
{
this.service = service;
}
}

// This (same name but not same type)
public class ParentClass
{
private readonly long value;

public ParentClass(long value)
{
this.value = value;
}
}

[AutoConstructor]
public partial class Test : ParentClass
{
private readonly int value;
}

// Generates
partial class Test
{
public Test(int service, long b0__service) : base(b0__service)
{
this.service = service;
}
}
```

If wanted, the matching on the type can be disable by setting the parameter `matchBaseParameterOnName` on `AutoConstructorAttribute` as `true`.
⚠️ This can lead to invalid code, since the type is no longer checked, anything can be used as a parameter with the same name. Use this only when necessary.

```csharp
// This (same name but not same type with matchBaseParameterOnName true)
public class ParentClass
{
private readonly long value;

public ParentClass(long value)
{
this.value = value;
}
}

[AutoConstructor(matchBaseParameterOnName: true)]
public partial class Test : ParentClass
{
private readonly int value;
}

// Generates
partial class Test
{
public Test(int service) : base(service)
{
this.service = service;
}
}
```

### Properties injection

Get-only properties (`public int Property { get; }`) are injected by the generator by default.
Expand Down
35 changes: 23 additions & 12 deletions src/AutoConstructor.Generator/AutoConstructorGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ private static Options ParseOptions(AnalyzerConfigOptions analyzerOptions)
return null;
}

AttributeData? attributeData = symbol.GetAttribute(Source.AttributeFullName);
List<FieldInfo> concatenatedFields = GetFieldsFromSymbol(symbol);

ExtractFieldsFromParent(symbol, concatenatedFields);
bool matchBaseParameterOnName = attributeData.GetOptionalBoolParameterValue("matchBaseParameterOnName");
ExtractFieldsFromParent(symbol, concatenatedFields, matchBaseParameterOnName);

EquatableArray<FieldInfo> fields = concatenatedFields.ToImmutableArray();

Expand Down Expand Up @@ -154,7 +156,6 @@ private static Options ParseOptions(AnalyzerConfigOptions analyzerOptions)
&& symbol.Constructors.Any(d => !d.IsStatic && d.Parameters.Length == 0);

string? accessibility = null;
AttributeData? attributeData = symbol.GetAttribute(Source.AttributeFullName);
if (attributeData?.AttributeConstructor?.Parameters.Length > 0
&& attributeData.GetParameterValue<string>("accessibility") is string { Length: > 0 } accessibilityValue
&& ConstructorAccessibilities.Contains(accessibilityValue))
Expand Down Expand Up @@ -438,30 +439,35 @@ private static FieldInfo GetFieldInfo(IFieldSymbol fieldSymbol)
null);
}

private static void ExtractFieldsFromParent(INamedTypeSymbol symbol, List<FieldInfo> concatenatedFields, int depth = 0)
private static void ExtractFieldsFromParent(INamedTypeSymbol symbol, List<FieldInfo> concatenatedFields, bool matchBaseParameterOnName, int depth = 0)
{
(IMethodSymbol? constructor, INamedTypeSymbol? baseType) = symbol.GetPreferredBaseConstructorOrBaseType();
if (constructor is not null)
{
ExtractFieldsFromConstructedParent(concatenatedFields, constructor, depth);
ExtractFieldsFromConstructedParent(concatenatedFields, constructor, matchBaseParameterOnName, depth);
}
else if (baseType is not null)
{
ExtractFieldsFromGeneratedParent(concatenatedFields, baseType, depth);
ExtractFieldsFromGeneratedParent(concatenatedFields, baseType, matchBaseParameterOnName, depth);
}
}

private static void ExtractFieldsFromConstructedParent(List<FieldInfo> concatenatedFields, IMethodSymbol constructor, int depth)
private static void ExtractFieldsFromConstructedParent(List<FieldInfo> concatenatedFields, IMethodSymbol constructor, bool matchBaseParameterOnName, int depth)
{
// Base order is function of the depth, it theoretically can conflict with an order already chosen, but this would mean that a parent has more than 1000 parameters
int order = depth * 1000;
foreach (IParameterSymbol parameter in constructor.Parameters)
{
string formatedType = parameter.Type.ToDisplayString(DisplayFormat);
int index = concatenatedFields.FindIndex(p => p.ParameterName == parameter.Name && (p.Type ?? p.FallbackType) == formatedType);
int index = concatenatedFields.FindIndex(p => p.ParameterName == parameter.Name
&& (matchBaseParameterOnName || (p.Type ?? p.FallbackType) == formatedType));
if (index != -1)
{
concatenatedFields[index] = concatenatedFields[index] with { FieldType = concatenatedFields[index].FieldType | FieldType.PassedToBase, BaseOrder = order };
concatenatedFields[index] = concatenatedFields[index] with
{
FieldType = concatenatedFields[index].FieldType | FieldType.PassedToBase,
BaseOrder = order
};
}
else
{
Expand All @@ -484,16 +490,21 @@ private static void ExtractFieldsFromConstructedParent(List<FieldInfo> concatena
}
}

private static void ExtractFieldsFromGeneratedParent(List<FieldInfo> concatenatedFields, INamedTypeSymbol symbol, int depth)
private static void ExtractFieldsFromGeneratedParent(List<FieldInfo> concatenatedFields, INamedTypeSymbol symbol, bool matchBaseParameterOnName, int depth)
{
// Base order is function of the depth, it theoretically can conflict with an order already chosen, but this would mean that a parent has more than 1000 parameters
int order = depth * 1000;
foreach (FieldInfo parameter in GetFieldsFromSymbol(symbol))
{
int index = concatenatedFields.FindIndex(p => p.ParameterName == parameter.ParameterName && (p.Type ?? p.FallbackType) == (parameter.Type ?? parameter.FallbackType));
int index = concatenatedFields.FindIndex(p => p.ParameterName == parameter.ParameterName
&& (matchBaseParameterOnName || (p.Type ?? p.FallbackType) == (parameter.Type ?? parameter.FallbackType)));
if (index != -1)
{
concatenatedFields[index] = concatenatedFields[index] with { FieldType = concatenatedFields[index].FieldType | FieldType.PassedToBase, BaseOrder = order };
concatenatedFields[index] = concatenatedFields[index] with
{
FieldType = concatenatedFields[index].FieldType | FieldType.PassedToBase,
BaseOrder = order
};
}
else
{
Expand All @@ -515,7 +526,7 @@ private static void ExtractFieldsFromGeneratedParent(List<FieldInfo> concatenate
order++;
}

ExtractFieldsFromParent(symbol, concatenatedFields, depth + 1);
ExtractFieldsFromParent(symbol, concatenatedFields, matchBaseParameterOnName, depth + 1);
}

private static bool IsNullable(ITypeSymbol typeSymbol)
Expand Down
6 changes: 5 additions & 1 deletion src/AutoConstructor.Generator/Source.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ internal sealed class {{AttributeFullName}} : System.Attribute
/// <param name="addDefaultBaseAttribute">Configure automatic injection of <c>AutoConstructorDefaultBaseAttribute</c></param>
/// <param name="disableThisCall">Disable call to this when it would have been called</param>
/// <param name="addParameterless">Generate a parameterless constructor too.</param>
public {{AttributeFullName}}(string accessibility = null, bool addDefaultBaseAttribute = false, bool disableThisCall = false, bool addParameterless = false)
/// <param name="matchBaseParameterOnName">If true, parameters that have the same name in base and child constructors will be matched together (when false, type must also match)</param>
public {{AttributeFullName}}(string accessibility = null, bool addDefaultBaseAttribute = false, bool disableThisCall = false, bool addParameterless = false, bool matchBaseParameterOnName = false)
{
Accessibility = accessibility;
AddDefaultBaseAttribute = addDefaultBaseAttribute;
DisableThisCall = disableThisCall;
AddParameterless = addParameterless;
MatchBaseParameterOnName = matchBaseParameterOnName;
}
public string Accessibility { get; }
Expand All @@ -52,6 +54,8 @@ internal sealed class {{AttributeFullName}} : System.Attribute
public bool DisableThisCall { get; }
public bool AddParameterless { get; }
public bool MatchBaseParameterOnName { get; }
}
""";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Xunit;
using VerifySourceGenerator = AutoConstructor.Tests.Verifiers.CSharpSourceGeneratorVerifier<AutoConstructor.Generator.AutoConstructorGenerator>;

namespace AutoConstructor.Tests;
namespace AutoConstructor.Tests.Generator;

public class AddParameterlessGeneratorTests
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Xunit;
using VerifySourceGenerator = AutoConstructor.Tests.Verifiers.CSharpSourceGeneratorVerifier<AutoConstructor.Generator.AutoConstructorGenerator>;

namespace AutoConstructor.Tests;
namespace AutoConstructor.Tests.Generator;

public class ConflictingParameterNameTests
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Xunit;
using VerifySourceGenerator = AutoConstructor.Tests.Verifiers.CSharpSourceGeneratorVerifier<AutoConstructor.Generator.AutoConstructorGenerator>;

namespace AutoConstructor.Tests;
namespace AutoConstructor.Tests.Generator;

public class GeneratorTests
{
Expand Down
118 changes: 118 additions & 0 deletions tests/AutoConstructor.Tests/Generator/MatchBaseParameterOnNameTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using Xunit;
using VerifySourceGenerator = AutoConstructor.Tests.Verifiers.CSharpSourceGeneratorVerifier<AutoConstructor.Generator.AutoConstructorGenerator>;

namespace AutoConstructor.Tests.Generator;

public class MatchBaseParameterOnNameTests
{
[Fact]
public async Task Run_WhenMatchBaseParameterOnNameIsTrue_ShouldGenerateClass()
{
const string code = @"
namespace Test
{
public class ParentClass
{
private readonly long service;
public ParentClass(long service)
{
this.service = service;
}
}
[AutoConstructor(matchBaseParameterOnName: true)]
public partial class Test : ParentClass
{
private readonly int service;
}
}";

const string generated = @"namespace Test
{
partial class Test
{
public Test(int service) : base(service)
{
this.service = service;
}
}
}
";
await VerifySourceGenerator.RunAsync(code, generated);
}

[Fact]
public async Task Run_WhenMatchBaseParameterOnNameIsFalse_ShouldGenerateClass()
{
const string code = @"
namespace Test
{
public class ParentClass
{
private readonly long service;
public ParentClass(long service)
{
this.service = service;
}
}
[AutoConstructor(matchBaseParameterOnName: false)]
public partial class Test : ParentClass
{
private readonly int service;
}
}";

const string generated = @"namespace Test
{
partial class Test
{
public Test(int service, long b0__service) : base(b0__service)
{
this.service = service;
}
}
}
";
await VerifySourceGenerator.RunAsync(code, generated);
}

[Fact]
public async Task Run_WhenMatchBaseParameterOnNameIsFalseWithSameType_ShouldGenerateClass()
{
const string code = @"
namespace Test
{
public class ParentClass
{
private readonly int service;
public ParentClass(int service)
{
this.service = service;
}
}
[AutoConstructor(matchBaseParameterOnName: false)]
public partial class Test : ParentClass
{
private readonly int service;
}
}";

const string generated = @"namespace Test
{
partial class Test
{
public Test(int service) : base(service)
{
this.service = service;
}
}
}
";
await VerifySourceGenerator.RunAsync(code, generated);
}
}

0 comments on commit 2b8d9b5

Please sign in to comment.