From 0d4057edb0c6ad8645e55d58c6bdfbc513682c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 9 Jan 2026 00:12:03 -0500 Subject: [PATCH 1/2] Extend MA0182 to all types --- .../Rules/AvoidUnusedInternalTypesAnalyzer.cs | 21 +- .../AvoidUnusedInternalTypesAnalyzerTests.cs | 314 +++++++++++++++++- 2 files changed, 324 insertions(+), 11 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs index 430d14d55..b6eea7363 100644 --- a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs @@ -45,12 +45,15 @@ public override void Initialize(AnalysisContext context) private static bool IsPotentialUnusedType(INamedTypeSymbol symbol, CancellationToken cancellationToken) { - // Only analyze internal types - if (symbol.DeclaredAccessibility != Accessibility.Internal) + // Only analyze types not visible outside of assembly + if (symbol.IsVisibleOutsideOfAssembly()) return false; - // Exclude abstract types, static types, and implicitly declared types - if (symbol.IsAbstract || symbol.IsStatic || symbol.IsImplicitlyDeclared) + // Exclude compiler-generated types (e.g., extension types, anonymous types) + if (!symbol.CanBeReferencedByName) + return false; + + if (symbol.IsStatic || symbol.IsImplicitlyDeclared) return false; // Exclude unit test classes @@ -90,7 +93,7 @@ public void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context) } #if CSHARP14_OR_GREATER - if(symbol.ExtensionParameter is not null) + if (symbol.ExtensionParameter is not null) { AddUsedType(symbol.ExtensionParameter.Type); } @@ -116,13 +119,13 @@ public void AnalyzePropertyOrFieldSymbol(SymbolAnalysisContext context) 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) { @@ -163,7 +166,7 @@ public void AnalyzeArrayCreation(OperationAnalysisContext context) public void AnalyzeInvocation(OperationAnalysisContext context) { var operation = (IInvocationOperation)context.Operation; - + // Track type arguments used in method invocations (e.g., JsonSerializer.Deserialize()) foreach (var typeArgument in operation.TargetMethod.TypeArguments) { @@ -183,7 +186,7 @@ public void AnalyzeTypeOf(OperationAnalysisContext context) 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.Empty if (operation.Member.ContainingType is not null) diff --git a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs index cf4074f19..8b2ebfdb7 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs @@ -30,7 +30,7 @@ await CreateProjectBuilder() public async Task AbstractClass_NoDiagnostic() { const string SourceCode = """ - internal abstract class AbstractClass + internal abstract class [|AbstractClass|] { public abstract void Method(); } @@ -58,7 +58,7 @@ await CreateProjectBuilder() public async Task Interface_NoDiagnostic() { const string SourceCode = """ - internal interface ITest + internal interface [|ITest|] { void Method(); } @@ -97,6 +97,147 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task UnusedPrivateNestedClass_Diagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private class [|UnusedNestedClass|] + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UnusedInternalNestedClass_Diagnostic() + { + const string SourceCode = """ + public class OuterClass + { + internal class [|UnusedNestedClass|] + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UnusedProtectedNestedClass_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + protected class UnusedNestedClass + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UnusedProtectedInternalNestedClass_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + protected internal class UnusedNestedClass + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UnusedPrivateProtectedNestedClass_Diagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private protected class [|UnusedNestedClass|] + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UsedPrivateNestedClass_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private class UsedNestedClass + { + public string Name { get; set; } + } + + public void Method() + { + var obj = new UsedNestedClass(); + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task PublicNestedClass_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + public class NestedClass + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task PrivateNestedClassInInternalClass_Diagnostic() + { + const string SourceCode = """ + internal class [|OuterClass|] + { + private class [|UnusedNestedClass|] + { + public string Name { get; set; } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + [Fact] public async Task UnusedInternalStruct_Diagnostic() { @@ -111,6 +252,45 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task UnusedPrivateNestedStruct_Diagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private struct [|UnusedNestedStruct|] + { + public int Value; + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UsedPrivateNestedStruct_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private struct UsedNestedStruct + { + public int Value; + } + + public void Method() + { + var obj = new UsedNestedStruct(); + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + [Fact] public async Task UnusedInternalRecord_Diagnostic() { @@ -126,6 +306,41 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task UnusedPrivateNestedRecord_Diagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private record [|UnusedNestedRecord|](string Name); + } + """; + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UsedPrivateNestedRecord_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private record UsedNestedRecord(string Name); + + public void Method() + { + var obj = new UsedNestedRecord("Test"); + } + } + """; + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + #if CSHARP10_OR_GREATER [Fact] public async Task UnusedInternalRecordStruct_Diagnostic() @@ -141,6 +356,41 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .ValidateAsync(); } + + [Fact] + public async Task UnusedPrivateNestedRecordStruct_Diagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private record struct [|UnusedNestedRecordStruct|](int Id); + } + """; + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task UsedPrivateNestedRecordStruct_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private record struct UsedNestedRecordStruct(int Id); + + public void Method() + { + var obj = new UsedNestedRecordStruct(42); + } + } + """; + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(SourceCode) + .ValidateAsync(); + } #endif [Fact] @@ -1002,4 +1252,64 @@ await CreateProjectBuilder() .ValidateAsync(); } #endif + + [Fact] + public async Task DeeplyNestedPrivateClass_Diagnostic() + { + const string SourceCode = """ + public class Level1 + { + public class Level2 + { + private class [|Level3|] + { + public string Name { get; set; } + } + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task PrivateNestedClassUsedInSameType_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private class NestedClass + { + public string Name { get; set; } + } + + private NestedClass _field; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task PrivateNestedClassUsedAsMethodParameter_NoDiagnostic() + { + const string SourceCode = """ + public class OuterClass + { + private class NestedClass + { + public string Name { get; set; } + } + + private void Method(NestedClass parameter) + { + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } } From 7167ab85d5cc25d6a1450f934be679428b8f7dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 9 Jan 2026 07:57:25 -0500 Subject: [PATCH 2/2] Check for self referencing constraints --- .../Rules/AvoidUnusedInternalTypesAnalyzer.cs | 35 ++++++++++- .../AvoidUnusedInternalTypesAnalyzerTests.cs | 58 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs index b6eea7363..cc00cbf56 100644 --- a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs @@ -83,12 +83,31 @@ public void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context) } } + // Track base type (skip system types) + if (symbol.BaseType is not null && !symbol.BaseType.IsVisibleOutsideOfAssembly()) + { + AddUsedType(symbol.BaseType); + } + + // Track implemented interfaces (skip system interfaces) + foreach (var @interface in symbol.Interfaces) + { + if (!@interface.IsVisibleOutsideOfAssembly()) + { + AddUsedType(@interface); + } + } + // Track types used in generic constraints foreach (var typeParameter in symbol.TypeParameters) { foreach (var constraintType in typeParameter.ConstraintTypes) { - AddUsedType(constraintType); + // Skip self-referencing constraints (e.g., INumber where TSelf : INumber) + if (!IsSelfReferencingConstraint(symbol, constraintType)) + { + AddUsedType(constraintType); + } } } @@ -100,6 +119,20 @@ public void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context) #endif } + private static bool IsSelfReferencingConstraint(INamedTypeSymbol declaringType, ITypeSymbol constraintType) + { + // Check if the constraint type is a generic instantiation of the declaring type + // For example: INumber where TSelf : INumber + if (constraintType is INamedTypeSymbol namedConstraint) + { + // Check if the original definitions match + if (SymbolEqualityComparer.Default.Equals(namedConstraint.OriginalDefinition, declaringType)) + return true; + } + + return false; + } + public void AnalyzePropertyOrFieldSymbol(SymbolAnalysisContext context) { var symbol = context.Symbol; diff --git a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs index 8b2ebfdb7..8d99e5266 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs @@ -1312,4 +1312,62 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .ValidateAsync(); } + + [Fact] + public async Task SelfReferencingInterface_Diagnostic() + { + const string SourceCode = """ + internal interface [|INumber|] where TSelf : INumber + { + TSelf Add(TSelf other); + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task SelfReferencingInterfaceUsedByType_NoDiagnostic() + { + const string SourceCode = """ + internal interface INumber where TSelf : INumber + { + TSelf Add(TSelf other); + } + + internal class MyNumber : INumber + { + public MyNumber Add(MyNumber other) => this; + } + + public class Consumer + { + public void Method() + { + var num = new MyNumber(); + num.Add(num); + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task SelfReferencingInterfaceWithMultipleConstraints_Diagnostic() + { + const string SourceCode = """ + using System; + + internal interface [|IComparable|] where TSelf : IComparable, IEquatable + { + int CompareTo(TSelf other); + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } }