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
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,49 @@ private static bool IsDependsOn(SyntaxNodeAnalysisContext context, out INamedTyp

namedTypeSymbol = methodSymbol.ContainingType;

if (!namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.StartsWith("global::ModularPipelines.Attributes.DependsOnAttribute"))
if (!IsDependsOnAttributeType(namedTypeSymbol, context.Compilation))
{
return false;
}

return true;
}

/// <summary>
/// Checks if the given type symbol is the DependsOnAttribute type (generic or non-generic).
/// Uses proper symbol comparison instead of string comparison.
/// </summary>
private static bool IsDependsOnAttributeType(INamedTypeSymbol typeSymbol, Compilation compilation)
{
// Get the non-generic DependsOnAttribute type
var dependsOnAttributeType = compilation.GetTypeByMetadataName("ModularPipelines.Attributes.DependsOnAttribute");
if (dependsOnAttributeType is null)
{
return false;
}

// For generic types, compare the original definition
var typeToCompare = typeSymbol.IsGenericType
? typeSymbol.OriginalDefinition
: typeSymbol;

// Check if it's the non-generic version
if (SymbolEqualityComparer.Default.Equals(typeToCompare, dependsOnAttributeType))
{
return true;
}

// Get and check the generic version (DependsOnAttribute`1)
var genericDependsOnAttributeType = compilation.GetTypeByMetadataName("ModularPipelines.Attributes.DependsOnAttribute`1");
return genericDependsOnAttributeType is not null &&
SymbolEqualityComparer.Default.Equals(typeToCompare, genericDependsOnAttributeType);
}

private static void ReportDiagnostics(SyntaxNodeAnalysisContext context, IEnumerable<AttributeData> allAttributesOnDependentType,
INamedTypeSymbol typeContainingAttribute, INamedTypeSymbol namedArgumentTypeSymbol)
{
foreach (var conflictingDependencyAttribute in allAttributesOnDependentType.Where(x =>
x.IsDependsOnAttributeFor(typeContainingAttribute)))
x.IsDependsOnAttributeFor(context.Compilation, typeContainingAttribute)))
{
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(),
namedArgumentTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,100 @@ namespace ModularPipelines.Analyzers.Extensions;
[ExcludeFromCodeCoverage]
internal static class SymbolExtensions
{
/// <summary>
/// Checks if the given type symbol inherits from or is the specified type by metadata name.
/// Uses proper symbol comparison instead of string comparison.
/// </summary>
internal static bool InheritsFrom(this ITypeSymbol? typeSymbol, Compilation compilation, string fullyQualifiedMetadataName)
{
if (typeSymbol is null)
{
return false;
}

var targetType = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName);
if (targetType is null)
{
return false;
}

return typeSymbol.InheritsFrom(targetType);
}

/// <summary>
/// Checks if the given type symbol inherits from or is the specified target type.
/// </summary>
internal static bool InheritsFrom(this ITypeSymbol? typeSymbol, INamedTypeSymbol? targetType)
{
if (typeSymbol is null || targetType is null)
{
return false;
}

var current = typeSymbol;
while (current != null)
{
if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, targetType.OriginalDefinition))
{
return true;
}

current = current.BaseType;
}

return false;
}

/// <summary>
/// Checks if the type symbol matches the specified type by metadata name.
/// Handles both generic and non-generic types using OriginalDefinition for generic type comparison.
/// </summary>
internal static bool IsType(this ITypeSymbol? typeSymbol, Compilation compilation, string fullyQualifiedMetadataName)
{
if (typeSymbol is null)
{
return false;
}

var targetType = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName);
if (targetType is null)
{
return false;
}

// Use OriginalDefinition for generic types (e.g., ILogger<T> matches ILogger<>)
return SymbolEqualityComparer.Default.Equals(typeSymbol.OriginalDefinition, targetType.OriginalDefinition);
}

/// <summary>
/// Checks if the type symbol matches any of the specified types by metadata name.
/// </summary>
internal static bool IsAnyType(this ITypeSymbol? typeSymbol, Compilation compilation, params string[] fullyQualifiedMetadataNames)
{
return typeSymbol.IsAnyType(compilation, fullyQualifiedMetadataNames.AsSpan());
}

/// <summary>
/// Checks if the type symbol matches any of the specified types by metadata name.
/// </summary>
internal static bool IsAnyType(this ITypeSymbol? typeSymbol, Compilation compilation, ReadOnlySpan<string> fullyQualifiedMetadataNames)
{
if (typeSymbol is null)
{
return false;
}

foreach (var metadataName in fullyQualifiedMetadataNames)
{
if (typeSymbol.IsType(compilation, metadataName))
{
return true;
}
}

return false;
}

internal static IEnumerable<AttributeData> GetAllAttributesIncludingBaseAndInterfaces(this INamedTypeSymbol classSymbol)
{
foreach (var attributeData in classSymbol.AllInterfaces.SelectMany(x => x.GetAttributes()))
Expand Down Expand Up @@ -42,36 +136,27 @@ internal static IEnumerable<INamedTypeSymbol> GetSelfAndAllBaseTypes(this INamed
}
}

internal static bool IsDependsOnAttributeFor(this AttributeData attributeData, INamedTypeSymbol namedTypeSymbol)
internal static bool IsDependsOnAttributeFor(this AttributeData attributeData, Compilation compilation, INamedTypeSymbol namedTypeSymbol)
{
var attributeClassName = attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

if (string.IsNullOrEmpty(attributeClassName))
var attributeClass = attributeData.AttributeClass;
if (attributeClass is null)
{
return false;
}

if (!attributeClassName!.StartsWith("global::ModularPipelines.Attributes.DependsOnAttribute"))
if (!IsDependsOnAttribute(attributeClass, compilation))
{
return false;
}

if (attributeData.AttributeClass!.IsGenericType)
if (attributeClass.IsGenericType)
{
return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass.TypeArguments.First(), namedTypeSymbol);
return SymbolEqualityComparer.Default.Equals(attributeClass.TypeArguments.First(), namedTypeSymbol);
}

return attributeData.ConstructorArguments.Any(x =>
{
var argumentValue = x.Value;

if (argumentValue is INamedTypeSymbol argumentNamedTypeSymbol)
{
return SymbolEqualityComparer.Default.Equals(argumentNamedTypeSymbol, namedTypeSymbol);
}

return false;
});
x.Value is INamedTypeSymbol argumentNamedTypeSymbol &&
SymbolEqualityComparer.Default.Equals(argumentNamedTypeSymbol, namedTypeSymbol));
}

internal static INamedTypeSymbol? GetClassThatNodeIsIn(this SyntaxNodeAnalysisContext context)
Expand All @@ -90,4 +175,34 @@ internal static bool IsDependsOnAttributeFor(this AttributeData attributeData, I

return context.SemanticModel.GetDeclaredSymbol(node) as INamedTypeSymbol;
}

/// <summary>
/// Checks if the given type symbol is the DependsOnAttribute type (generic or non-generic).
/// Uses proper symbol comparison instead of string comparison.
/// </summary>
private static bool IsDependsOnAttribute(INamedTypeSymbol attributeClass, Compilation compilation)
{
// Get the non-generic DependsOnAttribute type
var dependsOnAttributeType = compilation.GetTypeByMetadataName("ModularPipelines.Attributes.DependsOnAttribute");
if (dependsOnAttributeType is null)
{
return false;
}

// For generic types, compare the original definition (DependsOnAttribute<T> -> DependsOnAttribute<>)
var attributeToCompare = attributeClass.IsGenericType
? attributeClass.OriginalDefinition
: attributeClass;

// Check if it's the non-generic version
if (SymbolEqualityComparer.Default.Equals(attributeToCompare, dependsOnAttributeType))
{
return true;
}

// Get and check the generic version (DependsOnAttribute`1)
var genericDependsOnAttributeType = compilation.GetTypeByMetadataName("ModularPipelines.Attributes.DependsOnAttribute`1");
return genericDependsOnAttributeType is not null &&
SymbolEqualityComparer.Default.Equals(attributeToCompare, genericDependsOnAttributeType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using ModularPipelines.Analyzers.Extensions;

namespace ModularPipelines.Analyzers;

Expand All @@ -21,6 +22,15 @@ public class LoggerInConstructorAnalyzer : DiagnosticAnalyzer
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.LoggerInConstructorAnalyzerDescription), Resources.ResourceManager, typeof(Resources));
private static readonly DiagnosticDescriptor PrivateRule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

/// <summary>
/// Logging types from Microsoft.Extensions.Logging that should not be injected directly.
/// </summary>
private static readonly ImmutableArray<string> LoggingTypeMetadataNames = ImmutableArray.Create(
"Microsoft.Extensions.Logging.ILogger`1",
"Microsoft.Extensions.Logging.ILogger",
"Microsoft.Extensions.Logging.ILoggerProvider",
"Microsoft.Extensions.Logging.ILoggerFactory");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

Expand All @@ -33,7 +43,7 @@ public override void Initialize(AnalysisContext context)
context.RegisterSyntaxNodeAction(AnalyzeLoggersInConstructors, SyntaxKind.ConstructorDeclaration);
}

private void AnalyzeLoggersInConstructors(SyntaxNodeAnalysisContext context)
private static void AnalyzeLoggersInConstructors(SyntaxNodeAnalysisContext context)
{
if (context.Node is not ConstructorDeclarationSyntax constructorDeclarationSyntax)
{
Expand All @@ -42,75 +52,36 @@ private void AnalyzeLoggersInConstructors(SyntaxNodeAnalysisContext context)

foreach (var parameter in constructorDeclarationSyntax.ParameterList.Parameters)
{
if (IsGenericILogger(context, parameter, out var parameterSymbol))
if (TryGetProhibitedLoggerType(context, parameter, out var parameterSymbol))
{
ReportDiagnostic(context, parameter.GetLocation(), parameterSymbol!);
return;
}

foreach (var injectedLoggerType in ImmutableArray.Create("ILogger", "ILoggerProvider", "ILoggerFactory"))
{
if (IsNonGenericType(context, parameter, injectedLoggerType, out parameterSymbol))
{
ReportDiagnostic(context, parameter.GetLocation(), parameterSymbol!);
return;
}
}
}
}

private static bool IsGenericILogger(SyntaxNodeAnalysisContext context, ParameterSyntax parameter, out INamedTypeSymbol? parameterSymbol)
/// <summary>
/// Checks if the parameter type is a prohibited logging type from Microsoft.Extensions.Logging.
/// </summary>
private static bool TryGetProhibitedLoggerType(
SyntaxNodeAnalysisContext context,
ParameterSyntax parameter,
out INamedTypeSymbol? parameterSymbol)
{
parameterSymbol = null;

if (parameter.Type is not GenericNameSyntax genericNameSyntax)
{
return false;
}

if (genericNameSyntax.Identifier.ValueText is not "ILogger")
{
return false;
}

var genericArgumentSymbol = context.SemanticModel.GetSymbolInfo(genericNameSyntax).Symbol;

if (genericArgumentSymbol is not INamedTypeSymbol namedTypeSymbol)
{
return false;
}

if (!genericArgumentSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).StartsWith("global::Microsoft.Extensions.Logging.ILogger<"))
if (parameter.Type is null)
{
return false;
}

parameterSymbol = namedTypeSymbol;
return true;
}

private static bool IsNonGenericType(SyntaxNodeAnalysisContext context, ParameterSyntax parameter, string name, out INamedTypeSymbol? parameterSymbol)
{
parameterSymbol = null;

if (parameter.Type is not IdentifierNameSyntax identifierNameSyntax)
{
return false;
}

if (identifierNameSyntax.Identifier.ValueText != name)
{
return false;
}

var genericArgumentSymbol = context.SemanticModel.GetSymbolInfo(identifierNameSyntax).Symbol;

if (genericArgumentSymbol is not INamedTypeSymbol namedTypeSymbol)
var typeSymbol = context.SemanticModel.GetSymbolInfo(parameter.Type).Symbol;
if (typeSymbol is not INamedTypeSymbol namedTypeSymbol)
{
return false;
}

if (!genericArgumentSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).StartsWith($"global::Microsoft.Extensions.Logging.{name}"))
if (!namedTypeSymbol.IsAnyType(context.Compilation, LoggingTypeMetadataNames.AsSpan()))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private void AnalyzeMissingDependsOnAttributes(SyntaxNodeAnalysisContext context

var attributes = classSymbol.GetAllAttributesIncludingBaseAndInterfaces();

if (!attributes.Any(x => x.IsDependsOnAttributeFor(namedTypeSymbol)))
if (!attributes.Any(x => x.IsDependsOnAttributeFor(context.Compilation, namedTypeSymbol)))
{
var properties = new Dictionary<string, string?>
{
Expand Down
Loading