From 3d6e262fd5cc2030a851a89a4bf0af3d2776382f Mon Sep 17 00:00:00 2001 From: Nikita Berezkov <45544538+BerezkovN@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:40:30 +0100 Subject: [PATCH] Add [FactoryParam] attribute as inverse of [FromFactory] * Refactored files and fields that previously used ParameterAttribute to refer to [FromFactory] * New diagnostic to avoid using both attributes at the same time * Created tests for the new attribute --- src/AutoFactories.Tests/DiagnosticsTests.cs | 33 +++++++++- .../GenericFactoryTests.cs | 60 +++++++++++++++++++ ...ters_Mixed#Orders.OrderFactory.verified.cs | 39 ++++++++++++ ...eters#Products.IProductFactory.verified.cs | 23 +++++++ ...meters#Products.ProductFactory.verified.cs | 37 ++++++++++++ src/AutoFactories/AutoFactories.csproj | 3 +- src/AutoFactories/AutoFactoriesAnalyzer.cs | 1 + src/AutoFactories/AutoFactoriesGenerator.cs | 14 ++++- ...onflictingParameterAttributesDiagnostic.cs | 35 +++++++++++ .../Diagnostics/DiagnosticIdentifier.cs | 1 + .../Diagnostics/UnmarkedFactoryDiagnostic.cs | 4 +- src/AutoFactories/TypeNames.cs | 6 +- src/AutoFactories/ViewKey.cs | 5 +- .../ViewModels/ParameterViewModel.cs | 22 +++++-- .../Views/Fixed/FactoryParamAttribute.hbs | 21 +++++++ ...Attribute.hbs => FromFactoryAttribute.hbs} | 0 .../Visitors/ConstructorDeclarationVisitor.cs | 47 +++++++++++++++ .../Visitors/FactoryDeclartion.cs | 18 +++++- .../Visitors/ParameterSyntaxVisitor.cs | 46 +++++++++++--- 19 files changed, 391 insertions(+), 24 deletions(-) create mode 100644 src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Orders.OrderFactory.FactoryParam_Multiple_Parameters_Mixed#Orders.OrderFactory.verified.cs create mode 100644 src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.IProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.IProductFactory.verified.cs create mode 100644 src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.ProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.ProductFactory.verified.cs create mode 100644 src/AutoFactories/Diagnostics/ConflictingParameterAttributesDiagnostic.cs create mode 100644 src/AutoFactories/Views/Fixed/FactoryParamAttribute.hbs rename src/AutoFactories/Views/Fixed/{ParameterAttribute.hbs => FromFactoryAttribute.hbs} (100%) diff --git a/src/AutoFactories.Tests/DiagnosticsTests.cs b/src/AutoFactories.Tests/DiagnosticsTests.cs index ed3f95e..f8abec1 100644 --- a/src/AutoFactories.Tests/DiagnosticsTests.cs +++ b/src/AutoFactories.Tests/DiagnosticsTests.cs @@ -90,7 +90,7 @@ public interface IExternalType using AutoFactories; [AutoFactory] - public class Provider + public class Provider { public Provider(IExternalType externalType) { @@ -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()); } } diff --git a/src/AutoFactories.Tests/GenericFactoryTests.cs b/src/AutoFactories.Tests/GenericFactoryTests.cs index 2dee42a..57c1520 100644 --- a/src/AutoFactories.Tests/GenericFactoryTests.cs +++ b/src/AutoFactories.Tests/GenericFactoryTests.cs @@ -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 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) + { + } + } + } + """]); } } diff --git a/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Orders.OrderFactory.FactoryParam_Multiple_Parameters_Mixed#Orders.OrderFactory.verified.cs b/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Orders.OrderFactory.FactoryParam_Multiple_Parameters_Mixed#Orders.OrderFactory.verified.cs new file mode 100644 index 0000000..c0084d5 --- /dev/null +++ b/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Orders.OrderFactory.FactoryParam_Multiple_Parameters_Mixed#Orders.OrderFactory.verified.cs @@ -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; + } + + /// + /// Creates a new instance of + /// + 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; + } + } +} \ No newline at end of file diff --git a/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.IProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.IProductFactory.verified.cs b/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.IProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.IProductFactory.verified.cs new file mode 100644 index 0000000..bc31173 --- /dev/null +++ b/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.IProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.IProductFactory.verified.cs @@ -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 + { + /// + /// Creates a new instance of + /// + global::Products.Product Create(string name); + } +} diff --git a/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.ProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.ProductFactory.verified.cs b/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.ProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.ProductFactory.verified.cs new file mode 100644 index 0000000..afd206b --- /dev/null +++ b/src/AutoFactories.Tests/Snapshots/GenericFactoryTests_Products.ProductFactory.FactoryParam_Attribute_Marks_Required_Parameters#Products.ProductFactory.verified.cs @@ -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 m_comparer; + + public ProductFactory( +global::System.Collections.Generic.IEqualityComparer comparer) + { + m_comparer = comparer; + } + + /// + /// Creates a new instance of + /// + public global::Products.Product Create(string name) + { + global::Products.Product __result = new global::Products.Product( + name, + m_comparer); + return __result; + } + } +} \ No newline at end of file diff --git a/src/AutoFactories/AutoFactories.csproj b/src/AutoFactories/AutoFactories.csproj index 2018746..6032a7f 100644 --- a/src/AutoFactories/AutoFactories.csproj +++ b/src/AutoFactories/AutoFactories.csproj @@ -9,7 +9,8 @@ - + + True \ diff --git a/src/AutoFactories/AutoFactoriesAnalyzer.cs b/src/AutoFactories/AutoFactoriesAnalyzer.cs index 5494f4d..60cafd4 100644 --- a/src/AutoFactories/AutoFactoriesAnalyzer.cs +++ b/src/AutoFactories/AutoFactoriesAnalyzer.cs @@ -23,6 +23,7 @@ public AutoFactoriesAnalyzer() new InconsistentFactoryAccessibilityBuilder().Descriptor, new ExposedAsNotDerivedTypeDiagnostic().Descriptor, new UnresolvedParameterTypeDiagnostic().Descriptor, + new ConflictingParameterAttributesDiagnostic().Descriptor, ]; } diff --git a/src/AutoFactories/AutoFactoriesGenerator.cs b/src/AutoFactories/AutoFactoriesGenerator.cs index 0e37992..ab92527 100644 --- a/src/AutoFactories/AutoFactoriesGenerator.cs +++ b/src/AutoFactories/AutoFactoriesGenerator.cs @@ -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 }); } diff --git a/src/AutoFactories/Diagnostics/ConflictingParameterAttributesDiagnostic.cs b/src/AutoFactories/Diagnostics/ConflictingParameterAttributesDiagnostic.cs new file mode 100644 index 0000000..a311263 --- /dev/null +++ b/src/AutoFactories/Diagnostics/ConflictingParameterAttributesDiagnostic.cs @@ -0,0 +1,35 @@ +using AutoFactories.Visitors; +using Microsoft.CodeAnalysis; + +namespace AutoFactories.Diagnostics +{ + internal class ConflictingParameterAttributesDiagnostic : SyntaxDiagnosticBuilder + { + 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); + } + } +} diff --git a/src/AutoFactories/Diagnostics/DiagnosticIdentifier.cs b/src/AutoFactories/Diagnostics/DiagnosticIdentifier.cs index 9eabf83..64d7022 100644 --- a/src/AutoFactories/Diagnostics/DiagnosticIdentifier.cs +++ b/src/AutoFactories/Diagnostics/DiagnosticIdentifier.cs @@ -6,6 +6,7 @@ namespace AutoFactories.Diagnostics [Instance("InconsistentFactoryAccessibility", "AF1002")] [Instance("ExposedAsIsNotDerivedType", "AF1003")] [Instance("UnresolvedParameterType", "AF1004")] + [Instance("ConflictingParameterAttributes", "AF1005")] [ValueObject(conversions: Conversions.None, toPrimitiveCasting: CastOperator.Implicit)] internal partial record DiagnosticIdentifier {} diff --git a/src/AutoFactories/Diagnostics/UnmarkedFactoryDiagnostic.cs b/src/AutoFactories/Diagnostics/UnmarkedFactoryDiagnostic.cs index 1fa202d..5a895ce 100644 --- a/src/AutoFactories/Diagnostics/UnmarkedFactoryDiagnostic.cs +++ b/src/AutoFactories/Diagnostics/UnmarkedFactoryDiagnostic.cs @@ -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; } diff --git a/src/AutoFactories/TypeNames.cs b/src/AutoFactories/TypeNames.cs index 57c5a9c..639f3c1 100644 --- a/src/AutoFactories/TypeNames.cs +++ b/src/AutoFactories/TypeNames.cs @@ -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); } } } diff --git a/src/AutoFactories/ViewKey.cs b/src/AutoFactories/ViewKey.cs index b21c748..a84ff32 100644 --- a/src/AutoFactories/ViewKey.cs +++ b/src/AutoFactories/ViewKey.cs @@ -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(conversions: Conversions.None)] public readonly partial struct ViewKey { diff --git a/src/AutoFactories/ViewModels/ParameterViewModel.cs b/src/AutoFactories/ViewModels/ParameterViewModel.cs index 9153600..4aca42c 100644 --- a/src/AutoFactories/ViewModels/ParameterViewModel.cs +++ b/src/AutoFactories/ViewModels/ParameterViewModel.cs @@ -46,14 +46,28 @@ public ParameterViewModel() { Name = ""; } - - + + /// + /// See for mode explanation. + /// 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 }; + } } } diff --git a/src/AutoFactories/Views/Fixed/FactoryParamAttribute.hbs b/src/AutoFactories/Views/Fixed/FactoryParamAttribute.hbs new file mode 100644 index 0000000..5cad658 --- /dev/null +++ b/src/AutoFactories/Views/Fixed/FactoryParamAttribute.hbs @@ -0,0 +1,21 @@ +#nullable enable +using System; + +namespace {{Type.Namespace}} +{ + /// + /// 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. + /// + /// + /// This is the inverse of . Using both + /// attributes in the same constructor is not allowed. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + {{AccessModifier}} sealed class {{Type.Name}} : Attribute + { + public {{Type.Name}}() + {} + } +} diff --git a/src/AutoFactories/Views/Fixed/ParameterAttribute.hbs b/src/AutoFactories/Views/Fixed/FromFactoryAttribute.hbs similarity index 100% rename from src/AutoFactories/Views/Fixed/ParameterAttribute.hbs rename to src/AutoFactories/Views/Fixed/FromFactoryAttribute.hbs diff --git a/src/AutoFactories/Visitors/ConstructorDeclarationVisitor.cs b/src/AutoFactories/Visitors/ConstructorDeclarationVisitor.cs index b31914b..373e7b8 100644 --- a/src/AutoFactories/Visitors/ConstructorDeclarationVisitor.cs +++ b/src/AutoFactories/Visitors/ConstructorDeclarationVisitor.cs @@ -52,6 +52,32 @@ internal class ConstructorDeclarationVisitor : SyntaxVisitor public ClassDeclarationVisitor Class { get; } + /// + /// Gets the location of the constructor declaration + /// + public Location? Location { get; private set; } + + /// + /// Gets whether this constructor uses mode + /// to determine which parameters are required in the factory method. + /// + /// There are two mutually exclusive modes for marking constructor parameters: + /// + /// + /// mode: parameters marked with + /// the attribute are resolved from the DI container; all others are required in the factory method. + /// + /// + /// mode: parameters marked with + /// the attribute are required in the factory method; all others are resolved from DI. + /// + /// + /// If neither attribute is present on any parameter, all parameters are required. + /// Mixing both attributes in the same constructor produces a diagnostic error. + /// + /// + public bool UsesFactoryParamMode { get; private set; } + public ConstructorDeclarationVisitor( bool isAnalyzer, ClassDeclarationVisitor classVisitor, @@ -72,6 +98,7 @@ protected override void Visit(ConstructorDeclarationSyntax syntax) { IsStatic = syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)); IsPrivate = syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)); + Location = syntax.GetLocation(); VisitParameters(syntax.ParameterList); @@ -80,7 +107,27 @@ protected override void Visit(ConstructorDeclarationSyntax syntax) Accessibility = AccessModifier.MostRestrictive([ Accessibility, ..m_parameters.Select(p => p.Accessibility)]); + + ValidateParameterAttributes(); + } + private void ValidateParameterAttributes() + { + bool hasFromFactory = m_parameters.Any(p => p.HasFromFactoryAttribute); + bool hasFactoryParam = m_parameters.Any(p => p.HasFactoryParamAttribute); + + if (hasFromFactory && hasFactoryParam) + { + AddDiagnostic(Diagnostics.ConflictingParameterAttributesDiagnostic.Get(this)); + } + + // Redundant, but easier to read + if (!hasFromFactory && !hasFactoryParam) { + UsesFactoryParamMode = false; + return; + } + + UsesFactoryParamMode = hasFactoryParam; } private void VisitParameters(ParameterListSyntax parametersList) diff --git a/src/AutoFactories/Visitors/FactoryDeclartion.cs b/src/AutoFactories/Visitors/FactoryDeclartion.cs index b4654bc..08a7254 100644 --- a/src/AutoFactories/Visitors/FactoryDeclartion.cs +++ b/src/AutoFactories/Visitors/FactoryDeclartion.cs @@ -28,7 +28,7 @@ private FactoryDeclaration(MetadataTypeName type, IEnumerable !c.IsPrivate) .Where(c => !c.IsStatic) .SelectMany(c => c.Parameters) - .Where(p => p.HasMarkerAttribute) + .Where(ShouldBeDependencyInjectionParameter) .ToList(); Usings = classes.SelectMany(c => c.Usings) @@ -56,6 +56,22 @@ public static IEnumerable Create(IEnumerable + /// Determines if a parameter should be a DI-injected field in the factory. + /// See for mode explanation. + /// + private static bool ShouldBeDependencyInjectionParameter(ParameterSyntaxVisitor parameter) + { + if (parameter.Constructor.UsesFactoryParamMode) + { + return !parameter.HasFactoryParamAttribute; + } + else + { + return parameter.HasFromFactoryAttribute; + } + } + public static FactoryViewModel Map(FactoryDeclaration declaration) => new FactoryViewModel() diff --git a/src/AutoFactories/Visitors/ParameterSyntaxVisitor.cs b/src/AutoFactories/Visitors/ParameterSyntaxVisitor.cs index 940656e..04b9200 100644 --- a/src/AutoFactories/Visitors/ParameterSyntaxVisitor.cs +++ b/src/AutoFactories/Visitors/ParameterSyntaxVisitor.cs @@ -16,7 +16,24 @@ internal class ParameterSyntaxVisitor : SyntaxVisitor public string? Name { get; private set; } public MetadataTypeName Type { get; private set; } - public bool HasMarkerAttribute { get; private set; } + + /// + /// Gets whether the parameter has the attribute. + /// When true, this parameter will be resolved from the DI container. + /// + public bool HasFromFactoryAttribute { get; private set; } + + /// + /// Gets whether the parameter has the attribute. + /// When true, this parameter must be passed to the factory method by the caller. + /// + public bool HasFactoryParamAttribute { get; private set; } + + /// + /// Gets whether the parameter has either marker attribute. + /// Kept for backwards compatibility. + /// + public bool HasMarkerAttribute => HasFromFactoryAttribute || HasFactoryParamAttribute; public AccessModifier Accessibility { get; private set; } @@ -49,10 +66,11 @@ protected override void Visit(ParameterSyntax syntax) typeSymbol = m_semanticModel.GetSymbolInfo(syntax.Type).Symbol as ITypeSymbol; } - AttributeSyntax? markerAttribute = GetMarkerAttribute(syntax); + MarkerAttributeResult markers = GetMarkerAttributes(syntax); Name = syntax.Identifier.Text; - HasMarkerAttribute = markerAttribute is not null; - AttributeLocation = markerAttribute?.GetLocation(); + HasFromFactoryAttribute = markers.FromFactory is not null; + HasFactoryParamAttribute = markers.FactoryParam is not null; + AttributeLocation = markers.FromFactory?.GetLocation() ?? markers.FactoryParam?.GetLocation(); if (typeSymbol is not null) { @@ -83,8 +101,11 @@ protected override void Visit(ParameterSyntax syntax) } - private AttributeSyntax? GetMarkerAttribute(ParameterSyntax node) + private MarkerAttributeResult GetMarkerAttributes(ParameterSyntax node) { + AttributeSyntax? fromFactoryAttribute = null; + AttributeSyntax? factoryParamAttribute = null; + foreach (AttributeListSyntax attributeList in node.AttributeLists) { foreach (AttributeSyntax attribute in attributeList.Attributes) @@ -96,13 +117,22 @@ protected override void Visit(ParameterSyntax syntax) string displayString = typeSymbol.ToDisplayString(); - if (string.Equals(TypeNames.ParameterAttributeType.QualifiedName, displayString)) + if (string.Equals(TypeNames.FromFactoryAttributeType.QualifiedName, displayString)) { - return attribute; + fromFactoryAttribute = attribute; + } + else if (string.Equals(TypeNames.FactoryParamAttributeType.QualifiedName, displayString)) + { + factoryParamAttribute = attribute; } } } - return null; + + return new MarkerAttributeResult(fromFactoryAttribute, factoryParamAttribute); } + + internal record MarkerAttributeResult( + AttributeSyntax? FromFactory, + AttributeSyntax? FactoryParam); } } \ No newline at end of file