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
96 changes: 5 additions & 91 deletions DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using DesignPatterns.Analyzers.Di;
using DesignPatterns.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -37,13 +38,6 @@ public override void Initialize(AnalysisContext context)
context.RegisterCompilationStartAction(OnCompilationStart);
}

private enum Lifetime
{
Singleton = 0,
Scoped = 1,
Transient = 2,
}

/// <summary>
/// Categories of DesignPatterns registration attributes.
/// Used to match attributed types to the correct RegisterDi call
Expand Down Expand Up @@ -456,8 +450,8 @@ private static void CollectTryAddRegistration(
return;
}

// Third arg: ServiceLifetime.Singleton
var lifetime = ResolveLifetimeArg(descriptorArgs[2].Expression, context.SemanticModel);
// Third arg: lifetime constant (e.g. Singleton)
var lifetime = LifetimeResolution.TryResolve(descriptorArgs[2].Expression, context.SemanticModel);
if (lifetime is null)
{
return;
Expand Down Expand Up @@ -513,7 +507,8 @@ private static void CollectRegisterDiRegistration(
}

// Resolve the lifetime value from the call arguments.
var lifetime = ResolveLifetimeFromArg(invocation, implLifetimeParam, context.SemanticModel);
var lifetime = LifetimeResolution.TryResolveArgument(
invocation, implLifetimeParam, context.SemanticModel);
var containingTypeName = methodSymbol.ContainingType?.Name ?? "";
if (lifetime is null)
{
Expand Down Expand Up @@ -588,54 +583,6 @@ private static void CollectRegisterDiRegistration(
return null;
}

/// <summary>
/// Resolves a ServiceLifetime argument from an invocation by parameter name.
/// </summary>
private static Lifetime? ResolveLifetimeFromArg(
InvocationExpressionSyntax invocation,
IParameterSymbol parameter,
SemanticModel semanticModel)
{
var argList = invocation.ArgumentList;
if (argList is null)
{
return null;
}

// Check named arguments first.
ArgumentSyntax? matchedArg = null;
foreach (var arg in argList.Arguments)
{
if (arg.NameColon is { Name.Identifier.ValueText: var name } &&
name == parameter.Name)
{
matchedArg = arg;
break;
}
}

// Fall back to positional argument.
if (matchedArg is null)
{
var index = parameter.Ordinal;
if (index < argList.Arguments.Count)
{
var arg = argList.Arguments[index];
if (arg.NameColon is null)
{
matchedArg = arg;
}
}
}

if (matchedArg is null)
{
return null;
}

return ResolveLifetimeArg(matchedArg.Expression, semanticModel);
}

private static void AnalyzeRegistrations(
CompilationAnalysisContext context,
List<RegistrationEntry> registrations,
Expand Down Expand Up @@ -793,39 +740,6 @@ private static void AnalyzeSingleton(
return typeInfo.Type as INamedTypeSymbol;
}

private static Lifetime? ResolveLifetimeArg(
ExpressionSyntax expr,
SemanticModel semanticModel)
{
// Try constant value (enum member → int)
var constantValue = semanticModel.GetConstantValue(expr);
if (constantValue.HasValue && constantValue.Value is int intValue)
{
return intValue switch
{
0 => Lifetime.Singleton,
1 => Lifetime.Scoped,
2 => Lifetime.Transient,
_ => null,
};
}

// Try field symbol (ServiceLifetime.Singleton etc.)
var symbol = semanticModel.GetSymbolInfo(expr).Symbol;
if (symbol is IFieldSymbol field && field.HasConstantValue)
{
return field.ConstantValue switch
{
0 => Lifetime.Singleton,
1 => Lifetime.Scoped,
2 => Lifetime.Transient,
_ => null,
};
}

return null;
}

private static bool IsFactoryOrInstanceArg(
ArgumentSyntax arg,
SemanticModel semanticModel)
Expand Down
4 changes: 4 additions & 0 deletions DesignPatterns.Analyzers/DesignPatterns.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
<ProjectReference Include="..\DesignPatterns.Diagnostics\DesignPatterns.Diagnostics.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="DesignPatterns.Analyzers.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
13 changes: 13 additions & 0 deletions DesignPatterns.Analyzers/Di/Lifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace DesignPatterns.Analyzers.Di;

/// <summary>
/// DI container lifetime aligned with common meaning (Singleton / Scoped / Transient).
/// Numeric values match <c>Microsoft.Extensions.DependencyInjection.ServiceLifetime</c>
/// so constant folding of that enum can be mapped directly.
/// </summary>
internal enum Lifetime
{
Singleton = 0,
Scoped = 1,
Transient = 2,
}
91 changes: 91 additions & 0 deletions DesignPatterns.Analyzers/Di/LifetimeResolution.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace DesignPatterns.Analyzers.Di;

/// <summary>
/// Shared primitives for resolving DI lifetime arguments from Roslyn syntax.
/// </summary>
internal static class LifetimeResolution
{
/// <summary>
/// Resolves a <see cref="Lifetime"/> from an expression that is a compile-time
/// constant matching MSDI <c>ServiceLifetime</c> values (0/1/2) or a constant field.
/// Returns <see langword="null"/> when the value cannot be determined.
/// </summary>
internal static Lifetime? TryResolve(ExpressionSyntax expression, SemanticModel semanticModel)
{
var constantValue = semanticModel.GetConstantValue(expression);
if (constantValue.HasValue && constantValue.Value is int intValue)
{
return FromInt(intValue);
}

var symbol = semanticModel.GetSymbolInfo(expression).Symbol;
if (symbol is IFieldSymbol field && field.HasConstantValue)
{
return field.ConstantValue is int fieldValue ? FromInt(fieldValue) : null;
}

return null;
}

/// <summary>
/// Resolves a lifetime argument on an invocation by matching the given parameter
/// (named argument first, then positional). Returns <see langword="null"/> when
/// the argument is omitted or its value cannot be resolved.
/// </summary>
internal static Lifetime? TryResolveArgument(
InvocationExpressionSyntax invocation,
IParameterSymbol parameter,
SemanticModel semanticModel)
{
var argument = FindArgument(invocation.ArgumentList, parameter);
if (argument is null)
{
return null;
}

return TryResolve(argument.Expression, semanticModel);
}

private static ArgumentSyntax? FindArgument(
ArgumentListSyntax? argumentList,
IParameterSymbol parameter)
{
if (argumentList is null)
{
return null;
}

foreach (var arg in argumentList.Arguments)
{
if (arg.NameColon is { Name.Identifier.ValueText: var name } &&
name == parameter.Name)
{
return arg;
}
}

var index = parameter.Ordinal;
if (index < argumentList.Arguments.Count)
{
var arg = argumentList.Arguments[index];
if (arg.NameColon is null)
{
return arg;
}
}

return null;
}

private static Lifetime? FromInt(int value) =>
value switch
{
0 => Lifetime.Singleton,
1 => Lifetime.Scoped,
2 => Lifetime.Transient,
_ => null,
};
}
Loading
Loading