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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[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|⚠️|❌|✔️|
|[MA0181](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0181.md)|Style|Do not use cast|ℹ️|❌|❌|
|[MA0182](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0182.md)|Design|Avoid unused internal types|ℹ️|✔️|❌|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
|[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>|❌|✔️|
|[MA0181](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0181.md)|Style|Do not use cast|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0182](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0182.md)|Design|Avoid unused internal types|<span title='Info'>ℹ️</span>|✔️|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -735,6 +736,9 @@ dotnet_diagnostic.MA0180.severity = none

# MA0181: Do not use cast
dotnet_diagnostic.MA0181.severity = none

# MA0182: Avoid unused internal types
dotnet_diagnostic.MA0182.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1276,4 +1280,7 @@ dotnet_diagnostic.MA0180.severity = none

# MA0181: Do not use cast
dotnet_diagnostic.MA0181.severity = none

# MA0182: Avoid unused internal types
dotnet_diagnostic.MA0182.severity = none
```
12 changes: 12 additions & 0 deletions docs/Rules/MA0182.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# MA0182 - Avoid unused internal types
<!-- sources -->
Source: [AvoidUnusedInternalTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs)
<!-- sources -->

This analyzer detects internal types (classes, structs, records, record structs) that are never used.

## How to fix violations

1. If the type is truly unused, remove it from the codebase.
2. If the type contains only static members, make it `static` (applies to classes only).

1 change: 1 addition & 0 deletions docs/comparison-with-other-analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
| [CA1002](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1002?WT.mc_id=DT-MVP-5003978) | [MA0016](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0016.md) | CA only applies to `List<T>` |
| [CA1003](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1003?WT.mc_id=DT-MVP-5003978) | [MA0046](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0046.md) | CA only applies to public types by default (can be configured) |
| [CA1052](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1052?WT.mc_id=DT-MVP-5003978) | [MA0036](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0036.md) | CA can be configured to only run against specific API surfaces |
| [CA1812](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1812?WT.mc_id=DT-MVP-5003978) | [MA0182](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0182.md) | MA correctly handles internal classes used as generic type arguments and in typeof() expressions |
| [CA1815](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1815?WT.mc_id=DT-MVP-5003978) | [MA0065](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0065.md) | MA reports only when Equals or GetHashCode is used |
| [CA1815](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1815?WT.mc_id=DT-MVP-5003978) | [MA0066](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0066.md) | MA reports only when the struct is used with HashSet types |
| [CA1819](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1819?WT.mc_id=DT-MVP-5003978) | [MA0016](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0016.md) | CA only applies to arrays |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,3 +538,6 @@ dotnet_diagnostic.MA0180.severity = none

# MA0181: Do not use cast
dotnet_diagnostic.MA0181.severity = none

# MA0182: Avoid unused internal types
dotnet_diagnostic.MA0182.severity = suggestion
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 @@ -538,3 +538,6 @@ dotnet_diagnostic.MA0180.severity = none

# MA0181: Do not use cast
dotnet_diagnostic.MA0181.severity = none

# MA0182: Avoid unused internal types
dotnet_diagnostic.MA0182.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 @@ -181,6 +181,7 @@ internal static class RuleIdentifiers
public const string UseAttributeIsDefined = "MA0179";
public const string ILoggerParameterTypeShouldMatchContainingType = "MA0180";
public const string DoNotUseCast = "MA0181";
public const string AvoidUnusedInternalTypes = "MA0182";

public static string GetHelpUri(string identifier)
{
Expand Down
238 changes: 238 additions & 0 deletions src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AvoidUnusedInternalTypesAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.AvoidUnusedInternalTypes,
title: "Avoid unused internal types",
messageFormat: "Internal type '{0}' is apparently never used. If so, remove it from the assembly. If this type is intended to contain only static members, make it 'static'.",
RuleCategories.Design,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.AvoidUnusedInternalTypes),
customTags: ["CompilationEnd"]);

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

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

context.RegisterCompilationStartAction(ctx =>
{
var analyzerContext = new AnalyzerContext();

ctx.RegisterSymbolAction(analyzerContext.AnalyzeNamedTypeSymbol, SymbolKind.NamedType);
ctx.RegisterSymbolAction(analyzerContext.AnalyzePropertyOrFieldSymbol, SymbolKind.Property, SymbolKind.Field);
ctx.RegisterSymbolAction(analyzerContext.AnalyzeMethodSymbol, SymbolKind.Method);
ctx.RegisterOperationAction(analyzerContext.AnalyzeObjectCreation, OperationKind.ObjectCreation);
ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
ctx.RegisterOperationAction(analyzerContext.AnalyzeArrayCreation, OperationKind.ArrayCreation);
ctx.RegisterOperationAction(analyzerContext.AnalyzeTypeOf, OperationKind.TypeOf);
ctx.RegisterOperationAction(analyzerContext.AnalyzeMemberReference, OperationKind.PropertyReference, OperationKind.FieldReference, OperationKind.MethodReference, OperationKind.EventReference);
ctx.RegisterCompilationEndAction(analyzerContext.AnalyzeCompilationEnd);
});
}

private static bool IsPotentialUnusedType(INamedTypeSymbol symbol, CancellationToken cancellationToken)
{
// Only analyze internal types
if (symbol.DeclaredAccessibility != Accessibility.Internal)
return false;

// Exclude abstract types, static types, and implicitly declared types
if (symbol.IsAbstract || symbol.IsStatic || symbol.IsImplicitlyDeclared)
return false;

// Exclude unit test classes
if (symbol.IsUnitTestClass())
return false;

// Exclude top-level statements
if (symbol.IsTopLevelStatement(cancellationToken))
return false;

return true;
}

private sealed class AnalyzerContext
{
private readonly List<ITypeSymbol> _potentialUnusedTypes = [];
private readonly HashSet<ITypeSymbol> _usedTypes = new(SymbolEqualityComparer.Default);

public void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context)
{
var symbol = (INamedTypeSymbol)context.Symbol;
if (IsPotentialUnusedType(symbol, context.CancellationToken))
{
lock (_potentialUnusedTypes)
{
_potentialUnusedTypes.Add(symbol);
}
}

// Track types used in generic constraints
foreach (var typeParameter in symbol.TypeParameters)
{
foreach (var constraintType in typeParameter.ConstraintTypes)
{
AddUsedType(constraintType);
}
}

#if CSHARP14_OR_GREATER
if(symbol.ExtensionParameter is not null)
{
AddUsedType(symbol.ExtensionParameter.Type);
}
#endif
}

public void AnalyzePropertyOrFieldSymbol(SymbolAnalysisContext context)
{
var symbol = context.Symbol;
ITypeSymbol? type = symbol switch
{
IPropertySymbol property => property.Type,
IFieldSymbol field => field.Type,
_ => null,
};

if (type is not null)
{
AddUsedType(type);
}
}

public void AnalyzeMethodSymbol(SymbolAnalysisContext context)
{
var method = (IMethodSymbol)context.Symbol;

// Track return type
if (method.ReturnType is not null)
{
AddUsedType(method.ReturnType);
}

// Track parameter types
foreach (var parameter in method.Parameters)
{
if (parameter.Type is not null)
{
AddUsedType(parameter.Type);
}
}

// Track types used in generic constraints
foreach (var typeParameter in method.TypeParameters)
{
foreach (var constraintType in typeParameter.ConstraintTypes)
{
AddUsedType(constraintType);
}
}
}

public void AnalyzeObjectCreation(OperationAnalysisContext context)
{
var operation = (IObjectCreationOperation)context.Operation;
if (operation.Type is not null)
{
AddUsedType(operation.Type);
}
}

public void AnalyzeArrayCreation(OperationAnalysisContext context)
{
var operation = (IArrayCreationOperation)context.Operation;
if (operation.Type is IArrayTypeSymbol arrayType)
{
AddUsedType(arrayType.ElementType);
}
}

public void AnalyzeInvocation(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;

// Track type arguments used in method invocations (e.g., JsonSerializer.Deserialize<T>())
foreach (var typeArgument in operation.TargetMethod.TypeArguments)
{
AddUsedType(typeArgument);
}
}

public void AnalyzeTypeOf(OperationAnalysisContext context)
{
var operation = (ITypeOfOperation)context.Operation;
if (operation.TypeOperand is not null)
{
AddUsedType(operation.TypeOperand);
}
}

public void AnalyzeMemberReference(OperationAnalysisContext context)
{
var operation = (IMemberReferenceOperation)context.Operation;

// Track type arguments in the containing type of the member being accessed
// For example: Sample<InternalClass>.Empty
if (operation.Member.ContainingType is not null)
{
AddUsedType(operation.Member.ContainingType);
}
}

public void AnalyzeCompilationEnd(CompilationAnalysisContext context)
{
foreach (var type in _potentialUnusedTypes)
{
if (_usedTypes.Contains(type))
continue;

var properties = ImmutableDictionary<string, string?>.Empty;
context.ReportDiagnostic(Diagnostic.Create(Rule, type.Locations.FirstOrDefault(), properties, type.Name));
}
}

private void AddUsedType(ITypeSymbol typeSymbol)
{
lock (_usedTypes)
{
// Prevent re-processing already seen types
if (!_usedTypes.Add(typeSymbol))
return;

// Also mark the original definition as used (in case of generic instantiations)
if (!typeSymbol.IsEqualTo(typeSymbol.OriginalDefinition))
{
AddUsedType(typeSymbol.OriginalDefinition);
}

// Handle array element types
if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol)
{
AddUsedType(arrayTypeSymbol.ElementType);
}

// Handle generic type arguments
if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
foreach (var typeArgument in namedTypeSymbol.TypeArguments)
{
AddUsedType(typeArgument);
}
}
}
}
}
}
Loading