Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/AutoFactories.Tests/DiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public interface IExternalType
using AutoFactories;

[AutoFactory]
public class Provider
public class Provider
{
public Provider(IExternalType externalType)
{
Expand All @@ -100,5 +100,36 @@ public Provider(IExternalType externalType)
assertAnalyzerResult:
d => d.Should().OnlyContain(d => d.Id == DiagnosticIdentifier.UnresolvedParameterType));
}

[Fact]
public Task Conflicting_Attributes_FromFactory_And_FactoryParam()
=> Compose("""
using AutoFactories;

[AutoFactory]
public class Widget
{
public Widget([FromFactory] string name, [FactoryParam] int count)
{
}
}
""",
assertAnalyzerResult: d => d.Should()
.OnlyContain(d => d.Id == DiagnosticIdentifier.ConflictingParameterAttributes));

[Fact]
public Task FactoryParam_Only_Produces_No_Error()
=> Compose("""
using AutoFactories;

[AutoFactory]
public class Widget
{
public Widget([FactoryParam] string name, int count)
{
}
}
""",
assertAnalyzerResult: d => d.Should().BeEmpty());
}
}
60 changes: 60 additions & 0 deletions src/AutoFactories.Tests/GenericFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,65 @@ public Widget(string name)
}
}
"""]);

[Fact]
public Task FactoryParam_Attribute_Marks_Required_Parameters()
=> CaptureAsync(
notes: [
"[FactoryParam] marks parameters that should be passed to the factory method.",
"Parameters without [FactoryParam] should come from DI.",
"'name' has [FactoryParam] so it appears in Create() method signature.",
"'comparer' does NOT have [FactoryParam] so it becomes a DI-injected field."
],
verifySource: ["Products.ProductFactory", "Products.IProductFactory"],
source: ["""
using AutoFactories;
using System.Collections.Generic;

namespace Products
{
[AutoFactory]
public class Product
{
public string Name { get; }

public Product([FactoryParam] string name, IEqualityComparer<string?> comparer)
{
Name = name;
}
}
}
"""]);

[Fact]
public Task FactoryParam_Multiple_Parameters_Mixed()
=> CaptureAsync(
notes: [
"Multiple parameters with mixed [FactoryParam] usage.",
"'name' and 'quantity' have [FactoryParam] - they appear in Create().",
"'logger' and 'validator' do NOT have [FactoryParam] - they come from DI."
],
verifySource: ["Orders.OrderFactory"],
source: ["""
using AutoFactories;

namespace Orders
{
public interface ILogger {}
public interface IValidator {}

[AutoFactory]
public class Order
{
public Order(
[FactoryParam] string name,
ILogger logger,
[FactoryParam] int quantity,
IValidator validator)
{
}
}
}
"""]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// -----------------------------| Notes |-----------------------------
// 1. Multiple parameters with mixed [FactoryParam] usage.
// 2. 'name' and 'quantity' have [FactoryParam] - they appear in Create().
// 3. 'logger' and 'validator' do NOT have [FactoryParam] - they come from DI.
// -------------------------------------------------------------------
#nullable enable
#pragma warning disable CS8019 // Unnecessary using directive.

using AutoFactories;


namespace Orders
{
public partial class OrderFactory : IOrderFactory
{
private readonly global::Orders.ILogger m_logger; private readonly global::Orders.IValidator m_validator;

public OrderFactory(
global::Orders.ILogger logger,
global::Orders.IValidator validator)
{
m_logger = logger;
m_validator = validator;
}

/// <summary>
/// Creates a new instance of <see cref="Orders.Order"/>
/// </summary>
public global::Orders.Order Create(string name, int quantity)
{
global::Orders.Order __result = new global::Orders.Order(
name,
m_logger,
quantity,
m_validator);
return __result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// -----------------------------| Notes |-----------------------------
// 1. [FactoryParam] marks parameters that should be passed to the factory method.
// 2. Parameters without [FactoryParam] should come from DI.
// 3. 'name' has [FactoryParam] so it appears in Create() method signature.
// 4. 'comparer' does NOT have [FactoryParam] so it becomes a DI-injected field.
// -------------------------------------------------------------------
#nullable enable
#pragma warning disable CS8019 // Unnecessary using directive.

using System.Collections.Generic;
using AutoFactories;


namespace Products
{
public partial interface IProductFactory
{
/// <summary>
/// Creates a new instance of <see cref="Products.Product"/>
/// </summary>
global::Products.Product Create(string name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// -----------------------------| Notes |-----------------------------
// 1. [FactoryParam] marks parameters that should be passed to the factory method.
// 2. Parameters without [FactoryParam] should come from DI.
// 3. 'name' has [FactoryParam] so it appears in Create() method signature.
// 4. 'comparer' does NOT have [FactoryParam] so it becomes a DI-injected field.
// -------------------------------------------------------------------
#nullable enable
#pragma warning disable CS8019 // Unnecessary using directive.

using System.Collections.Generic;
using AutoFactories;


namespace Products
{
public partial class ProductFactory : IProductFactory
{
private readonly global::System.Collections.Generic.IEqualityComparer<string?> m_comparer;

public ProductFactory(
global::System.Collections.Generic.IEqualityComparer<string?> comparer)
{
m_comparer = comparer;
}

/// <summary>
/// Creates a new instance of <see cref="Products.Product"/>
/// </summary>
public global::Products.Product Create(string name)
{
global::Products.Product __result = new global::Products.Product(
name,
m_comparer);
return __result;
}
}
}
3 changes: 2 additions & 1 deletion src/AutoFactories/AutoFactories.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
<ItemGroup>
<Content Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<EmbeddedResource Include="Views\Fixed\ClassAttribute.hbs" LogicalName="ClassAttribute.hbs" />
<EmbeddedResource Include="Views\Fixed\ParameterAttribute.hbs" LogicalName="ParameterAttribute.hbs" />
<EmbeddedResource Include="Views\Fixed\FromFactoryAttribute.hbs" LogicalName="FromFactoryAttribute.hbs" />
<EmbeddedResource Include="Views\Fixed\FactoryParamAttribute.hbs" LogicalName="FactoryParamAttribute.hbs" />
<None Include="..\..\img\Icon.jpg">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
Expand Down
1 change: 1 addition & 0 deletions src/AutoFactories/AutoFactoriesAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public AutoFactoriesAnalyzer()
new InconsistentFactoryAccessibilityBuilder().Descriptor,
new ExposedAsNotDerivedTypeDiagnostic().Descriptor,
new UnresolvedParameterTypeDiagnostic().Descriptor,
new ConflictingParameterAttributesDiagnostic().Descriptor,
];
}

Expand Down
14 changes: 11 additions & 3 deletions src/AutoFactories/AutoFactoriesGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,19 @@ private void AddSource(IncrementalGeneratorPostInitializationContext context)


renderer.WritePage(
$"{TypeNames.ParameterAttributeType.QualifiedName}.g.cs",
ViewKey.ParameterAttribute, new GenericView()
$"{TypeNames.FromFactoryAttributeType.QualifiedName}.g.cs",
ViewKey.FromFactoryAttribute, new GenericView()
{
AccessModifier = TypeNames.AttributeAccessModifier,
Type = TypeNames.ParameterAttributeType
Type = TypeNames.FromFactoryAttributeType
});

renderer.WritePage(
$"{TypeNames.FactoryParamAttributeType.QualifiedName}.g.cs",
ViewKey.FactoryParamAttribute, new GenericView()
{
AccessModifier = TypeNames.AttributeAccessModifier,
Type = TypeNames.FactoryParamAttributeType
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using AutoFactories.Visitors;
using Microsoft.CodeAnalysis;

namespace AutoFactories.Diagnostics
{
internal class ConflictingParameterAttributesDiagnostic : SyntaxDiagnosticBuilder<ConstructorDeclarationVisitor>
{
public ConflictingParameterAttributesDiagnostic()
: base(
id: DiagnosticIdentifier.ConflictingParameterAttributes,
title: "Conflicting Parameter Attributes",
category: "Usage",
messageFormat:
"The constructor for '{0}' uses both [FromFactory] and [FactoryParam] attributes. " +
"These attributes are mutually exclusive. Use [FromFactory] to mark parameters that should be " +
"resolved from the DI container, OR use [FactoryParam] to mark parameters that should be " +
"passed to the factory method. Do not mix both in the same constructor.")
{
Severity = DiagnosticSeverity.Error;
}

public override Diagnostic Create(ConstructorDeclarationVisitor visitor)
{
return Diagnostic.Create(Descriptor, visitor.Location, new object?[] {
visitor.Class.Type.Name
});
}

public static Diagnostic Get(ConstructorDeclarationVisitor visitor)
{
ConflictingParameterAttributesDiagnostic builder = new ConflictingParameterAttributesDiagnostic();
return builder.Create(visitor);
}
}
}
1 change: 1 addition & 0 deletions src/AutoFactories/Diagnostics/DiagnosticIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace AutoFactories.Diagnostics
[Instance("InconsistentFactoryAccessibility", "AF1002")]
[Instance("ExposedAsIsNotDerivedType", "AF1003")]
[Instance("UnresolvedParameterType", "AF1004")]
[Instance("ConflictingParameterAttributes", "AF1005")]
[ValueObject<string>(conversions: Conversions.None, toPrimitiveCasting: CastOperator.Implicit)]
internal partial record DiagnosticIdentifier
{}
Expand Down
4 changes: 2 additions & 2 deletions src/AutoFactories/Diagnostics/UnmarkedFactoryDiagnostic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public UnmarkedFactoryDiagnostic() : base(
title: "Unmarked FactoryView",
category: "Code",
messageFormat:
$"The parameter '{{0}}' is marked with the [{TypeNames.ParameterAttributeType.Name}] attribute, which indicates it should be used in a factory. However, the declaring class '{{1}}' is missing the [{TypeNames.ClassAttributeType.Name}] attribute, which is required to mark it as a factory. " +
$"To resolve this, either remove the [{TypeNames.ParameterAttributeType.Name}] attribute from the parameter or add the [{TypeNames.ClassAttributeType.Name}] attribute to the class '{{1}}'. This ensures the factory is properly recognized by the source generator.")
$"The parameter '{{0}}' is marked with the [{TypeNames.FromFactoryAttributeType.Name}] attribute, which indicates it should be used in a factory. However, the declaring class '{{1}}' is missing the [{TypeNames.ClassAttributeType.Name}] attribute, which is required to mark it as a factory. " +
$"To resolve this, either remove the [{TypeNames.FromFactoryAttributeType.Name}] attribute from the parameter or add the [{TypeNames.ClassAttributeType.Name}] attribute to the class '{{1}}'. This ensures the factory is properly recognized by the source generator.")
{
Severity = DiagnosticSeverity.Warning;
}
Expand Down
6 changes: 4 additions & 2 deletions src/AutoFactories/TypeNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ namespace AutoFactories
internal static class TypeNames
{
public static MetadataTypeName ClassAttributeType { get; }
public static MetadataTypeName ParameterAttributeType { get; }
public static MetadataTypeName FromFactoryAttributeType { get; }
public static MetadataTypeName FactoryParamAttributeType { get; }
public static AccessModifier AttributeAccessModifier { get; }

static TypeNames()
{
AttributeAccessModifier = AccessModifier.Internal;
ClassAttributeType = new MetadataTypeName(name: "AutoFactoryAttribute", @namespace:"AutoFactories", false, false);
ParameterAttributeType = new MetadataTypeName(name: "FromFactoryAttribute", @namespace: "AutoFactories", false, false);
FromFactoryAttributeType = new MetadataTypeName(name: "FromFactoryAttribute", @namespace: "AutoFactories", false, false);
FactoryParamAttributeType = new MetadataTypeName(name: "FactoryParamAttribute", @namespace: "AutoFactories", false, false);
}
}
}
5 changes: 3 additions & 2 deletions src/AutoFactories/ViewKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ namespace AutoFactories
{
[Instance("Factory", "FactoryView")]
[Instance("FactoryInterface", "FactoryInterfaceView")]
[Instance("ClassAttribute", "ClassAttribute")]
[Instance("ParameterAttribute", "ParameterAttribute")]
[Instance("ClassAttribute", "ClassAttribute")]
[Instance("FromFactoryAttribute", "FromFactoryAttribute")]
[Instance("FactoryParamAttribute", "FactoryParamAttribute")]
[ValueObject<string>(conversions: Conversions.None)]
public readonly partial struct ViewKey
{
Expand Down
22 changes: 18 additions & 4 deletions src/AutoFactories/ViewModels/ParameterViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,28 @@ public ParameterViewModel()
{
Name = "";
}



/// <summary>
/// See <see cref="ConstructorDeclarationVisitor.UsesFactoryParamMode"/> for mode explanation.
/// </summary>
public static ParameterViewModel Map(ParameterSyntaxVisitor visitor)
=> new ParameterViewModel()
{
bool isRequired;
if (visitor.Constructor.UsesFactoryParamMode)
{
isRequired = visitor.HasFactoryParamAttribute;
}
else
{
isRequired = !visitor.HasFromFactoryAttribute;
}

return new ParameterViewModel()
{
Name = visitor.Name?.ToCamelCase(),
Type = visitor.Type,
IsRequired = !visitor.HasMarkerAttribute
IsRequired = isRequired
};
}
}
}
21 changes: 21 additions & 0 deletions src/AutoFactories/Views/Fixed/FactoryParamAttribute.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#nullable enable
using System;

namespace {{Type.Namespace}}
{
/// <summary>
/// Applied to a parameter of a constructor to indicate this parameter
/// should be passed to the factory method by the caller. Parameters without
/// this attribute will be resolved from the dependency injection container.
/// </summary>
/// <remarks>
/// This is the inverse of <see cref="FromFactoryAttribute"/>. Using both
/// attributes in the same constructor is not allowed.
/// </remarks>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
{{AccessModifier}} sealed class {{Type.Name}} : Attribute
{
public {{Type.Name}}()
{}
}
}
Loading