diff --git a/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs b/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs
index bfb4dd5..c8507b0 100644
--- a/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs
+++ b/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs
@@ -480,27 +480,39 @@ private DependencyFlags GetDependencyFlags(GeneratorSyntaxContext context)
switch (name)
{
case "OnlyRealm":
- onlyRealm = argumentSyntax.Expression.ToString() == "true";
+ onlyRealm =
+ ConstantArgumentReader.ReadBool(context, argumentSyntax.Expression)
+ ?? false;
break;
case "Using":
- registrationType = GetRegistrationType(
- argumentSyntax.Expression.ToString()
+ registrationType = ConstantArgumentReader.ReadRegistrationType(
+ context,
+ argumentSyntax.Expression
);
break;
case "GenerateAttribute":
- generateAttribute =
- argumentSyntax.Expression.ToString().Trim('"') == "true";
+ generateAttribute = ConstantArgumentReader.ReadBool(
+ context,
+ argumentSyntax.Expression
+ );
break;
case "RegisterJsonSerializers":
- registerGenerator =
- argumentSyntax.Expression.ToString().Trim('"') == "true";
+ registerGenerator = ConstantArgumentReader.ReadBool(
+ context,
+ argumentSyntax.Expression
+ );
break;
case "GenerateUseMethod":
- useMethod = argumentSyntax.Expression.ToString().Trim('"');
+ useMethod = ConstantArgumentReader.ReadString(
+ context,
+ argumentSyntax.Expression
+ );
break;
case "GenerateFactories":
- generateFactories =
- argumentSyntax.Expression.ToString().Trim('"') == "true";
+ generateFactories = ConstantArgumentReader.ReadBool(
+ context,
+ argumentSyntax.Expression
+ );
break;
}
}
diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstantArgumentReader.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstantArgumentReader.cs
new file mode 100644
index 0000000..3391a89
--- /dev/null
+++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstantArgumentReader.cs
@@ -0,0 +1,65 @@
+using DependencyModules.SourceGenerator.Impl.Models;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace DependencyModules.SourceGenerator.Impl.Utilities;
+
+///
+/// Reads the value of an argument from the semantic model, not from its source text.
+///
+///
+/// A constant declared elsewhere, a digit separator and a fully qualified enum member all read as
+/// the value they evaluate to. The generator does not reference the runtime assembly, so an enum
+/// argument arrives as its underlying number. Each method returns null for an argument that has no
+/// constant value.
+///
+public static class ConstantArgumentReader
+{
+ public static int? ReadInt(SyntaxTransformContext context, ExpressionSyntax expression) =>
+ context.SemanticModel.GetConstantValue(expression).Value is int value ? value : null;
+
+ public static bool? ReadBool(SyntaxTransformContext context, ExpressionSyntax expression) =>
+ context.SemanticModel.GetConstantValue(expression).Value is bool value ? value : null;
+
+ public static string? ReadString(SyntaxTransformContext context, ExpressionSyntax expression) =>
+ context.SemanticModel.GetConstantValue(expression).Value as string;
+
+ ///
+ /// The generator's has the member order of the runtime's.
+ ///
+ public static RegistrationType? ReadRegistrationType(
+ SyntaxTransformContext context,
+ ExpressionSyntax expression
+ ) =>
+ ReadInt(context, expression) is { } value && Enum.IsDefined(typeof(RegistrationType), value)
+ ? (RegistrationType)value
+ : null;
+
+ ///
+ /// ServiceLifetime numbers its members Singleton, Scoped, Transient, which is the reverse
+ /// of .
+ ///
+ public static ServiceLifestyle? ReadLifetime(
+ SyntaxTransformContext context,
+ ExpressionSyntax expression
+ ) =>
+ ReadInt(context, expression) switch
+ {
+ 0 => ServiceLifestyle.Singleton,
+ 1 => ServiceLifestyle.Scoped,
+ 2 => ServiceLifestyle.Transient,
+ _ => null,
+ };
+
+ ///
+ /// has the flag values of the runtime's
+ /// InterceptedMembers.
+ ///
+ public static InterceptedMemberKinds? ReadMemberKinds(
+ SyntaxTransformContext context,
+ ExpressionSyntax expression
+ ) =>
+ ReadInt(context, expression) is { } value
+ ? (InterceptedMemberKinds)value & InterceptedMemberKinds.All
+ : null;
+}
diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs
index 728a2f6..f872ff0 100644
--- a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs
+++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs
@@ -48,10 +48,7 @@ CancellationToken cancellationToken
switch (argument.NameEquals?.Name.ToString())
{
case "Order":
- if (int.TryParse(argument.Expression.ToString(), out var parsed))
- {
- order = parsed;
- }
+ order = ConstantArgumentReader.ReadInt(context, argument.Expression) ?? 0;
break;
case "Service":
explicitService = GetTypeOfArgument(argument, context);
diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs
index 57337c3..2d9bca5 100644
--- a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs
+++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs
@@ -84,7 +84,7 @@ is not INamedTypeSymbol implementationSymbol
ref realm
);
- memberKinds &= ReadMemberKinds(attribute);
+ memberKinds &= ReadMemberKinds(attribute, context);
}
if (interceptorSymbols.Count == 0)
@@ -245,13 +245,10 @@ CancellationToken cancellationToken
///
/// The kinds of member [Intercept] names, as flags. Everything when it names none.
///
- ///
- /// Read as written rather than resolved, for the reason the lifetime is: the alternative is
- /// referencing an enum this generator does not. Splitting on the or-operator and taking the last
- /// segment of each part handles `Methods`, `InterceptedMembers.Methods` and any qualification of
- /// either.
- ///
- private static InterceptedMemberKinds ReadMemberKinds(AttributeSyntax attribute)
+ private static InterceptedMemberKinds ReadMemberKinds(
+ AttributeSyntax attribute,
+ SyntaxTransformContext context
+ )
{
foreach (
var argument in attribute.ArgumentList?.Arguments
@@ -263,33 +260,11 @@ var argument in attribute.ArgumentList?.Arguments
continue;
}
- var kinds = InterceptedMemberKinds.None;
-
- foreach (var part in argument.Expression.ToString().Split('|'))
- {
- var member = part.Substring(part.LastIndexOf('.') + 1).Trim();
+ var kinds = ConstantArgumentReader.ReadMemberKinds(context, argument.Expression);
- switch (member)
- {
- case "Methods":
- kinds |= InterceptedMemberKinds.Methods;
- break;
- case "Properties":
- kinds |= InterceptedMemberKinds.Properties;
- break;
- case "Indexers":
- kinds |= InterceptedMemberKinds.Indexers;
- break;
- case "Events":
- kinds |= InterceptedMemberKinds.Events;
- break;
- case "All":
- kinds |= InterceptedMemberKinds.All;
- break;
- }
- }
-
- return kinds == InterceptedMemberKinds.None ? InterceptedMemberKinds.All : kinds;
+ return kinds is null or InterceptedMemberKinds.None
+ ? InterceptedMemberKinds.All
+ : kinds.Value;
}
return InterceptedMemberKinds.All;
@@ -446,7 +421,7 @@ ref INamedTypeSymbol? realm
// Read first, applied below. Lifetime is a named argument and may be written after the
// interceptors it applies to, so collecting them in one pass would attach whatever the
// lifetime happened to be at that point in the argument list.
- var lifestyle = ReadLifetime(attribute);
+ var lifestyle = ReadLifetime(attribute, context);
foreach (var argument in attribute.ArgumentList.Arguments)
{
@@ -470,10 +445,7 @@ ref INamedTypeSymbol? realm
switch (name)
{
case "Order":
- if (int.TryParse(argument.Expression.ToString(), out var parsed))
- {
- order = parsed;
- }
+ order = ConstantArgumentReader.ReadInt(context, argument.Expression) ?? 0;
break;
case "Service":
if (argument.Expression is TypeOfExpressionSyntax serviceTypeOf)
@@ -495,35 +467,20 @@ ref INamedTypeSymbol? realm
/// The lifetime one [Intercept] names for the interceptors it lists, Singleton when it names
/// none.
///
- ///
- /// Read from the written name rather than resolved, because the alternative is a constant of an
- /// enum this generator does not reference. Every legal spelling ends in the member name, so the
- /// last segment is the answer for `Scoped`, `ServiceLifetime.Scoped` and any qualification of
- /// it alike.
- ///
- private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute)
+ private static ServiceLifestyle ReadLifetime(
+ AttributeSyntax attribute,
+ SyntaxTransformContext context
+ )
{
foreach (
var argument in attribute.ArgumentList?.Arguments
?? default(SeparatedSyntaxList)
)
{
- if (argument.NameEquals?.Name.ToString() != "Lifetime")
- {
- continue;
- }
-
- var written = argument.Expression.ToString();
- var member = written.Substring(written.LastIndexOf('.') + 1).Trim();
-
- switch (member)
+ if (argument.NameEquals?.Name.ToString() == "Lifetime")
{
- case "Scoped":
- return ServiceLifestyle.Scoped;
- case "Transient":
- return ServiceLifestyle.Transient;
- case "Singleton":
- return ServiceLifestyle.Singleton;
+ return ConstantArgumentReader.ReadLifetime(context, argument.Expression)
+ ?? ServiceLifestyle.Singleton;
}
}
diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs
index 4dcc8ff..f6344db 100644
--- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs
+++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs
@@ -602,13 +602,18 @@ is MemberAccessExpressionSyntax accessExpressionSyntax
break;
case "Using":
- registrationType = BaseSourceGenerator.GetRegistrationType(
- argumentSyntax.Expression.ToString()
+ registrationType = ConstantArgumentReader.ReadRegistrationType(
+ context,
+ argumentSyntax.Expression
);
break;
case "Lifetime":
- lifestyle = GetLifestyle(argumentSyntax.Expression.ToString());
+ lifestyle =
+ ConstantArgumentReader.ReadLifetime(
+ context,
+ argumentSyntax.Expression
+ ) ?? ServiceLifestyle.Singleton;
break;
case "Realm":
@@ -619,15 +624,9 @@ is MemberAccessExpressionSyntax accessExpressionSyntax
break;
case "Order":
- if (
- int.TryParse(
- argumentSyntax.Expression.ToString(),
- out var parsedOrder
- )
- )
- {
- order = parsedOrder;
- }
+ order =
+ ConstantArgumentReader.ReadInt(context, argumentSyntax.Expression)
+ ?? 0;
break;
}
}
@@ -657,24 +656,6 @@ out var parsedOrder
}
}
- private static ServiceLifestyle GetLifestyle(string toString)
- {
- // The value arrives as written in source, normally qualified: "ServiceLifetime.Scoped".
- // Parsing that whole string fails, and the silent fallback below then registered every
- // cross-wired service as a singleton regardless of the lifetime the developer asked for.
- var separatorIndex = toString.LastIndexOf('.');
-
- var value =
- separatorIndex >= 0 ? toString.Substring(separatorIndex + 1).Trim() : toString.Trim();
-
- if (Enum.TryParse(value, out ServiceLifestyle lifestyle))
- {
- return lifestyle;
- }
-
- return ServiceLifestyle.Singleton;
- }
-
private static ServiceRegistrationModel GetServiceRegistration(
SyntaxTransformContext context,
AttributeSyntax attributeSyntax,
@@ -724,8 +705,9 @@ is MemberAccessExpressionSyntax accessExpressionSyntax
}
break;
case "Using":
- registrationType = BaseSourceGenerator.GetRegistrationType(
- argumentSyntax.Expression.ToString()
+ registrationType = ConstantArgumentReader.ReadRegistrationType(
+ context,
+ argumentSyntax.Expression
);
break;
@@ -753,15 +735,9 @@ argumentSyntax.Expression is TypeOfExpressionSyntax typeOfExpression
break;
case "Order":
- if (
- int.TryParse(
- argumentSyntax.Expression.ToString(),
- out var parsedOrder
- )
- )
- {
- order = parsedOrder;
- }
+ order =
+ ConstantArgumentReader.ReadInt(context, argumentSyntax.Expression)
+ ?? 0;
break;
}
}
diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs
index 0b9dfdd..4880f5e 100644
--- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs
+++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs
@@ -371,9 +371,7 @@ out string? reason
registrationType =
argument == null
? null
- : SourceGenerator.Impl.BaseSourceGenerator.GetRegistrationType(
- argument.ToString()
- );
+ : ConstantArgumentReader.ReadRegistrationType(context, argument);
if (registrationType == null)
{
@@ -449,7 +447,14 @@ out string? reason
if (name is WithNameCall or WithoutNameCall)
{
- var patterns = ReadPatterns(context, call);
+ var patterns = ReadPatterns(context, call, out var unreadable);
+
+ if (unreadable != null)
+ {
+ reason = NotConstant(name, unreadable);
+
+ return null;
+ }
if (patterns.Count == 0)
{
@@ -473,7 +478,14 @@ out string? reason
// Read as literals for the same reason every other filter is: the declaration is
// parsed, never executed, so anything the build cannot see is refused rather than
// quietly dropped.
- var arguments = ReadPatterns(context, call);
+ var arguments = ReadPatterns(context, call, out var unreadable);
+
+ if (unreadable != null)
+ {
+ reason = NotConstant(name, unreadable);
+
+ return null;
+ }
if (arguments.Count == 0)
{
@@ -619,26 +631,41 @@ out string? reason
///
///
/// GetConstantValue rather than the literal text, so a const declared elsewhere
- /// reads as the string it evaluates to.
+ /// reads as the string it evaluates to. One argument that is not a constant refuses the whole
+ /// call, as the namespace calls do. Dropping it would read IfEnvironmentValue(key, "on")
+ /// as a test for the key on.
///
private static IReadOnlyList ReadPatterns(
SyntaxTransformContext context,
- InvocationExpressionSyntax call
+ InvocationExpressionSyntax call,
+ out ArgumentSyntax? unreadable
)
{
var values = new List();
foreach (var argument in call.ArgumentList.Arguments)
{
- if (context.SemanticModel.GetConstantValue(argument.Expression).Value is string value)
+ if (
+ context.SemanticModel.GetConstantValue(argument.Expression).Value
+ is not string value
+ )
{
- values.Add(value);
+ unreadable = argument;
+
+ return values;
}
+
+ values.Add(value);
}
+ unreadable = null;
+
return values;
}
+ private static string NotConstant(string callName, ArgumentSyntax argument) =>
+ $"'{argument}' in '{callName}' is not a compile-time constant";
+
///
/// The name of the assembly the call's marker type lives in.
///
diff --git a/tests/DependencyModules.Tests/GeneratorTests/ConstantArgumentTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ConstantArgumentTests.cs
new file mode 100644
index 0000000..a419cd1
--- /dev/null
+++ b/tests/DependencyModules.Tests/GeneratorTests/ConstantArgumentTests.cs
@@ -0,0 +1,337 @@
+using DependencyModules.Tests.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace DependencyModules.Tests.GeneratorTests;
+
+///
+/// An attribute or convention argument is read as the value it evaluates to. A constant declared
+/// elsewhere, a digit separator and a fully qualified enum member all have to give the same result
+/// as the literal.
+///
+public class ConstantArgumentTests
+{
+ private const string Preamble = """
+ using DependencyModules.Runtime.Attributes;
+ using DependencyModules.Runtime.Conventions;
+ using DependencyModules.Runtime.Interception;
+ using Microsoft.Extensions.DependencyInjection;
+
+ namespace TestNamespace;
+
+ public static class Orders
+ {
+ public const int Late = 2000;
+ }
+
+ public static class Choices
+ {
+ public const RegistrationType TryIt = RegistrationType.Try;
+ public const InterceptedMembers MethodsOnly = InterceptedMembers.Methods;
+ public const ServiceLifetime PerScope = ServiceLifetime.Scoped;
+ public const bool Yes = true;
+ }
+
+ """;
+
+ private static GeneratorResult Run(string source) =>
+ GeneratorTestHarness.Run(Preamble + source);
+
+ private static GeneratedAssembly Compile(string source) =>
+ GeneratedAssembly.Create(Preamble + source);
+
+ [Fact]
+ public void ServiceOrder_IsReadAsItsValue()
+ {
+ var assembly = Compile(
+ """
+ public interface IRule { }
+
+ [SingletonService(Order = Orders.Late)]
+ public class ConstantOrderRule : IRule { }
+
+ [SingletonService(Order = 1_000)]
+ public class SeparatorOrderRule : IRule { }
+
+ [SingletonService(Order = 5)]
+ public class LiteralOrderRule : IRule { }
+
+ [DependencyModule]
+ public partial class TestModule;
+ """
+ );
+
+ Assert.Equal(
+ new[] { "LiteralOrderRule", "SeparatorOrderRule", "ConstantOrderRule" },
+ assembly.Descriptors("IRule").Select(d => d.ImplementationType!.Name)
+ );
+ }
+
+ [Theory]
+ [InlineData("DependencyModules.Runtime.Attributes.RegistrationType.Try")]
+ [InlineData("Choices.TryIt")]
+ public void ServiceUsing_IsReadAsItsValue(string registrationType)
+ {
+ var assembly = Compile(
+ $$"""
+ public interface IClock { }
+
+ [SingletonService(Using = {{registrationType}})]
+ public class FirstClock : IClock { }
+
+ [SingletonService(Using = {{registrationType}})]
+ public class SecondClock : IClock { }
+
+ [DependencyModule]
+ public partial class TestModule;
+ """
+ );
+
+ Assert.Single(assembly.Descriptors("IClock"));
+ }
+
+ [Fact]
+ public void ModuleArguments_AreReadAsTheirValues()
+ {
+ var result = Run(
+ """
+ public interface IClock { }
+
+ [SingletonService]
+ public class FirstClock : IClock { }
+
+ [SingletonService]
+ public class SecondClock : IClock { }
+
+ [DependencyModule(Using = Choices.TryIt, GenerateFactories = Choices.Yes)]
+ public partial class TestModule;
+ """
+ );
+
+ result.AssertNoErrors();
+
+ var registrations = result.SourceContaining("Dependencies");
+
+ Assert.Contains("services.TryAddSingleton(", registrations);
+ Assert.DoesNotContain("services.AddSingleton(", registrations);
+ Assert.Contains("new global::TestNamespace.FirstClock()", registrations);
+ }
+
+ [Fact]
+ public void CrossWireLifetime_IsReadAsItsValue()
+ {
+ var assembly = Compile(
+ """
+ public interface IReader { }
+
+ [CrossWireService(Lifetime = Choices.PerScope)]
+ public class Store : IReader { }
+
+ [DependencyModule]
+ public partial class TestModule;
+ """
+ );
+
+ Assert.Equal(ServiceLifetime.Scoped, assembly.Descriptor("Store").Lifetime);
+ Assert.Equal(ServiceLifetime.Scoped, assembly.Descriptor("IReader").Lifetime);
+ }
+
+ [Fact]
+ public void DecoratorOrder_IsReadAsItsValue()
+ {
+ var assembly = Compile(
+ """
+ public interface IGreeter
+ {
+ string Greet();
+ }
+
+ [SingletonService]
+ public class Greeter : IGreeter
+ {
+ public string Greet() => "hello";
+ }
+
+ [Decorator(Order = Orders.Late)]
+ public class OuterGreeter(IGreeter inner) : IGreeter
+ {
+ public string Greet() => $"outer({inner.Greet()})";
+ }
+
+ [Decorator(Order = 1_000)]
+ public class InnerGreeter(IGreeter inner) : IGreeter
+ {
+ public string Greet() => $"inner({inner.Greet()})";
+ }
+
+ [DependencyModule]
+ public partial class TestModule;
+ """
+ );
+
+ var greeter = assembly.ResolveRequired("IGreeter");
+
+ Assert.Equal(
+ "outer(inner(hello))",
+ greeter.GetType().GetMethod("Greet")!.Invoke(greeter, null)
+ );
+ }
+
+ [Fact]
+ public void InterceptArguments_AreReadAsTheirValues()
+ {
+ var assembly = Compile(
+ """
+ public interface IGreeter
+ {
+ string Hello();
+
+ string Name { get; }
+ }
+
+ public class CountingInterceptor : IInterceptor
+ {
+ public static int Calls;
+
+ public TResult Intercept(InvocationContext context)
+ {
+ Calls++;
+ return context.Proceed();
+ }
+ }
+
+ [SingletonService]
+ [Intercept(
+ typeof(CountingInterceptor),
+ Order = Orders.Late,
+ Members = Choices.MethodsOnly,
+ Lifetime = Choices.PerScope
+ )]
+ public class Greeter : IGreeter
+ {
+ public string Hello() => "hello";
+
+ public string Name => "greeter";
+ }
+
+ [Decorator(Order = 1_000)]
+ public class LoggingGreeter(IGreeter inner) : IGreeter
+ {
+ public string Hello() => inner.Hello();
+
+ public string Name => inner.Name;
+ }
+
+ [DependencyModule]
+ public partial class TestModule;
+ """
+ );
+
+ Assert.Equal(ServiceLifetime.Scoped, assembly.Descriptor("CountingInterceptor").Lifetime);
+
+ using var scope = assembly.BuildProvider().CreateScope();
+ var greeterType = assembly.Type("IGreeter");
+ var greeter = scope.ServiceProvider.GetRequiredService(greeterType);
+
+ // Order 2000 puts the interception outside the decorator at 1000.
+ Assert.Equal("Greeter_Intercepted", greeter.GetType().Name);
+
+ greeterType.GetMethod("Hello")!.Invoke(greeter, null);
+ greeterType.GetProperty("Name")!.GetValue(greeter);
+
+ Assert.Equal(1, assembly.Type("CountingInterceptor").GetField("Calls")!.GetValue(null));
+ }
+
+ [Fact]
+ public void ConventionUsing_IsReadAsItsValue()
+ {
+ var result = Run(
+ """
+ public interface IHandler { }
+
+ public class OrderHandler : IHandler { }
+
+ [DependencyModule]
+ public partial class TestModule : IConventionModule
+ {
+ public void Conventions(IConventionDefinitions conventions)
+ {
+ conventions.RegisterAll().Using(Choices.TryIt).AsSingleton();
+ }
+ }
+ """
+ );
+
+ result.AssertNoErrors();
+
+ Assert.Contains(
+ "services.TryAddSingleton(",
+ result.SourceContaining("ConventionDependencies")
+ );
+ }
+
+ [Theory]
+ [InlineData("IfEnvironmentValue(Keys.Feature, \"on\")", "'Keys.Feature'")]
+ [InlineData("WithName(\"Order*\", Keys.InvoicePattern)", "'Keys.InvoicePattern'")]
+ [InlineData("Using(Keys.Using)", "RegistrationType")]
+ public void AConventionArgumentThatIsNotAConstant_IsReported(string call, string named)
+ {
+ var result = Run(
+ $$"""
+ public static class Keys
+ {
+ public static readonly string Feature = "FEATURE_X";
+ public static readonly string InvoicePattern = "Invoice*";
+ public static readonly RegistrationType Using = RegistrationType.Try;
+ }
+
+ public interface IHandler { }
+
+ public class OrderHandler : IHandler { }
+
+ [DependencyModule]
+ public partial class TestModule : IConventionModule
+ {
+ public void Conventions(IConventionDefinitions conventions)
+ {
+ conventions.RegisterAll().{{call}}.AsSingleton();
+ }
+ }
+ """
+ );
+
+ var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0009");
+ Assert.Contains(named, diagnostic.GetMessage());
+ }
+
+ [Fact]
+ public void AConstantConventionArgument_IsRead()
+ {
+ var result = Run(
+ """
+ public static class Keys
+ {
+ public const string Feature = "FEATURE_X";
+ }
+
+ public interface IHandler { }
+
+ public class OrderHandler : IHandler { }
+
+ [DependencyModule]
+ public partial class TestModule : IConventionModule
+ {
+ public void Conventions(IConventionDefinitions conventions)
+ {
+ conventions.RegisterAll().IfEnvironmentValue(Keys.Feature, "on").AsSingleton();
+ }
+ }
+ """
+ );
+
+ result.AssertNoErrors();
+
+ Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0009");
+ Assert.Contains("\"FEATURE_X\"", result.SourceContaining("ConventionDependencies"));
+ }
+}
diff --git a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt
index faa65b9..79c0103 100644
--- a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt
+++ b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt
@@ -1851,6 +1851,15 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities
protected abstract bool TestForTypes(Microsoft.CodeAnalysis.SyntaxNode node, System.Threading.CancellationToken token);
public bool Where(Microsoft.CodeAnalysis.SyntaxNode node, System.Threading.CancellationToken token) { }
}
+ public static class ConstantArgumentReader
+ {
+ public static bool? ReadBool(DependencyModules.SourceGenerator.Impl.Utilities.SyntaxTransformContext context, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) { }
+ public static int? ReadInt(DependencyModules.SourceGenerator.Impl.Utilities.SyntaxTransformContext context, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) { }
+ public static DependencyModules.SourceGenerator.Impl.Models.ServiceLifestyle? ReadLifetime(DependencyModules.SourceGenerator.Impl.Utilities.SyntaxTransformContext context, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) { }
+ public static DependencyModules.SourceGenerator.Impl.Models.InterceptedMemberKinds? ReadMemberKinds(DependencyModules.SourceGenerator.Impl.Utilities.SyntaxTransformContext context, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) { }
+ public static DependencyModules.SourceGenerator.Impl.Models.RegistrationType? ReadRegistrationType(DependencyModules.SourceGenerator.Impl.Utilities.SyntaxTransformContext context, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) { }
+ public static string? ReadString(DependencyModules.SourceGenerator.Impl.Utilities.SyntaxTransformContext context, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) { }
+ }
public static class ConstructorArgumentWriter
{
public static object[] Arguments(CSharpAuthor.ParameterDefinition serviceProvider, System.Collections.Generic.IReadOnlyList parameters) { }