Skip to content
Merged
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
32 changes: 22 additions & 10 deletions src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using DependencyModules.SourceGenerator.Impl.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace DependencyModules.SourceGenerator.Impl.Utilities;

/// <summary>
/// Reads the value of an argument from the semantic model, not from its source text.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;

/// <remarks>
/// The generator's <see cref="RegistrationType"/> has the member order of the runtime's.
/// </remarks>
public static RegistrationType? ReadRegistrationType(
SyntaxTransformContext context,
ExpressionSyntax expression
) =>
ReadInt(context, expression) is { } value && Enum.IsDefined(typeof(RegistrationType), value)
? (RegistrationType)value
: null;

/// <remarks>
/// <c>ServiceLifetime</c> numbers its members Singleton, Scoped, Transient, which is the reverse
/// of <see cref="ServiceLifestyle"/>.
/// </remarks>
public static ServiceLifestyle? ReadLifetime(
SyntaxTransformContext context,
ExpressionSyntax expression
) =>
ReadInt(context, expression) switch
{
0 => ServiceLifestyle.Singleton,
1 => ServiceLifestyle.Scoped,
2 => ServiceLifestyle.Transient,
_ => null,
};

/// <remarks>
/// <see cref="InterceptedMemberKinds"/> has the flag values of the runtime's
/// <c>InterceptedMembers</c>.
/// </remarks>
public static InterceptedMemberKinds? ReadMemberKinds(
SyntaxTransformContext context,
ExpressionSyntax expression
) =>
ReadInt(context, expression) is { } value
? (InterceptedMemberKinds)value & InterceptedMemberKinds.All
: null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ is not INamedTypeSymbol implementationSymbol
ref realm
);

memberKinds &= ReadMemberKinds(attribute);
memberKinds &= ReadMemberKinds(attribute, context);
}

if (interceptorSymbols.Count == 0)
Expand Down Expand Up @@ -245,13 +245,10 @@ CancellationToken cancellationToken
/// <summary>
/// The kinds of member <c>[Intercept]</c> names, as flags. Everything when it names none.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static InterceptedMemberKinds ReadMemberKinds(AttributeSyntax attribute)
private static InterceptedMemberKinds ReadMemberKinds(
AttributeSyntax attribute,
SyntaxTransformContext context
)
{
foreach (
var argument in attribute.ArgumentList?.Arguments
Expand All @@ -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;
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
Expand All @@ -495,35 +467,20 @@ ref INamedTypeSymbol? realm
/// The lifetime one [Intercept] names for the interceptors it lists, Singleton when it names
/// none.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute)
private static ServiceLifestyle ReadLifetime(
AttributeSyntax attribute,
SyntaxTransformContext context
)
{
foreach (
var argument in attribute.ArgumentList?.Arguments
?? default(SeparatedSyntaxList<AttributeArgumentSyntax>)
)
{
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -724,8 +705,9 @@ is MemberAccessExpressionSyntax accessExpressionSyntax
}
break;
case "Using":
registrationType = BaseSourceGenerator.GetRegistrationType(
argumentSyntax.Expression.ToString()
registrationType = ConstantArgumentReader.ReadRegistrationType(
context,
argumentSyntax.Expression
);
break;

Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading
Loading