diff --git a/README.md b/README.md
index 54e55b57..a9b98505 100755
--- a/README.md
+++ b/README.md
@@ -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|ℹ️|✔️|❌|
diff --git a/docs/README.md b/docs/README.md
index 8cf8465b..a561df9f 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -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)|ℹ️|✔️|✔️|
|[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|ℹ️|✔️|❌|
|Id|Suppressed rule|Justification|
|--|---------------|-------------|
@@ -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
@@ -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
```
diff --git a/docs/Rules/MA0182.md b/docs/Rules/MA0182.md
new file mode 100644
index 00000000..127d2910
--- /dev/null
+++ b/docs/Rules/MA0182.md
@@ -0,0 +1,12 @@
+# MA0182 - Avoid unused internal types
+
+Source: [AvoidUnusedInternalTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs)
+
+
+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).
+
diff --git a/docs/comparison-with-other-analyzers.md b/docs/comparison-with-other-analyzers.md
index 722eb4c1..22f45d1b 100644
--- a/docs/comparison-with-other-analyzers.md
+++ b/docs/comparison-with-other-analyzers.md
@@ -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` |
| [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 |
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index 25de23de..d8096fe4 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -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
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index daf7983d..cdf5ff59 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -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
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index 92ab9c36..c607a3fc 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -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)
{
diff --git a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs
new file mode 100644
index 00000000..430d14d5
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs
@@ -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 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 _potentialUnusedTypes = [];
+ private readonly HashSet _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())
+ 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.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.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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs
new file mode 100644
index 00000000..cf4074f1
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs
@@ -0,0 +1,1005 @@
+using Meziantou.Analyzer.Rules;
+using Meziantou.Analyzer.Test.Helpers;
+using TestHelper;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class AvoidUnusedInternalTypesAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer();
+ }
+
+ [Fact]
+ public async Task PublicClass_NoDiagnostic()
+ {
+ const string SourceCode = """
+ public class PublicClass
+ {
+ public string Name { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AbstractClass_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal abstract class AbstractClass
+ {
+ public abstract void Method();
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StaticClass_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal static class StaticClass
+ {
+ public static void Method() { }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Interface_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal interface ITest
+ {
+ void Method();
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Enum_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal enum TestEnum
+ {
+ Value1,
+ Value2
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task UnusedInternalClass_Diagnostic()
+ {
+ const string SourceCode = """
+ internal class [|UnusedClass|]
+ {
+ public string Name { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task UnusedInternalStruct_Diagnostic()
+ {
+ const string SourceCode = """
+ internal struct [|UnusedStruct|]
+ {
+ public string Name { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task UnusedInternalRecord_Diagnostic()
+ {
+ const string SourceCode = """
+ internal record [|UnusedRecord|]
+ {
+ public string Name { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task UnusedInternalRecordStruct_Diagnostic()
+ {
+ const string SourceCode = """
+ internal record struct [|UnusedRecordStruct|]
+ {
+ public string Name { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+
+ [Fact]
+ public async Task InternalClassUsedInObjectCreation_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class UsedClass
+ {
+ public string Name { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var obj = new UsedClass();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalStructUsedInObjectCreation_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal struct UsedStruct
+ {
+ public string Name { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var obj = new UsedStruct();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalRecordUsedInObjectCreation_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal record UsedRecord(string Name);
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var obj = new UsedRecord("Test");
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task InternalRecordStructUsedInObjectCreation_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal record struct UsedRecordStruct(string Name);
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var obj = new UsedRecordStruct("Test");
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+
+ [Fact]
+ public async Task InternalClassUsedAsFieldType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Data
+ {
+ public int Value;
+ }
+
+ public class Container
+ {
+ internal Data _data;
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalStructUsedAsFieldType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal struct Data
+ {
+ public int Value;
+ }
+
+ public class Container
+ {
+ internal Data _data;
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalRecordUsedAsPropertyType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal record Settings(string Key, string Value);
+
+ public class Configuration
+ {
+ internal Settings AppSettings { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task InternalRecordStructUsedAsParameterType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal record struct Point(int X, int Y);
+
+ public class Graphics
+ {
+ internal void DrawAt(Point location)
+ {
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+
+ [Fact]
+ public async Task InternalStructUsedAsGenericTypeArgument_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System.Collections.Generic;
+
+ internal struct ItemData
+ {
+ public int Id { get; set; }
+ }
+
+ public class Service
+ {
+ internal List GetData()
+ {
+ return new List();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalRecordUsedInTypeOf_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal record Config(string Key);
+
+ public class Registry
+ {
+ public void Register()
+ {
+ var type = typeof(Config);
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task InternalRecordStructUsedInArrayCreation_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal record struct Vector(double X, double Y);
+
+ public class Math
+ {
+ public void Process()
+ {
+ var vectors = Array.Empty();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task MultipleInternalTypes_SomeUsedSomeNot()
+ {
+ const string SourceCode = """
+ internal class [|UnusedClass|]
+ {
+ public string Name { get; set; }
+ }
+
+ internal struct [|UnusedStruct|]
+ {
+ public int Value;
+ }
+
+ internal record [|UnusedRecord|](string Data);
+
+ internal record struct [|UnusedRecordStruct|](int Id);
+
+ internal class UsedClass
+ {
+ public string Value { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var obj = new UsedClass();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithTargetFramework(TargetFramework.Net9_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+
+ [Fact]
+ public async Task InternalClassUsedInTypeOfInAttribute_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class ConfigAttribute : Attribute
+ {
+ public Type Type { get; }
+
+ public ConfigAttribute(Type type)
+ {
+ Type = type;
+ }
+ }
+
+ internal sealed class MultiFrameworkConfig
+ {
+ }
+
+ [Config(typeof(MultiFrameworkConfig))]
+ internal static class Program
+ {
+ private static void Main(string[] args)
+ {
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInArrayCreation_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal sealed class Config
+ {
+ }
+
+ internal static class Program
+ {
+ private static void Main(string[] args)
+ {
+ var list = Array.Empty();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInGenericList_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System.Collections.Generic;
+
+ internal class Item
+ {
+ public string Name { get; set; }
+ }
+
+ public class Container
+ {
+ internal List- Items { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInNestedGenericType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System.Collections.Generic;
+
+ internal class InnerData
+ {
+ public int Value { get; set; }
+ }
+
+ public class Outer
+ {
+ internal Dictionary> Data { get; set; }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInMethodParameter_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Config
+ {
+ public string Value { get; set; }
+ }
+
+ public class Service
+ {
+ internal void ProcessConfig(Config config)
+ {
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedAsMethodReturnType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Result
+ {
+ public bool Success { get; set; }
+ }
+
+ public class Service
+ {
+ internal Result GetResult()
+ {
+ return new Result();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task MultipleInternalClasses_SomeUsedSomeNot()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal class [|UnusedClass|]
+ {
+ public string Name { get; set; }
+ }
+
+ internal class UsedClass
+ {
+ public string Value { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var obj = new UsedClass();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInMethodTypeParameter_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal class Settings
+ {
+ public string Value { get; set; }
+ }
+
+ public class Service
+ {
+ public T GetConfiguration() where T : new()
+ {
+ return new T();
+ }
+
+ public void Use()
+ {
+ var settings = GetConfiguration();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInActivatorCreateInstance_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal class DynamicClass
+ {
+ public string Name { get; set; }
+ }
+
+ public class Factory
+ {
+ public object Create()
+ {
+ return Activator.CreateInstance(typeof(DynamicClass));
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInLocalFunction_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Data
+ {
+ public int Value { get; set; }
+ }
+
+ public class Processor
+ {
+ public void Process()
+ {
+ void LocalFunc()
+ {
+ var data = new Data { Value = 42 };
+ }
+ LocalFunc();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassOnlyUsedAsTypeOf_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System;
+
+ internal class MetadataClass
+ {
+ }
+
+ public class Registry
+ {
+ public void Register()
+ {
+ var type = typeof(MetadataClass);
+ Console.WriteLine(type.Name);
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalSealedClass_UnusedClass_Diagnostic()
+ {
+ const string SourceCode = """
+ internal sealed class [|SealedUnusedClass|]
+ {
+ public void Method() { }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedAsGenericTypeArgumentForStaticMemberAccess_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Sample
+ {
+ public static int Empty { get; } = 0;
+ }
+
+ internal class InternalClass
+ {
+ }
+
+ public class Consumer
+ {
+ public void A()
+ {
+ _ = Sample.Empty;
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedByXmlSerializer_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System.IO;
+ using System.Xml.Serialization;
+
+ internal class InternalData
+ {
+ public string Name { get; set; }
+ public int Value { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var serializer = new XmlSerializer(typeof(InternalData));
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, new InternalData());
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedByNewtonsoftJsonSerializer_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using Newtonsoft.Json;
+
+ internal class InternalData
+ {
+ public string Name { get; set; }
+ public int Value { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ string json = "{}";
+ var data = JsonConvert.DeserializeObject(json);
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .AddNuGetReference("Newtonsoft.Json", "13.0.3", "lib/netstandard2.0/")
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedByYamlDotNetSerializer_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using YamlDotNet.Serialization;
+
+ internal class InternalData
+ {
+ public string Name { get; set; }
+ public int Value { get; set; }
+ }
+
+ public class Consumer
+ {
+ public void Method()
+ {
+ var deserializer = new DeserializerBuilder().Build();
+ var data = deserializer.Deserialize("name: test");
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .AddNuGetReference("YamlDotNet", "16.3.0", "lib/netstandard2.0/")
+ .WithSourceCode(SourceCode)
+ .WithTargetFramework(TargetFramework.NetStandard2_0)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInMethodGenericConstraint_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class BaseConfig
+ {
+ public string Value { get; set; }
+ }
+
+ public class Service
+ {
+ internal T Create() where T : BaseConfig, new()
+ {
+ return new T();
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInTypeGenericConstraint_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class BaseEntity
+ {
+ public int Id { get; set; }
+ }
+
+ internal class [|Repository|] where T : BaseEntity
+ {
+ public T Get(int id) => null;
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInMultipleGenericConstraints_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal interface IValidator
+ {
+ bool Validate();
+ }
+
+ internal class BaseModel
+ {
+ public string Name { get; set; }
+ }
+
+ internal class [|Processor|] where T : BaseModel, IValidator, new()
+ {
+ public void Process(T item)
+ {
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+#if CSHARP14_OR_GREATER
+ [Fact]
+ public async Task InternalClassUsedInImplicitExtensionType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class DataStore
+ {
+ public string Value { get; set; }
+ }
+
+ internal static class DataStoreExtensions
+ {
+ extension (DataStore datastore)
+ {
+ public void Save()
+ {
+ }
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInExplicitExtensionType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Settings
+ {
+ public string Key { get; set; }
+ }
+
+ internal static class DataStoreExtensions
+ {
+ extension (Settings settings)
+ {
+ public string GetValue() => settings.Key;
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInGenericExtensionType_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal class Entity
+ {
+ public int Id { get; set; }
+ }
+
+ internal static class EntityExtension
+ {
+ extension(T entity) where T : Entity
+ {
+ public void Delete()
+ {
+ }
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedInExtensionTypeWithMultipleConstraints_NoDiagnostic()
+ {
+ const string SourceCode = """
+ internal interface IIdentifiable
+ {
+ int Id { get; }
+ }
+
+ internal class BaseEntity
+ {
+ public string Name { get; set; }
+ }
+
+ internal static class RepositoryExtension
+ {
+ extension(T entity) where T : BaseEntity, IIdentifiable, new()
+ {
+ public void Save()
+ {
+ }
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InternalClassUsedAsExtensionTypeParameter_NoDiagnostic()
+ {
+ const string SourceCode = """
+ using System.Collections.Generic;
+
+ internal class Item
+ {
+ public string Name { get; set; }
+ }
+
+ public static class ListExtensions
+ {
+ extension (List
- items)
+ {
+ internal Item GetFirst() => items[0];
+ }
+ }
+ """;
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+}