Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️|
|[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|ℹ️|✔️|✔️|
|[MA0179](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0179.md)|Performance|Use Attribute.IsDefined instead of GetCustomAttribute(s)|ℹ️|✔️|✔️|
|[MA0180](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0180.md)|Design|ILogger type parameter should match containing type|⚠️|❌|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0179](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0179.md)|Performance|Use Attribute.IsDefined instead of GetCustomAttribute(s)|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0180](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0180.md)|Design|ILogger type parameter should match containing type|<span title='Warning'>⚠️</span>|❌|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -724,6 +725,9 @@ dotnet_diagnostic.MA0178.severity = suggestion

# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s)
dotnet_diagnostic.MA0179.severity = suggestion

# MA0180: ILogger type parameter should match containing type
dotnet_diagnostic.MA0180.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1262,4 +1266,7 @@ dotnet_diagnostic.MA0178.severity = none

# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s)
dotnet_diagnostic.MA0179.severity = none

# MA0180: ILogger type parameter should match containing type
dotnet_diagnostic.MA0180.severity = none
```
45 changes: 45 additions & 0 deletions docs/Rules/MA0180.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# MA0180 - ILogger type parameter should match containing type
<!-- sources -->
Sources: [ILoggerParameterTypeShouldMatchContainingTypeAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ILoggerParameterTypeShouldMatchContainingTypeAnalyzer.cs), [ILoggerParameterTypeShouldMatchContainingTypeFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/ILoggerParameterTypeShouldMatchContainingTypeFixer.cs)
<!-- sources -->

## Description

When using `ILogger<T>` in a class constructor, the type parameter should match the containing class. This helps ensure proper categorization of log messages and makes it easier to filter logs by component.

## Non-compliant code

```csharp
using Microsoft.Extensions.Logging;

class A(ILogger<B> logger)
{
}

class B
{
}
```

## Compliant code

```csharp
using Microsoft.Extensions.Logging;

class A(ILogger<A> logger)
{
}

class B
{
}
```

## Configuration

This rule is disabled by default. You can enable it by setting the severity in your `.editorconfig` file:

```editorconfig
# MA0180: ILogger type parameter should match containing type
dotnet_diagnostic.MA0180.severity = warning
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Immutable;
using System.Composition;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class ILoggerParameterTypeShouldMatchContainingTypeFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.ILoggerParameterTypeShouldMatchContainingType);

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true);
if (nodeToFix is null)
return;

var title = "Use ILogger with matching type parameter";
var codeAction = CodeAction.Create(
title,
ct => FixAsync(context.Document, nodeToFix, ct),
equivalenceKey: title);

context.RegisterCodeFix(codeAction, context.Diagnostics);
}

private static async Task<Document> FixAsync(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var semanticModel = editor.SemanticModel;

// Find the parameter that contains the node
var parameter = nodeToFix.AncestorsAndSelf().OfType<ParameterSyntax>().FirstOrDefault();
if (parameter is null)
return document;

// Get the ILogger<T> type
var parameterType = parameter.Type;
if (parameterType is not GenericNameSyntax genericType)
return document;

// Find the containing type by traversing up the syntax tree
var containingTypeDeclaration = parameter.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
if (containingTypeDeclaration is null)
return document;

// Get the symbol for the containing type
var containingTypeSymbol = semanticModel.GetDeclaredSymbol(containingTypeDeclaration, cancellationToken);
if (containingTypeSymbol is null)
return document;

// Create new type argument
var generator = editor.Generator;
var newTypeArgument = generator.TypeExpression(containingTypeSymbol);

// Create new generic type
var newGenericType = genericType.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList((TypeSyntax)newTypeArgument)));

editor.ReplaceNode(genericType, newGenericType);
return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,6 @@ dotnet_diagnostic.MA0178.severity = suggestion

# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s)
dotnet_diagnostic.MA0179.severity = suggestion

# MA0180: ILogger type parameter should match containing type
dotnet_diagnostic.MA0180.severity = none
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,6 @@ dotnet_diagnostic.MA0178.severity = none

# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s)
dotnet_diagnostic.MA0179.severity = none

# MA0180: ILogger type parameter should match containing type
dotnet_diagnostic.MA0180.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ internal static class RuleIdentifiers
public const string UseSingleLineXmlCommentSyntaxWhenPossible = "MA0177";
public const string UseTimeSpanZero = "MA0178";
public const string UseAttributeIsDefined = "MA0179";
public const string ILoggerParameterTypeShouldMatchContainingType = "MA0180";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ILoggerParameterTypeShouldMatchContainingTypeAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.ILoggerParameterTypeShouldMatchContainingType,
title: "ILogger type parameter should match containing type",
messageFormat: "ILogger type parameter should be '{0}' instead of '{1}'",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.ILoggerParameterTypeShouldMatchContainingType));

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

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(compilationContext =>
{
var iloggerSymbol = compilationContext.Compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger`1");
if (iloggerSymbol is null)
return;

compilationContext.RegisterSymbolAction(context => AnalyzeNamedType(context, iloggerSymbol), SymbolKind.NamedType);
});
}

private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol iloggerSymbol)
{
var namedType = (INamedTypeSymbol)context.Symbol;

// Skip interfaces, abstract classes, etc. - only check concrete types
if (namedType.TypeKind != TypeKind.Class || namedType.IsAbstract)
return;

// Check all constructors (including primary constructors)
foreach (var constructor in namedType.Constructors)
{
// Skip implicitly declared constructors
if (constructor.IsImplicitlyDeclared)
continue;

foreach (var parameter in constructor.Parameters)
{
// Check if parameter type is ILogger<T>
if (parameter.Type is INamedTypeSymbol { IsGenericType: true } parameterType &&
parameterType.OriginalDefinition.IsEqualTo(iloggerSymbol))
{
// Get the type argument of ILogger<T>
var typeArgument = parameterType.TypeArguments[0];

// Check if it matches the containing type
if (!typeArgument.IsEqualTo(namedType))
{
context.ReportDiagnostic(
Rule,
parameter,
DiagnosticParameterReportOptions.ReportOnType,
namedType.Name,
typeArgument.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
}
}
}
}
}
}
Loading