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 +}